From 772b5c571900f2c5a3caa6330c4b7f1564a52d62 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 22 Oct 2025 21:11:08 +0530 Subject: [PATCH 001/212] [WEB-5201] chore: work item and inbox modal refactor (#7990) * chore: work item and inbbox modal refactor * chore: code refactor * chore: code refactor * chore: code refactor --- apps/web/core/components/issues/issue-modal/form.tsx | 1 + packages/types/src/de-dupe.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/core/components/issues/issue-modal/form.tsx b/apps/web/core/components/issues/issue-modal/form.tsx index 91c567daa..2d8e81f24 100644 --- a/apps/web/core/components/issues/issue-modal/form.tsx +++ b/apps/web/core/components/issues/issue-modal/form.tsx @@ -319,6 +319,7 @@ export const IssueFormRoot: FC = observer((props) => { { name: watch("name"), description_html: getTextContent(watch("description_html")), + issueId: data?.id, } ); diff --git a/packages/types/src/de-dupe.ts b/packages/types/src/de-dupe.ts index 539a151a7..e6105a0c0 100644 --- a/packages/types/src/de-dupe.ts +++ b/packages/types/src/de-dupe.ts @@ -3,7 +3,7 @@ import { TIssuePriorities } from "./issues"; export type TDuplicateIssuePayload = { title: string; workspace_id: string; - issue_id?: string; + issue_id?: string | null; project_id?: string; description_stripped?: string; }; From 5fa9943b661d21566dffc4106db770ded2f2a7b2 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Thu, 23 Oct 2025 00:27:16 +0530 Subject: [PATCH 002/212] [WEB-5141] chore: project features refactor (#7960) * chore: project features refactor * fix: enable/disable condition for toggle * fix: dependencies * chore: lint fix * chore: lint fix * chore: added fallback exports --- .../projects/[projectId]/features/page.tsx | 2 +- .../projects/settings/features-list.tsx | 1 + .../constants/project/settings/features.tsx | 1 + .../modals/existing-issues-list-modal.tsx | 10 ++--- .../project/settings/features-list.tsx | 15 +++---- .../components/project/settings/helper.tsx | 41 +++++++++++++++++++ .../core/components/sidebar/add-button.tsx | 2 +- .../core/components/sidebar/search-button.tsx | 2 +- apps/web/core/store/project/project.store.ts | 2 +- .../projects/settings/features-list.tsx | 1 + 10 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 apps/web/ce/components/projects/settings/features-list.tsx create mode 100644 apps/web/core/components/project/settings/helper.tsx create mode 100644 apps/web/ee/components/projects/settings/features-list.tsx diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx index 40d2bed66..730177e13 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx @@ -6,11 +6,11 @@ import { useParams } from "next/navigation"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; import { PageHead } from "@/components/core/page-title"; -import { ProjectFeaturesList } from "@/components/project/settings/features-list"; import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; // hooks import { useProject } from "@/hooks/store/use-project"; import { useUserPermissions } from "@/hooks/store/user"; +import { ProjectFeaturesList } from "@/plane-web/components/projects/settings/features-list"; const FeaturesSettingsPage = observer(() => { const { workspaceSlug, projectId } = useParams(); diff --git a/apps/web/ce/components/projects/settings/features-list.tsx b/apps/web/ce/components/projects/settings/features-list.tsx new file mode 100644 index 000000000..26fc591fd --- /dev/null +++ b/apps/web/ce/components/projects/settings/features-list.tsx @@ -0,0 +1 @@ +export { ProjectFeaturesList } from "@/components/project/settings/features-list"; diff --git a/apps/web/ce/constants/project/settings/features.tsx b/apps/web/ce/constants/project/settings/features.tsx index b86135f08..380272ea4 100644 --- a/apps/web/ce/constants/project/settings/features.tsx +++ b/apps/web/ce/constants/project/settings/features.tsx @@ -13,6 +13,7 @@ export type TProperties = { isPro: boolean; isEnabled: boolean; renderChildren?: (currentProjectDetails: IProject, workspaceSlug: string) => ReactNode; + href?: string; }; type TProjectBaseFeatureKeys = "cycles" | "modules" | "views" | "pages" | "inbox"; diff --git a/apps/web/core/components/core/modals/existing-issues-list-modal.tsx b/apps/web/core/components/core/modals/existing-issues-list-modal.tsx index 41a8e0688..b8c3eaf8b 100644 --- a/apps/web/core/components/core/modals/existing-issues-list-modal.tsx +++ b/apps/web/core/components/core/modals/existing-issues-list-modal.tsx @@ -33,7 +33,7 @@ type Props = { handleOnSubmit: (data: ISearchIssueResponse[]) => Promise; workspaceLevelToggle?: boolean; shouldHideIssue?: (issue: ISearchIssueResponse) => boolean; - selectedWorkItems?: ISearchIssueResponse[]; + selectedWorkItemIds?: string[]; workItemSearchServiceCallback?: (params: TProjectIssuesSearchParams) => Promise; }; @@ -51,7 +51,7 @@ export const ExistingIssuesListModal: React.FC = (props) => { handleOnSubmit, workspaceLevelToggle = false, shouldHideIssue, - selectedWorkItems, + selectedWorkItemIds, workItemSearchServiceCallback, } = props; // states @@ -117,10 +117,10 @@ export const ExistingIssuesListModal: React.FC = (props) => { }; useEffect(() => { - if (selectedWorkItems) { - setSelectedIssues(selectedWorkItems); + if (selectedWorkItemIds) { + setSelectedIssues(issues.filter((issue) => selectedWorkItemIds.includes(issue.id))); } - }, [isOpen, selectedWorkItems]); + }, [isOpen, selectedWorkItemIds]); useEffect(() => { handleSearch(); diff --git a/apps/web/core/components/project/settings/features-list.tsx b/apps/web/core/components/project/settings/features-list.tsx index c919b7c9e..8024def86 100644 --- a/apps/web/core/components/project/settings/features-list.tsx +++ b/apps/web/core/components/project/settings/features-list.tsx @@ -3,12 +3,11 @@ import type { FC } from "react"; import { observer } from "mobx-react"; // plane imports -import { PROJECT_TRACKER_ELEMENTS, PROJECT_TRACKER_EVENTS } from "@plane/constants"; +import { PROJECT_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { setPromiseToast } from "@plane/propel/toast"; import { Tooltip } from "@plane/propel/tooltip"; import type { IProject } from "@plane/types"; -import { ToggleSwitch } from "@plane/ui"; // components import { SettingsHeading } from "@/components/settings/heading"; // helpers @@ -19,6 +18,7 @@ import { useUser } from "@/hooks/store/user"; // plane web imports import { UpgradeBadge } from "@/plane-web/components/workspace/upgrade-badge"; import { PROJECT_FEATURES_LIST } from "@/plane-web/constants/project/settings"; +import { ProjectFeatureToggle } from "./helper"; type Props = { workspaceSlug: string; @@ -96,12 +96,13 @@ export const ProjectFeaturesList: FC = observer((props) => {

- handleSubmit(featureItemKey, featureItem.property)} - disabled={!featureItem.isEnabled || !isAdmin} - size="sm" - data-ph-element={PROJECT_TRACKER_ELEMENTS.TOGGLE_FEATURE} + handleSubmit={handleSubmit} + disabled={!isAdmin} />
diff --git a/apps/web/core/components/project/settings/helper.tsx b/apps/web/core/components/project/settings/helper.tsx new file mode 100644 index 000000000..0b7b18432 --- /dev/null +++ b/apps/web/core/components/project/settings/helper.tsx @@ -0,0 +1,41 @@ +import Link from "next/link"; +import { ChevronRight } from "lucide-react"; +import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; +import { EPillVariant, Pill, EPillSize } from "@plane/propel/pill"; +import { ToggleSwitch } from "@plane/ui"; +import type { TProperties } from "@/plane-web/constants/project/settings/features"; + +type Props = { + workspaceSlug: string; + projectId: string; + featureItem: TProperties; + value: boolean; + handleSubmit: (featureKey: string, featureProperty: string) => void; + disabled?: boolean; +}; + +export const ProjectFeatureToggle = (props: Props) => { + const { workspaceSlug, projectId, featureItem, value, handleSubmit, disabled } = props; + return featureItem.href ? ( + +
+ + {value ? "Enabled" : "Disabled"} + + +
+ + ) : ( + handleSubmit(featureItem.key, featureItem.property)} + disabled={disabled} + size="sm" + data-ph-element={PROJECT_TRACKER_ELEMENTS.TOGGLE_FEATURE} + /> + ); +}; diff --git a/apps/web/core/components/sidebar/add-button.tsx b/apps/web/core/components/sidebar/add-button.tsx index 7634e508d..cea37334f 100644 --- a/apps/web/core/components/sidebar/add-button.tsx +++ b/apps/web/core/components/sidebar/add-button.tsx @@ -1,5 +1,5 @@ -import React from "react"; import type { FC } from "react"; +import React from "react"; import { cn } from "@plane/utils"; type Props = React.ComponentProps<"button"> & { diff --git a/apps/web/core/components/sidebar/search-button.tsx b/apps/web/core/components/sidebar/search-button.tsx index cf8614905..d87adf84b 100644 --- a/apps/web/core/components/sidebar/search-button.tsx +++ b/apps/web/core/components/sidebar/search-button.tsx @@ -1,5 +1,5 @@ -import React from "react"; import type { FC } from "react"; +import React from "react"; import { Search } from "lucide-react"; import { cn } from "@plane/utils"; diff --git a/apps/web/core/store/project/project.store.ts b/apps/web/core/store/project/project.store.ts index fa99f77f1..1f4cd0802 100644 --- a/apps/web/core/store/project/project.store.ts +++ b/apps/web/core/store/project/project.store.ts @@ -12,7 +12,7 @@ import { ProjectService, ProjectStateService, ProjectArchiveService } from "@/se // store import type { CoreRootStore } from "../root.store"; -type ProjectOverviewCollapsible = "links" | "attachments"; +type ProjectOverviewCollapsible = "links" | "attachments" | "milestones"; export interface IProjectStore { // observables diff --git a/apps/web/ee/components/projects/settings/features-list.tsx b/apps/web/ee/components/projects/settings/features-list.tsx new file mode 100644 index 000000000..26fc591fd --- /dev/null +++ b/apps/web/ee/components/projects/settings/features-list.tsx @@ -0,0 +1 @@ +export { ProjectFeaturesList } from "@/components/project/settings/features-list"; From 68aa2fe0b8d889dfde5ca8793a71d59479029fb7 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Thu, 23 Oct 2025 00:29:05 +0530 Subject: [PATCH 003/212] [WIKI-553] chore: improved pages components tracking (#7966) * chore: page components tracking * chore: changed the transaction task * chore: added logger for description html --- apps/api/plane/app/views/page/base.py | 25 ++- .../plane/bgtasks/page_transaction_task.py | 148 +++++++++++++----- apps/api/plane/utils/content_validator.py | 9 +- 3 files changed, 129 insertions(+), 53 deletions(-) diff --git a/apps/api/plane/app/views/page/base.py b/apps/api/plane/app/views/page/base.py index 72fb4ef8e..b8946d22a 100644 --- a/apps/api/plane/app/views/page/base.py +++ b/apps/api/plane/app/views/page/base.py @@ -137,7 +137,11 @@ class PageViewSet(BaseViewSet): if serializer.is_valid(): serializer.save() # capture the page transaction - page_transaction.delay(request.data, None, serializer.data["id"]) + page_transaction.delay( + new_description_html=request.data.get("description_html", "

"), + old_description_html=None, + page_id=serializer.data["id"], + ) page = self.get_queryset().get(pk=serializer.data["id"]) serializer = PageDetailSerializer(page) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -168,11 +172,8 @@ class PageViewSet(BaseViewSet): # capture the page transaction if request.data.get("description_html"): page_transaction.delay( - new_value=request.data, - old_value=json.dumps( - {"description_html": page_description}, - cls=DjangoJSONEncoder, - ), + new_description_html=request.data.get("description_html", "

"), + old_description_html=page_description, page_id=page_id, ) @@ -504,7 +505,11 @@ class PagesDescriptionViewSet(BaseViewSet): if serializer.is_valid(): # Capture the page transaction if request.data.get("description_html"): - page_transaction.delay(new_value=request.data, old_value=existing_instance, page_id=page_id) + page_transaction.delay( + new_description_html=request.data.get("description_html", "

"), + old_description_html=page.description_html, + page_id=page_id, + ) # Update the page using serializer updated_page = serializer.save() @@ -550,7 +555,11 @@ class PageDuplicateEndpoint(BaseAPIView): updated_by_id=page.updated_by_id, ) - page_transaction.delay({"description_html": page.description_html}, None, page.id) + page_transaction.delay( + new_description_html=page.description_html, + old_description_html=None, + page_id=page.id, + ) # Copy the s3 objects uploaded in the page copy_s3_objects_of_description_and_assets.delay( diff --git a/apps/api/plane/bgtasks/page_transaction_task.py b/apps/api/plane/bgtasks/page_transaction_task.py index 09e2cb2ad..402d0a3ee 100644 --- a/apps/api/plane/bgtasks/page_transaction_task.py +++ b/apps/api/plane/bgtasks/page_transaction_task.py @@ -1,5 +1,5 @@ # Python imports -import json +import logging # Django imports from django.utils import timezone @@ -7,72 +7,134 @@ from django.utils import timezone # Third-party imports from bs4 import BeautifulSoup -# Module imports -from plane.db.models import Page, PageLog +# App imports from celery import shared_task +from plane.db.models import Page, PageLog from plane.utils.exception_logger import log_exception +logger = logging.getLogger("plane.worker") -def extract_components(value, tag): +COMPONENT_MAP = { + "mention-component": { + "attributes": ["id", "entity_identifier", "entity_name", "entity_type"], + "extract": lambda m: { + "entity_name": m.get("entity_name"), + "entity_type": None, + "entity_identifier": m.get("entity_identifier"), + }, + }, + "image-component": { + "attributes": ["id", "src"], + "extract": lambda m: { + "entity_name": "image", + "entity_type": None, + "entity_identifier": m.get("src"), + }, + }, +} + +component_map = { + **COMPONENT_MAP, +} + + +def extract_all_components(description_html): + """ + Extracts all component types from the HTML value in a single pass. + Returns a dict mapping component_type -> list of extracted entities. + """ try: - mentions = [] - html = value.get("description_html") - soup = BeautifulSoup(html, "html.parser") - mention_tags = soup.find_all(tag) + if not description_html: + return {component: [] for component in component_map.keys()} - for mention_tag in mention_tags: - mention = { - "id": mention_tag.get("id"), - "entity_identifier": mention_tag.get("entity_identifier"), - "entity_name": mention_tag.get("entity_name"), - } - mentions.append(mention) + soup = BeautifulSoup(description_html, "html.parser") + results = {} + + for component, config in component_map.items(): + attributes = config.get("attributes", ["id"]) + component_tags = soup.find_all(component) + + entities = [] + for tag in component_tags: + entity = {attr: tag.get(attr) for attr in attributes} + entities.append(entity) + + results[component] = entities + + return results - return mentions except Exception: - return [] + return {component: [] for component in component_map.keys()} + + +def get_entity_details(component: str, mention: dict): + """ + Normalizes mention attributes into entity_name, entity_type, entity_identifier. + """ + config = component_map.get(component) + if not config: + return {"entity_name": None, "entity_type": None, "entity_identifier": None} + return config["extract"](mention) @shared_task -def page_transaction(new_value, old_value, page_id): +def page_transaction(new_description_html, old_description_html, page_id): + """ + Tracks changes in page content (mentions, embeds, etc.) + and logs them in PageLog for audit and reference. + """ try: page = Page.objects.get(pk=page_id) - new_page_mention = PageLog.objects.filter(page_id=page_id).exists() - old_value = json.loads(old_value) if old_value else {} + has_existing_logs = PageLog.objects.filter(page_id=page_id).exists() + + + # Extract all components in a single pass (optimized) + old_components = extract_all_components(old_description_html) + new_components = extract_all_components(new_description_html) new_transactions = [] deleted_transaction_ids = set() - # TODO - Add "issue-embed-component", "img", "todo" components - components = ["mention-component"] - for component in components: - old_mentions = extract_components(old_value, component) - new_mentions = extract_components(new_value, component) + for component in component_map.keys(): + old_entities = old_components[component] + new_entities = new_components[component] - new_mentions_ids = {mention["id"] for mention in new_mentions} - old_mention_ids = {mention["id"] for mention in old_mentions} - deleted_transaction_ids.update(old_mention_ids - new_mentions_ids) + old_ids = {m.get("id") for m in old_entities if m.get("id")} + new_ids = {m.get("id") for m in new_entities if m.get("id")} + deleted_transaction_ids.update(old_ids - new_ids) - new_transactions.extend( - PageLog( - transaction=mention["id"], - page_id=page_id, - entity_identifier=mention["entity_identifier"], - entity_name=mention["entity_name"], - workspace_id=page.workspace_id, - created_at=timezone.now(), - updated_at=timezone.now(), + for mention in new_entities: + mention_id = mention.get("id") + if not mention_id or (mention_id in old_ids and has_existing_logs): + continue + + details = get_entity_details(component, mention) + current_time = timezone.now() + + new_transactions.append( + PageLog( + transaction=mention_id, + page_id=page_id, + entity_identifier=details["entity_identifier"], + entity_name=details["entity_name"], + entity_type=details["entity_type"], + workspace_id=page.workspace_id, + created_at=current_time, + updated_at=current_time, + ) ) - for mention in new_mentions - if mention["id"] not in old_mention_ids or not new_page_mention + + + # Bulk insert and cleanup + if new_transactions: + PageLog.objects.bulk_create( + new_transactions, batch_size=50, ignore_conflicts=True ) - # Create new PageLog objects for new transactions - PageLog.objects.bulk_create(new_transactions, batch_size=10, ignore_conflicts=True) + if deleted_transaction_ids: + PageLog.objects.filter(transaction__in=deleted_transaction_ids).delete() - # Delete the removed transactions - PageLog.objects.filter(transaction__in=deleted_transaction_ids).delete() except Page.DoesNotExist: return except Exception as e: diff --git a/apps/api/plane/utils/content_validator.py b/apps/api/plane/utils/content_validator.py index 5163fad7d..ff06a562f 100644 --- a/apps/api/plane/utils/content_validator.py +++ b/apps/api/plane/utils/content_validator.py @@ -4,7 +4,9 @@ import nh3 from plane.utils.exception_logger import log_exception from bs4 import BeautifulSoup from collections import defaultdict +import logging +logger = logging.getLogger("plane.api") # Maximum allowed size for binary data (10MB) MAX_SIZE = 10 * 1024 * 1024 @@ -54,7 +56,9 @@ def validate_binary_data(data): # Check for suspicious text patterns (HTML/JS) try: decoded_text = binary_data.decode("utf-8", errors="ignore")[:200] - if any(pattern in decoded_text.lower() for pattern in SUSPICIOUS_BINARY_PATTERNS): + if any( + pattern in decoded_text.lower() for pattern in SUSPICIOUS_BINARY_PATTERNS + ): return False, "Binary data contains suspicious content patterns" except Exception: pass # Binary data might not be decodable as text, which is fine @@ -232,8 +236,9 @@ def validate_html_content(html_content: str): summary = json.dumps(diff) except Exception: summary = str(diff) + logger.warning(f"HTML sanitization removals: {summary}") log_exception( - f"HTML sanitization removals: {summary}", + ValueError(f"HTML sanitization removals: {summary}"), warning=True, ) return True, None, clean_html From f94da6859729c06e52f48bae070ff7094cf50d75 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Thu, 23 Oct 2025 00:29:32 +0530 Subject: [PATCH 004/212] [WEB-5153] chore: optimised the cycle transfer issues (#7969) * chore: optimised the cycle transfer issues * chore: added more vlaidation in transfer * chore: improve the comments --- apps/api/plane/api/views/cycle.py | 517 +++-------------- apps/api/plane/app/views/cycle/base.py | 533 ++++-------------- apps/api/plane/utils/cycle_transfer_issues.py | 486 ++++++++++++++++ 3 files changed, 688 insertions(+), 848 deletions(-) create mode 100644 apps/api/plane/utils/cycle_transfer_issues.py diff --git a/apps/api/plane/api/views/cycle.py b/apps/api/plane/api/views/cycle.py index 1908ceada..849dab340 100644 --- a/apps/api/plane/api/views/cycle.py +++ b/apps/api/plane/api/views/cycle.py @@ -12,13 +12,7 @@ from django.db.models import ( OuterRef, Q, Sum, - FloatField, - Case, - When, - Value, ) -from django.db.models.functions import Cast, Concat -from django.db import models # Third party imports from rest_framework import status @@ -47,7 +41,7 @@ from plane.db.models import ( ProjectMember, UserFavorite, ) -from plane.utils.analytics_plot import burndown_plot +from plane.utils.cycle_transfer_issues import transfer_cycle_issues from plane.utils.host import base_host from .base import BaseAPIView from plane.bgtasks.webhook_task import model_activity @@ -201,7 +195,9 @@ class CycleListCreateAPIEndpoint(BaseAPIView): # Current Cycle if cycle_view == "current": - queryset = queryset.filter(start_date__lte=timezone.now(), end_date__gte=timezone.now()) + queryset = queryset.filter( + start_date__lte=timezone.now(), end_date__gte=timezone.now() + ) data = CycleSerializer( queryset, many=True, @@ -258,7 +254,9 @@ class CycleListCreateAPIEndpoint(BaseAPIView): # Incomplete Cycles if cycle_view == "incomplete": - queryset = queryset.filter(Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True)) + queryset = queryset.filter( + Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True) + ) return self.paginate( request=request, queryset=(queryset), @@ -304,11 +302,17 @@ class CycleListCreateAPIEndpoint(BaseAPIView): Create a new development cycle with specified name, description, and date range. Supports external ID tracking for integration purposes. """ - if (request.data.get("start_date", None) is None and request.data.get("end_date", None) is None) or ( - request.data.get("start_date", None) is not None and request.data.get("end_date", None) is not None + if ( + request.data.get("start_date", None) is None + and request.data.get("end_date", None) is None + ) or ( + request.data.get("start_date", None) is not None + and request.data.get("end_date", None) is not None ): - serializer = CycleCreateSerializer(data=request.data, context={"request": request}) + serializer = CycleCreateSerializer( + data=request.data, context={"request": request} + ) if serializer.is_valid(): if ( request.data.get("external_id") @@ -351,7 +355,9 @@ class CycleListCreateAPIEndpoint(BaseAPIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) else: return Response( - {"error": "Both start date and end date are either required or are to be null"}, + { + "error": "Both start date and end date are either required or are to be null" + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -499,7 +505,9 @@ class CycleDetailAPIEndpoint(BaseAPIView): """ cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) - current_instance = json.dumps(CycleSerializer(cycle).data, cls=DjangoJSONEncoder) + current_instance = json.dumps( + CycleSerializer(cycle).data, cls=DjangoJSONEncoder + ) if cycle.archived_at: return Response( @@ -512,14 +520,20 @@ class CycleDetailAPIEndpoint(BaseAPIView): if cycle.end_date is not None and cycle.end_date < timezone.now(): if "sort_order" in request_data: # Can only change sort order - request_data = {"sort_order": request_data.get("sort_order", cycle.sort_order)} + request_data = { + "sort_order": request_data.get("sort_order", cycle.sort_order) + } else: return Response( - {"error": "The Cycle has already been completed so it cannot be edited"}, + { + "error": "The Cycle has already been completed so it cannot be edited" + }, status=status.HTTP_400_BAD_REQUEST, ) - serializer = CycleUpdateSerializer(cycle, data=request.data, partial=True, context={"request": request}) + serializer = CycleUpdateSerializer( + cycle, data=request.data, partial=True, context={"request": request} + ) if serializer.is_valid(): if ( request.data.get("external_id") @@ -527,7 +541,9 @@ class CycleDetailAPIEndpoint(BaseAPIView): and Cycle.objects.filter( project_id=project_id, workspace__slug=slug, - external_source=request.data.get("external_source", cycle.external_source), + external_source=request.data.get( + "external_source", cycle.external_source + ), external_id=request.data.get("external_id"), ).exists() ): @@ -584,7 +600,11 @@ class CycleDetailAPIEndpoint(BaseAPIView): status=status.HTTP_403_FORBIDDEN, ) - cycle_issues = list(CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list("issue", flat=True)) + cycle_issues = list( + CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list( + "issue", flat=True + ) + ) issue_activity.delay( type="cycle.activity.deleted", @@ -604,7 +624,9 @@ class CycleDetailAPIEndpoint(BaseAPIView): # Delete the cycle cycle.delete() # Delete the user favorite cycle - UserFavorite.objects.filter(entity_type="cycle", entity_identifier=pk, project_id=project_id).delete() + UserFavorite.objects.filter( + entity_type="cycle", entity_identifier=pk, project_id=project_id + ).delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -742,7 +764,9 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView): return self.paginate( request=request, queryset=(self.get_queryset()), - on_results=lambda cycles: CycleSerializer(cycles, many=True, fields=self.fields, expand=self.expand).data, + on_results=lambda cycles: CycleSerializer( + cycles, many=True, fields=self.fields, expand=self.expand + ).data, ) @cycle_docs( @@ -761,7 +785,9 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView): Move a completed cycle to archived status for historical tracking. Only cycles that have ended can be archived. """ - cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug) + cycle = Cycle.objects.get( + pk=cycle_id, project_id=project_id, workspace__slug=slug + ) if cycle.end_date >= timezone.now(): return Response( {"error": "Only completed cycles can be archived"}, @@ -792,7 +818,9 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView): Restore an archived cycle to active status, making it available for regular use. The cycle will reappear in active cycle lists. """ - cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug) + cycle = Cycle.objects.get( + pk=cycle_id, project_id=project_id, workspace__slug=slug + ) cycle.archived_at = None cycle.save() return Response(status=status.HTTP_204_NO_CONTENT) @@ -855,7 +883,9 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView): # List order_by = request.GET.get("order_by", "created_at") issues = ( - Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True) + Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True + ) .annotate( sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) .order_by() @@ -892,7 +922,9 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView): return self.paginate( request=request, queryset=(issues), - on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data, + on_results=lambda issues: IssueSerializer( + issues, many=True, fields=self.fields, expand=self.expand + ).data, ) @cycle_docs( @@ -922,10 +954,13 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView): if not issues: return Response( - {"error": "Work items are required", "code": "MISSING_WORK_ITEMS"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Work items are required", "code": "MISSING_WORK_ITEMS"}, + status=status.HTTP_400_BAD_REQUEST, ) - cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=cycle_id) + cycle = Cycle.objects.get( + workspace__slug=slug, project_id=project_id, pk=cycle_id + ) if cycle.end_date is not None and cycle.end_date < timezone.now(): return Response( @@ -937,9 +972,13 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView): ) # Get all CycleWorkItems already created - cycle_issues = list(CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)) + cycle_issues = list( + CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues) + ) existing_issues = [ - str(cycle_issue.issue_id) for cycle_issue in cycle_issues if str(cycle_issue.issue_id) in issues + str(cycle_issue.issue_id) + for cycle_issue in cycle_issues + if str(cycle_issue.issue_id) in issues ] new_issues = list(set(issues) - set(existing_issues)) @@ -990,7 +1029,9 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView): current_instance=json.dumps( { "updated_cycle_issues": update_cycle_issue_activity, - "created_cycle_issues": serializers.serialize("json", created_records), + "created_cycle_issues": serializers.serialize( + "json", created_records + ), } ), epoch=int(timezone.now().timestamp()), @@ -1066,7 +1107,9 @@ class CycleIssueDetailAPIEndpoint(BaseAPIView): cycle_id=cycle_id, issue_id=issue_id, ) - serializer = CycleIssueSerializer(cycle_issue, fields=self.fields, expand=self.expand) + serializer = CycleIssueSerializer( + cycle_issue, fields=self.fields, expand=self.expand + ) return Response(serializer.data, status=status.HTTP_200_OK) @cycle_docs( @@ -1171,406 +1214,34 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): {"error": "New Cycle Id is required"}, status=status.HTTP_400_BAD_REQUEST, ) - - new_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=new_cycle_id).first() - - old_cycle = ( - Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id) - .annotate( - total_issues=Count( - "issue_cycle", - filter=Q( - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - issue_cycle__deleted_at__isnull=True, - issue_cycle__issue__deleted_at__isnull=True, - ), - ) - ) - .annotate( - completed_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="completed", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - issue_cycle__deleted_at__isnull=True, - issue_cycle__issue__deleted_at__isnull=True, - ), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="cancelled", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - issue_cycle__deleted_at__isnull=True, - ), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="started", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - issue_cycle__deleted_at__isnull=True, - ), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="unstarted", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - issue_cycle__deleted_at__isnull=True, - ), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="backlog", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - issue_cycle__deleted_at__isnull=True, - ), - ) - ) - ) - old_cycle = old_cycle.first() - - estimate_type = Project.objects.filter( + + old_cycle = Cycle.objects.get( workspace__slug=slug, - pk=project_id, - estimate__isnull=False, - estimate__type="points", - ).exists() - - if estimate_type: - assignee_estimate_data = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(display_name=F("assignees__display_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate(avatar=F("assignees__avatar")) - .annotate( - avatar_url=Case( - # If `avatar_asset` exists, use it to generate the asset URL - When( - assignees__avatar_asset__isnull=False, - then=Concat( - Value("/api/assets/v2/static/"), - "assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field - Value("/"), - ), - ), - # If `avatar_asset` is None, fall back to using `avatar` field directly - When( - assignees__avatar_asset__isnull=True, - then="assignees__avatar", - ), - default=Value(None), - output_field=models.CharField(), - ) - ) - .values("display_name", "assignee_id", "avatar", "avatar_url") - .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField()))) - .annotate( - completed_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("display_name") - ) - # assignee distribution serialization - assignee_estimate_distribution = [ - { - "display_name": item["display_name"], - "assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None), - "avatar": item.get("avatar", None), - "avatar_url": item.get("avatar_url", None), - "total_estimates": item["total_estimates"], - "completed_estimates": item["completed_estimates"], - "pending_estimates": item["pending_estimates"], - } - for item in assignee_estimate_data - ] - - label_distribution_data = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(label_name=F("labels__name")) - .annotate(color=F("labels__color")) - .annotate(label_id=F("labels__id")) - .values("label_name", "color", "label_id") - .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField()))) - .annotate( - completed_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("label_name") - ) - - estimate_completion_chart = burndown_plot( - queryset=old_cycle, - slug=slug, - project_id=project_id, - plot_type="points", - cycle_id=cycle_id, - ) - # Label distribution serialization - label_estimate_distribution = [ - { - "label_name": item["label_name"], - "color": item["color"], - "label_id": (str(item["label_id"]) if item["label_id"] else None), - "total_estimates": item["total_estimates"], - "completed_estimates": item["completed_estimates"], - "pending_estimates": item["pending_estimates"], - } - for item in label_distribution_data - ] - - # Get the assignee distribution - assignee_distribution = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(display_name=F("assignees__display_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate(avatar=F("assignees__avatar")) - .annotate( - avatar_url=Case( - # If `avatar_asset` exists, use it to generate the asset URL - When( - assignees__avatar_asset__isnull=False, - then=Concat( - Value("/api/assets/v2/static/"), - "assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field - Value("/"), - ), - ), - # If `avatar_asset` is None, fall back to using `avatar` field directly - When(assignees__avatar_asset__isnull=True, then="assignees__avatar"), - default=Value(None), - output_field=models.CharField(), - ) - ) - .values("display_name", "assignee_id", "avatar_url") - .annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False))) - .annotate( - completed_issues=Count( - "id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("display_name") - ) - # assignee distribution serialized - assignee_distribution_data = [ - { - "display_name": item["display_name"], - "assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None), - "avatar": item.get("avatar", None), - "avatar_url": item.get("avatar_url", None), - "total_issues": item["total_issues"], - "completed_issues": item["completed_issues"], - "pending_issues": item["pending_issues"], - } - for item in assignee_distribution - ] - - # Get the label distribution - label_distribution = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(label_name=F("labels__name")) - .annotate(color=F("labels__color")) - .annotate(label_id=F("labels__id")) - .values("label_name", "color", "label_id") - .annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False))) - .annotate( - completed_issues=Count( - "id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("label_name") - ) - - # Label distribution serilization - label_distribution_data = [ - { - "label_name": item["label_name"], - "color": item["color"], - "label_id": (str(item["label_id"]) if item["label_id"] else None), - "total_issues": item["total_issues"], - "completed_issues": item["completed_issues"], - "pending_issues": item["pending_issues"], - } - for item in label_distribution - ] - - # Pass the new_cycle queryset to burndown_plot - completion_chart = burndown_plot( - queryset=old_cycle, - slug=slug, project_id=project_id, - plot_type="issues", - cycle_id=cycle_id, + pk=cycle_id, ) - - current_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id).first() - - current_cycle.progress_snapshot = { - "total_issues": old_cycle.total_issues, - "completed_issues": old_cycle.completed_issues, - "cancelled_issues": old_cycle.cancelled_issues, - "started_issues": old_cycle.started_issues, - "unstarted_issues": old_cycle.unstarted_issues, - "backlog_issues": old_cycle.backlog_issues, - "distribution": { - "labels": label_distribution_data, - "assignees": assignee_distribution_data, - "completion_chart": completion_chart, - }, - "estimate_distribution": ( - {} - if not estimate_type - else { - "labels": label_estimate_distribution, - "assignees": assignee_estimate_distribution, - "completion_chart": estimate_completion_chart, - } - ), - } - current_cycle.save(update_fields=["progress_snapshot"]) - - if new_cycle.end_date is not None and new_cycle.end_date < timezone.now(): + # transfer work items only when cycle is completed (passed the end data) + if old_cycle.end_date is not None and old_cycle.end_date < timezone.now(): return Response( - {"error": "The cycle where the issues are transferred is already completed"}, + {"error": "The old cycle is not completed yet"}, status=status.HTTP_400_BAD_REQUEST, ) - cycle_issues = CycleIssue.objects.filter( - cycle_id=cycle_id, + # Call the utility function to handle the transfer + result = transfer_cycle_issues( + slug=slug, project_id=project_id, - workspace__slug=slug, - issue__state__group__in=["backlog", "unstarted", "started"], + cycle_id=cycle_id, + new_cycle_id=new_cycle_id, + request=request, + user_id=self.request.user.id, ) - updated_cycles = [] - update_cycle_issue_activity = [] - for cycle_issue in cycle_issues: - cycle_issue.cycle_id = new_cycle_id - updated_cycles.append(cycle_issue) - update_cycle_issue_activity.append( - { - "old_cycle_id": str(cycle_id), - "new_cycle_id": str(new_cycle_id), - "issue_id": str(cycle_issue.issue_id), - } + # Handle the result + if result.get("success"): + return Response({"message": "Success"}, status=status.HTTP_200_OK) + else: + return Response( + {"error": result.get("error")}, + status=status.HTTP_400_BAD_REQUEST, ) - - cycle_issues = CycleIssue.objects.bulk_update(updated_cycles, ["cycle_id"], batch_size=100) - - # Capture Issue Activity - issue_activity.delay( - type="cycle.activity.created", - requested_data=json.dumps({"cycles_list": []}), - actor_id=str(self.request.user.id), - issue_id=None, - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "updated_cycle_issues": update_cycle_issue_activity, - "created_cycle_issues": "[]", - } - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=base_host(request=request, is_app=True), - ) - - return Response({"message": "Success"}, status=status.HTTP_200_OK) diff --git a/apps/api/plane/app/views/cycle/base.py b/apps/api/plane/app/views/cycle/base.py index 711d6724a..712d71754 100644 --- a/apps/api/plane/app/views/cycle/base.py +++ b/apps/api/plane/app/views/cycle/base.py @@ -51,6 +51,7 @@ from plane.db.models import ( from plane.utils.analytics_plot import burndown_plot from plane.bgtasks.recent_visited_task import recent_visited_task from plane.utils.host import base_host +from plane.utils.cycle_transfer_issues import transfer_cycle_issues from .. import BaseAPIView, BaseViewSet from plane.bgtasks.webhook_task import model_activity from plane.utils.timezone_converter import convert_to_utc, user_timezone_converter @@ -96,7 +97,9 @@ class CycleViewSet(BaseViewSet): .prefetch_related( Prefetch( "issue_cycle__issue__assignees", - queryset=User.objects.only("avatar_asset", "first_name", "id").distinct(), + queryset=User.objects.only( + "avatar_asset", "first_name", "id" + ).distinct(), ) ) .prefetch_related( @@ -147,7 +150,8 @@ class CycleViewSet(BaseViewSet): .annotate( status=Case( When( - Q(start_date__lte=current_time_in_utc) & Q(end_date__gte=current_time_in_utc), + Q(start_date__lte=current_time_in_utc) + & Q(end_date__gte=current_time_in_utc), then=Value("CURRENT"), ), When(start_date__gt=current_time_in_utc, then=Value("UPCOMING")), @@ -166,7 +170,11 @@ class CycleViewSet(BaseViewSet): "issue_cycle__issue__assignees__id", distinct=True, filter=~Q(issue_cycle__issue__assignees__id__isnull=True) - & (Q(issue_cycle__issue__issue_assignee__deleted_at__isnull=True)), + & ( + Q( + issue_cycle__issue__issue_assignee__deleted_at__isnull=True + ) + ), ), Value([], output_field=ArrayField(UUIDField())), ) @@ -197,7 +205,9 @@ class CycleViewSet(BaseViewSet): # Current Cycle if cycle_view == "current": - queryset = queryset.filter(start_date__lte=current_time_in_utc, end_date__gte=current_time_in_utc) + queryset = queryset.filter( + start_date__lte=current_time_in_utc, end_date__gte=current_time_in_utc + ) data = queryset.values( # necessary fields @@ -264,10 +274,16 @@ class CycleViewSet(BaseViewSet): @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id): - if (request.data.get("start_date", None) is None and request.data.get("end_date", None) is None) or ( - request.data.get("start_date", None) is not None and request.data.get("end_date", None) is not None + if ( + request.data.get("start_date", None) is None + and request.data.get("end_date", None) is None + ) or ( + request.data.get("start_date", None) is not None + and request.data.get("end_date", None) is not None ): - serializer = CycleWriteSerializer(data=request.data, context={"project_id": project_id}) + serializer = CycleWriteSerializer( + data=request.data, context={"project_id": project_id} + ) if serializer.is_valid(): serializer.save(project_id=project_id, owned_by=request.user) cycle = ( @@ -307,7 +323,9 @@ class CycleViewSet(BaseViewSet): project_timezone = project.timezone datetime_fields = ["start_date", "end_date"] - cycle = user_timezone_converter(cycle, datetime_fields, project_timezone) + cycle = user_timezone_converter( + cycle, datetime_fields, project_timezone + ) # Send the model activity model_activity.delay( @@ -323,13 +341,17 @@ class CycleViewSet(BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) else: return Response( - {"error": "Both start date and end date are either required or are to be null"}, + { + "error": "Both start date and end date are either required or are to be null" + }, status=status.HTTP_400_BAD_REQUEST, ) @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def partial_update(self, request, slug, project_id, pk): - queryset = self.get_queryset().filter(workspace__slug=slug, project_id=project_id, pk=pk) + queryset = self.get_queryset().filter( + workspace__slug=slug, project_id=project_id, pk=pk + ) cycle = queryset.first() if cycle.archived_at: return Response( @@ -337,21 +359,29 @@ class CycleViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) - current_instance = json.dumps(CycleSerializer(cycle).data, cls=DjangoJSONEncoder) + current_instance = json.dumps( + CycleSerializer(cycle).data, cls=DjangoJSONEncoder + ) request_data = request.data if cycle.end_date is not None and cycle.end_date < timezone.now(): if "sort_order" in request_data: # Can only change sort order for a completed cycle`` - request_data = {"sort_order": request_data.get("sort_order", cycle.sort_order)} + request_data = { + "sort_order": request_data.get("sort_order", cycle.sort_order) + } else: return Response( - {"error": "The Cycle has already been completed so it cannot be edited"}, + { + "error": "The Cycle has already been completed so it cannot be edited" + }, status=status.HTTP_400_BAD_REQUEST, ) - serializer = CycleWriteSerializer(cycle, data=request.data, partial=True, context={"project_id": project_id}) + serializer = CycleWriteSerializer( + cycle, data=request.data, partial=True, context={"project_id": project_id} + ) if serializer.is_valid(): serializer.save() cycle = queryset.values( @@ -451,7 +481,9 @@ class CycleViewSet(BaseViewSet): ) if data is None: - return Response({"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND) + return Response( + {"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND + ) queryset = queryset.first() # Fetch the project timezone @@ -473,7 +505,11 @@ class CycleViewSet(BaseViewSet): def destroy(self, request, slug, project_id, pk): cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) - cycle_issues = list(CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list("issue", flat=True)) + cycle_issues = list( + CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list( + "issue", flat=True + ) + ) issue_activity.delay( type="cycle.activity.deleted", @@ -524,7 +560,9 @@ class CycleDateCheckEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) - start_date = convert_to_utc(date=str(start_date), project_id=project_id, is_start_date=True) + start_date = convert_to_utc( + date=str(start_date), project_id=project_id, is_start_date=True + ) end_date = convert_to_utc( date=str(end_date), project_id=project_id, @@ -597,409 +635,23 @@ class TransferCycleIssueEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) - new_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=new_cycle_id).first() - - old_cycle = ( - Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id) - .annotate( - total_issues=Count( - "issue_cycle", - filter=Q( - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - issue_cycle__deleted_at__isnull=True, - issue_cycle__issue__deleted_at__isnull=True, - ), - ) - ) - .annotate( - completed_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="completed", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - issue_cycle__issue__deleted_at__isnull=True, - issue_cycle__deleted_at__isnull=True, - ), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="cancelled", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - issue_cycle__issue__deleted_at__isnull=True, - issue_cycle__deleted_at__isnull=True, - ), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="started", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - issue_cycle__issue__deleted_at__isnull=True, - issue_cycle__deleted_at__isnull=True, - ), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="unstarted", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - issue_cycle__issue__deleted_at__isnull=True, - issue_cycle__deleted_at__isnull=True, - ), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="backlog", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - issue_cycle__issue__deleted_at__isnull=True, - issue_cycle__deleted_at__isnull=True, - ), - ) - ) - ) - old_cycle = old_cycle.first() - - estimate_type = Project.objects.filter( - workspace__slug=slug, - pk=project_id, - estimate__isnull=False, - estimate__type="points", - ).exists() - - if estimate_type: - assignee_estimate_data = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(display_name=F("assignees__display_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate( - avatar_url=Case( - # If `avatar_asset` exists, use it to generate the asset URL - When( - assignees__avatar_asset__isnull=False, - then=Concat( - Value("/api/assets/v2/static/"), - "assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field - Value("/"), - ), - ), - # If `avatar_asset` is None, fall back to using `avatar` field directly - When( - assignees__avatar_asset__isnull=True, - then="assignees__avatar", - ), - default=Value(None), - output_field=models.CharField(), - ) - ) - .values("display_name", "assignee_id", "avatar_url") - .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField()))) - .annotate( - completed_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("display_name") - ) - # assignee distribution serialization - assignee_estimate_distribution = [ - { - "display_name": item["display_name"], - "assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None), - "avatar": item.get("avatar"), - "avatar_url": item.get("avatar_url"), - "total_estimates": item["total_estimates"], - "completed_estimates": item["completed_estimates"], - "pending_estimates": item["pending_estimates"], - } - for item in assignee_estimate_data - ] - - label_distribution_data = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(label_name=F("labels__name")) - .annotate(color=F("labels__color")) - .annotate(label_id=F("labels__id")) - .values("label_name", "color", "label_id") - .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField()))) - .annotate( - completed_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("label_name") - ) - - estimate_completion_chart = burndown_plot( - queryset=old_cycle, - slug=slug, - project_id=project_id, - plot_type="points", - cycle_id=cycle_id, - ) - # Label distribution serialization - label_estimate_distribution = [ - { - "label_name": item["label_name"], - "color": item["color"], - "label_id": (str(item["label_id"]) if item["label_id"] else None), - "total_estimates": item["total_estimates"], - "completed_estimates": item["completed_estimates"], - "pending_estimates": item["pending_estimates"], - } - for item in label_distribution_data - ] - - # Get the assignee distribution - assignee_distribution = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(display_name=F("assignees__display_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate( - avatar_url=Case( - # If `avatar_asset` exists, use it to generate the asset URL - When( - assignees__avatar_asset__isnull=False, - then=Concat( - Value("/api/assets/v2/static/"), - "assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field - Value("/"), - ), - ), - # If `avatar_asset` is None, fall back to using `avatar` field directly - When(assignees__avatar_asset__isnull=True, then="assignees__avatar"), - default=Value(None), - output_field=models.CharField(), - ) - ) - .values("display_name", "assignee_id", "avatar_url") - .annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False))) - .annotate( - completed_issues=Count( - "id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("display_name") - ) - # assignee distribution serialized - assignee_distribution_data = [ - { - "display_name": item["display_name"], - "assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None), - "avatar": item.get("avatar"), - "avatar_url": item.get("avatar_url"), - "total_issues": item["total_issues"], - "completed_issues": item["completed_issues"], - "pending_issues": item["pending_issues"], - } - for item in assignee_distribution - ] - - # Get the label distribution - label_distribution = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(label_name=F("labels__name")) - .annotate(color=F("labels__color")) - .annotate(label_id=F("labels__id")) - .values("label_name", "color", "label_id") - .annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False))) - .annotate( - completed_issues=Count( - "id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("label_name") - ) - - # Label distribution serilization - label_distribution_data = [ - { - "label_name": item["label_name"], - "color": item["color"], - "label_id": (str(item["label_id"]) if item["label_id"] else None), - "total_issues": item["total_issues"], - "completed_issues": item["completed_issues"], - "pending_issues": item["pending_issues"], - } - for item in label_distribution - ] - - # Pass the new_cycle queryset to burndown_plot - completion_chart = burndown_plot( - queryset=old_cycle, + # Transfer cycle issues and create progress snapshot + result = transfer_cycle_issues( slug=slug, project_id=project_id, - plot_type="issues", cycle_id=cycle_id, + new_cycle_id=new_cycle_id, + request=request, + user_id=request.user.id, ) - current_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id).first() - - current_cycle.progress_snapshot = { - "total_issues": old_cycle.total_issues, - "completed_issues": old_cycle.completed_issues, - "cancelled_issues": old_cycle.cancelled_issues, - "started_issues": old_cycle.started_issues, - "unstarted_issues": old_cycle.unstarted_issues, - "backlog_issues": old_cycle.backlog_issues, - "distribution": { - "labels": label_distribution_data, - "assignees": assignee_distribution_data, - "completion_chart": completion_chart, - }, - "estimate_distribution": ( - {} - if not estimate_type - else { - "labels": label_estimate_distribution, - "assignees": assignee_estimate_distribution, - "completion_chart": estimate_completion_chart, - } - ), - } - current_cycle.save(update_fields=["progress_snapshot"]) - - if new_cycle.end_date is not None and new_cycle.end_date < timezone.now(): + # Handle error response + if result.get("error"): return Response( - {"error": "The cycle where the issues are transferred is already completed"}, + {"error": result["error"]}, status=status.HTTP_400_BAD_REQUEST, ) - cycle_issues = CycleIssue.objects.filter( - cycle_id=cycle_id, - project_id=project_id, - workspace__slug=slug, - issue__state__group__in=["backlog", "unstarted", "started"], - ) - - updated_cycles = [] - update_cycle_issue_activity = [] - for cycle_issue in cycle_issues: - cycle_issue.cycle_id = new_cycle_id - updated_cycles.append(cycle_issue) - update_cycle_issue_activity.append( - { - "old_cycle_id": str(cycle_id), - "new_cycle_id": str(new_cycle_id), - "issue_id": str(cycle_issue.issue_id), - } - ) - - cycle_issues = CycleIssue.objects.bulk_update(updated_cycles, ["cycle_id"], batch_size=100) - - # Capture Issue Activity - issue_activity.delay( - type="cycle.activity.created", - requested_data=json.dumps({"cycles_list": []}), - actor_id=str(self.request.user.id), - issue_id=None, - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "updated_cycle_issues": update_cycle_issue_activity, - "created_cycle_issues": "[]", - } - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=base_host(request=request, is_app=True), - ) - return Response({"message": "Success"}, status=status.HTTP_200_OK) @@ -1014,8 +666,12 @@ class CycleUserPropertiesEndpoint(BaseAPIView): ) cycle_properties.filters = request.data.get("filters", cycle_properties.filters) - cycle_properties.rich_filters = request.data.get("rich_filters", cycle_properties.rich_filters) - cycle_properties.display_filters = request.data.get("display_filters", cycle_properties.display_filters) + cycle_properties.rich_filters = request.data.get( + "rich_filters", cycle_properties.rich_filters + ) + cycle_properties.display_filters = request.data.get( + "display_filters", cycle_properties.display_filters + ) cycle_properties.display_properties = request.data.get( "display_properties", cycle_properties.display_properties ) @@ -1039,9 +695,13 @@ class CycleUserPropertiesEndpoint(BaseAPIView): class CycleProgressEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id, cycle_id): - cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, id=cycle_id).first() + cycle = Cycle.objects.filter( + workspace__slug=slug, project_id=project_id, id=cycle_id + ).first() if not cycle: - return Response({"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND) + return Response( + {"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND + ) aggregate_estimates = ( Issue.issue_objects.filter( estimate_point__estimate__type="points", @@ -1087,7 +747,9 @@ class CycleProgressEndpoint(BaseAPIView): output_field=FloatField(), ) ), - total_estimate_points=Sum("value_as_float", default=Value(0), output_field=FloatField()), + total_estimate_points=Sum( + "value_as_float", default=Value(0), output_field=FloatField() + ), ) ) if cycle.progress_snapshot: @@ -1147,11 +809,22 @@ class CycleProgressEndpoint(BaseAPIView): return Response( { - "backlog_estimate_points": aggregate_estimates["backlog_estimate_point"] or 0, - "unstarted_estimate_points": aggregate_estimates["unstarted_estimate_point"] or 0, - "started_estimate_points": aggregate_estimates["started_estimate_point"] or 0, - "cancelled_estimate_points": aggregate_estimates["cancelled_estimate_point"] or 0, - "completed_estimate_points": aggregate_estimates["completed_estimate_points"] or 0, + "backlog_estimate_points": aggregate_estimates["backlog_estimate_point"] + or 0, + "unstarted_estimate_points": aggregate_estimates[ + "unstarted_estimate_point" + ] + or 0, + "started_estimate_points": aggregate_estimates["started_estimate_point"] + or 0, + "cancelled_estimate_points": aggregate_estimates[ + "cancelled_estimate_point" + ] + or 0, + "completed_estimate_points": aggregate_estimates[ + "completed_estimate_points" + ] + or 0, "total_estimate_points": aggregate_estimates["total_estimate_points"], "backlog_issues": backlog_issues, "total_issues": total_issues, @@ -1169,7 +842,9 @@ class CycleAnalyticsEndpoint(BaseAPIView): def get(self, request, slug, project_id, cycle_id): analytic_type = request.GET.get("type", "issues") cycle = ( - Cycle.objects.filter(workspace__slug=slug, project_id=project_id, id=cycle_id) + Cycle.objects.filter( + workspace__slug=slug, project_id=project_id, id=cycle_id + ) .annotate( total_issues=Count( "issue_cycle__issue__id", @@ -1252,7 +927,9 @@ class CycleAnalyticsEndpoint(BaseAPIView): ) ) .values("display_name", "assignee_id", "avatar_url") - .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField()))) + .annotate( + total_estimates=Sum(Cast("estimate_point__value", FloatField())) + ) .annotate( completed_estimates=Sum( Cast("estimate_point__value", FloatField()), @@ -1287,7 +964,9 @@ class CycleAnalyticsEndpoint(BaseAPIView): .annotate(color=F("labels__color")) .annotate(label_id=F("labels__id")) .values("label_name", "color", "label_id") - .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField()))) + .annotate( + total_estimates=Sum(Cast("estimate_point__value", FloatField())) + ) .annotate( completed_estimates=Sum( Cast("estimate_point__value", FloatField()), @@ -1389,7 +1068,11 @@ class CycleAnalyticsEndpoint(BaseAPIView): .annotate(color=F("labels__color")) .annotate(label_id=F("labels__id")) .values("label_name", "color", "label_id") - .annotate(total_issues=Count("label_id", filter=Q(archived_at__isnull=True, is_draft=False))) + .annotate( + total_issues=Count( + "label_id", filter=Q(archived_at__isnull=True, is_draft=False) + ) + ) .annotate( completed_issues=Count( "label_id", diff --git a/apps/api/plane/utils/cycle_transfer_issues.py b/apps/api/plane/utils/cycle_transfer_issues.py new file mode 100644 index 000000000..ec934e889 --- /dev/null +++ b/apps/api/plane/utils/cycle_transfer_issues.py @@ -0,0 +1,486 @@ +# Python imports +import json + +# Django imports +from django.db.models import ( + Case, + Count, + F, + Q, + Sum, + FloatField, + Value, + When, +) +from django.db import models +from django.db.models.functions import Cast, Concat +from django.utils import timezone + +# Module imports +from plane.db.models import ( + Cycle, + CycleIssue, + Issue, + Project, +) +from plane.utils.analytics_plot import burndown_plot +from plane.bgtasks.issue_activities_task import issue_activity +from plane.utils.host import base_host + + +def transfer_cycle_issues( + slug, + project_id, + cycle_id, + new_cycle_id, + request, + user_id, +): + """ + Transfer incomplete issues from one cycle to another and create progress snapshot. + + Args: + slug: Workspace slug + project_id: Project ID + cycle_id: Source cycle ID + new_cycle_id: Destination cycle ID + request: HTTP request object + user_id: User ID performing the transfer + + Returns: + dict: Response data with success or error message + """ + # Get the new cycle + new_cycle = Cycle.objects.filter( + workspace__slug=slug, project_id=project_id, pk=new_cycle_id + ).first() + + # Check if new cycle is already completed + if new_cycle.end_date is not None and new_cycle.end_date < timezone.now(): + return { + "success": False, + "error": "The cycle where the issues are transferred is already completed", + } + + # Get the old cycle with issue counts + old_cycle = ( + Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id) + .annotate( + total_issues=Count( + "issue_cycle", + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, + ), + ) + ) + .annotate( + completed_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + issue_cycle__issue__deleted_at__isnull=True, + issue_cycle__deleted_at__isnull=True, + ), + ) + ) + .annotate( + cancelled_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="cancelled", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + issue_cycle__issue__deleted_at__isnull=True, + issue_cycle__deleted_at__isnull=True, + ), + ) + ) + .annotate( + started_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + issue_cycle__issue__deleted_at__isnull=True, + issue_cycle__deleted_at__isnull=True, + ), + ) + ) + .annotate( + unstarted_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="unstarted", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + issue_cycle__issue__deleted_at__isnull=True, + issue_cycle__deleted_at__isnull=True, + ), + ) + ) + .annotate( + backlog_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="backlog", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + issue_cycle__issue__deleted_at__isnull=True, + issue_cycle__deleted_at__isnull=True, + ), + ) + ) + ) + old_cycle = old_cycle.first() + + if old_cycle is None: + return { + "success": False, + "error": "Source cycle not found", + } + + # Check if project uses estimates + estimate_type = Project.objects.filter( + workspace__slug=slug, + pk=project_id, + estimate__isnull=False, + estimate__type="points", + ).exists() + + # Initialize estimate distribution variables + assignee_estimate_distribution = [] + label_estimate_distribution = [] + estimate_completion_chart = {} + + if estimate_type: + assignee_estimate_data = ( + Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(display_name=F("assignees__display_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate( + avatar_url=Case( + # If `avatar_asset` exists, use it to generate the asset URL + When( + assignees__avatar_asset__isnull=False, + then=Concat( + Value("/api/assets/v2/static/"), + "assignees__avatar_asset", + Value("/"), + ), + ), + # If `avatar_asset` is None, fall back to using `avatar` field directly + When( + assignees__avatar_asset__isnull=True, + then="assignees__avatar", + ), + default=Value(None), + output_field=models.CharField(), + ) + ) + .values("display_name", "assignee_id", "avatar_url") + .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField()))) + .annotate( + completed_estimates=Sum( + Cast("estimate_point__value", FloatField()), + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_estimates=Sum( + Cast("estimate_point__value", FloatField()), + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("display_name") + ) + # Assignee estimate distribution serialization + assignee_estimate_distribution = [ + { + "display_name": item["display_name"], + "assignee_id": ( + str(item["assignee_id"]) if item["assignee_id"] else None + ), + "avatar_url": item.get("avatar_url"), + "total_estimates": item["total_estimates"], + "completed_estimates": item["completed_estimates"], + "pending_estimates": item["pending_estimates"], + } + for item in assignee_estimate_data + ] + + label_distribution_data = ( + Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(label_name=F("labels__name")) + .annotate(color=F("labels__color")) + .annotate(label_id=F("labels__id")) + .values("label_name", "color", "label_id") + .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField()))) + .annotate( + completed_estimates=Sum( + Cast("estimate_point__value", FloatField()), + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_estimates=Sum( + Cast("estimate_point__value", FloatField()), + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("label_name") + ) + + estimate_completion_chart = burndown_plot( + queryset=old_cycle, + slug=slug, + project_id=project_id, + plot_type="points", + cycle_id=cycle_id, + ) + # Label estimate distribution serialization + label_estimate_distribution = [ + { + "label_name": item["label_name"], + "color": item["color"], + "label_id": (str(item["label_id"]) if item["label_id"] else None), + "total_estimates": item["total_estimates"], + "completed_estimates": item["completed_estimates"], + "pending_estimates": item["pending_estimates"], + } + for item in label_distribution_data + ] + + # Get the assignee distribution + assignee_distribution = ( + Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(display_name=F("assignees__display_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate( + avatar_url=Case( + # If `avatar_asset` exists, use it to generate the asset URL + When( + assignees__avatar_asset__isnull=False, + then=Concat( + Value("/api/assets/v2/static/"), + "assignees__avatar_asset", + Value("/"), + ), + ), + # If `avatar_asset` is None, fall back to using `avatar` field directly + When(assignees__avatar_asset__isnull=True, then="assignees__avatar"), + default=Value(None), + output_field=models.CharField(), + ) + ) + .values("display_name", "assignee_id", "avatar_url") + .annotate( + total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)) + ) + .annotate( + completed_issues=Count( + "id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("display_name") + ) + # Assignee distribution serialized + assignee_distribution_data = [ + { + "display_name": item["display_name"], + "assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None), + "avatar_url": item.get("avatar_url"), + "total_issues": item["total_issues"], + "completed_issues": item["completed_issues"], + "pending_issues": item["pending_issues"], + } + for item in assignee_distribution + ] + + # Get the label distribution + label_distribution = ( + Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(label_name=F("labels__name")) + .annotate(color=F("labels__color")) + .annotate(label_id=F("labels__id")) + .values("label_name", "color", "label_id") + .annotate( + total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)) + ) + .annotate( + completed_issues=Count( + "id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("label_name") + ) + + # Label distribution serialization + label_distribution_data = [ + { + "label_name": item["label_name"], + "color": item["color"], + "label_id": (str(item["label_id"]) if item["label_id"] else None), + "total_issues": item["total_issues"], + "completed_issues": item["completed_issues"], + "pending_issues": item["pending_issues"], + } + for item in label_distribution + ] + + # Generate completion chart + completion_chart = burndown_plot( + queryset=old_cycle, + slug=slug, + project_id=project_id, + plot_type="issues", + cycle_id=cycle_id, + ) + + # Get the current cycle and save progress snapshot + current_cycle = Cycle.objects.filter( + workspace__slug=slug, project_id=project_id, pk=cycle_id + ).first() + + current_cycle.progress_snapshot = { + "total_issues": old_cycle.total_issues, + "completed_issues": old_cycle.completed_issues, + "cancelled_issues": old_cycle.cancelled_issues, + "started_issues": old_cycle.started_issues, + "unstarted_issues": old_cycle.unstarted_issues, + "backlog_issues": old_cycle.backlog_issues, + "distribution": { + "labels": label_distribution_data, + "assignees": assignee_distribution_data, + "completion_chart": completion_chart, + }, + "estimate_distribution": ( + {} + if not estimate_type + else { + "labels": label_estimate_distribution, + "assignees": assignee_estimate_distribution, + "completion_chart": estimate_completion_chart, + } + ), + } + current_cycle.save(update_fields=["progress_snapshot"]) + + # Get issues to transfer (only incomplete issues) + cycle_issues = CycleIssue.objects.filter( + cycle_id=cycle_id, + project_id=project_id, + workspace__slug=slug, + issue__archived_at__isnull=True, + issue__is_draft=False, + issue__state__group__in=["backlog", "unstarted", "started"], + ) + + updated_cycles = [] + update_cycle_issue_activity = [] + for cycle_issue in cycle_issues: + cycle_issue.cycle_id = new_cycle_id + updated_cycles.append(cycle_issue) + update_cycle_issue_activity.append( + { + "old_cycle_id": str(cycle_id), + "new_cycle_id": str(new_cycle_id), + "issue_id": str(cycle_issue.issue_id), + } + ) + + # Bulk update cycle issues + cycle_issues = CycleIssue.objects.bulk_update( + updated_cycles, ["cycle_id"], batch_size=100 + ) + + # Capture Issue Activity + issue_activity.delay( + type="cycle.activity.created", + requested_data=json.dumps({"cycles_list": []}), + actor_id=str(user_id), + issue_id=None, + project_id=str(project_id), + current_instance=json.dumps( + { + "updated_cycle_issues": update_cycle_issue_activity, + "created_cycle_issues": [], + } + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=base_host(request=request, is_app=True), + ) + + return {"success": True} From 38cdf756a5b456cd2817768adb330884a0c9ca76 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:59:29 +0530 Subject: [PATCH 005/212] [WEB-5158] chore: calendar start of week alignment fix and code refactoring #7978 --- .../store/issue/issue_calendar_view.store.ts | 43 +++++++++++++++++-- apps/web/core/store/issue/root.store.ts | 2 +- packages/utils/src/calendar.ts | 20 +++++++-- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/apps/web/core/store/issue/issue_calendar_view.store.ts b/apps/web/core/store/issue/issue_calendar_view.store.ts index f9ee2c29d..9e00cbd7e 100644 --- a/apps/web/core/store/issue/issue_calendar_view.store.ts +++ b/apps/web/core/store/issue/issue_calendar_view.store.ts @@ -1,10 +1,13 @@ -import { observable, action, makeObservable, runInAction, computed } from "mobx"; +import { observable, action, makeObservable, runInAction, computed, reaction } from "mobx"; // helpers import { computedFn } from "mobx-utils"; import type { ICalendarPayload, ICalendarWeek } from "@plane/types"; +import { EStartOfTheWeek } from "@plane/types"; import { generateCalendarData, getWeekNumberOfDate } from "@plane/utils"; // types +import type { IIssueRootStore } from "./root.store"; + export interface ICalendarStore { calendarFilters: { activeMonthDate: Date; @@ -15,6 +18,7 @@ export interface ICalendarStore { // action updateCalendarFilters: (filters: Partial<{ activeMonthDate: Date; activeWeekDate: Date }>) => void; updateCalendarPayload: (date: Date) => void; + regenerateCalendar: () => void; // computed allWeeksOfActiveMonth: @@ -38,8 +42,10 @@ export class CalendarStore implements ICalendarStore { activeWeekDate: new Date(), }; calendarPayload: ICalendarPayload | null = null; + // root store + rootStore; - constructor() { + constructor(_rootStore: IIssueRootStore) { makeObservable(this, { loader: observable.ref, error: observable.ref, @@ -51,6 +57,7 @@ export class CalendarStore implements ICalendarStore { // actions updateCalendarFilters: action, updateCalendarPayload: action, + regenerateCalendar: action, //computed allWeeksOfActiveMonth: computed, @@ -58,7 +65,17 @@ export class CalendarStore implements ICalendarStore { allDaysOfActiveWeek: computed, }); + this.rootStore = _rootStore; this.initCalendar(); + + // Watch for changes in startOfWeek preference and regenerate calendar + reaction( + () => this.rootStore.rootStore.user.userProfile.data?.start_of_the_week, + () => { + // Regenerate calendar when startOfWeek preference changes + this.regenerateCalendar(); + } + ); } get allWeeksOfActiveMonth() { @@ -138,14 +155,32 @@ export class CalendarStore implements ICalendarStore { if (!this.calendarPayload) return null; const nextDate = new Date(date); + const startOfWeek = this.rootStore.rootStore.user.userProfile.data?.start_of_the_week ?? EStartOfTheWeek.SUNDAY; runInAction(() => { - this.calendarPayload = generateCalendarData(this.calendarPayload, nextDate); + this.calendarPayload = generateCalendarData(this.calendarPayload, nextDate, startOfWeek); }); }; initCalendar = () => { - const newCalendarPayload = generateCalendarData(null, new Date()); + const startOfWeek = this.rootStore.rootStore.user.userProfile.data?.start_of_the_week ?? EStartOfTheWeek.SUNDAY; + const newCalendarPayload = generateCalendarData(null, new Date(), startOfWeek); + + runInAction(() => { + this.calendarPayload = newCalendarPayload; + }); + }; + + /** + * Force complete regeneration of calendar data + * This should be called when startOfWeek preference changes + */ + regenerateCalendar = () => { + const startOfWeek = this.rootStore.rootStore.user.userProfile.data?.start_of_the_week ?? EStartOfTheWeek.SUNDAY; + const { activeMonthDate } = this.calendarFilters; + + // Force complete regeneration by passing null to clear all cached data + const newCalendarPayload = generateCalendarData(null, activeMonthDate, startOfWeek); runInAction(() => { this.calendarPayload = newCalendarPayload; diff --git a/apps/web/core/store/issue/root.store.ts b/apps/web/core/store/issue/root.store.ts index 02dcfe184..ad671a566 100644 --- a/apps/web/core/store/issue/root.store.ts +++ b/apps/web/core/store/issue/root.store.ts @@ -266,7 +266,7 @@ export class IssueRootStore implements IIssueRootStore { this.archivedIssues = new ArchivedIssues(this, this.archivedIssuesFilter); this.issueKanBanView = new IssueKanBanViewStore(this); - this.issueCalendarView = new CalendarStore(); + this.issueCalendarView = new CalendarStore(this); this.projectEpicsFilter = new ProjectEpicsFilter(this); this.projectEpics = new ProjectEpics(this, this.projectEpicsFilter); diff --git a/packages/utils/src/calendar.ts b/packages/utils/src/calendar.ts index a8038ec5e..6d1f1de57 100644 --- a/packages/utils/src/calendar.ts +++ b/packages/utils/src/calendar.ts @@ -7,9 +7,14 @@ import { getWeekNumberOfDate, renderFormattedPayloadDate } from "./datetime"; * @returns {ICalendarPayload} calendar payload to render the calendar * @param {ICalendarPayload | null} currentStructure current calendar payload * @param {Date} startDate date of the month to render + * @param {EStartOfTheWeek} startOfWeek the day to start the week on * @description Returns calendar payload to render the calendar, if currentStructure is null, it will generate the payload for the month of startDate, else it will construct the payload for the month of startDate and append it to the currentStructure */ -export const generateCalendarData = (currentStructure: ICalendarPayload | null, startDate: Date): ICalendarPayload => { +export const generateCalendarData = ( + currentStructure: ICalendarPayload | null, + startDate: Date, + startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY +): ICalendarPayload => { const calendarData: ICalendarPayload = currentStructure ?? {}; const startMonth = startDate.getMonth(); @@ -19,10 +24,15 @@ export const generateCalendarData = (currentStructure: ICalendarPayload | null, const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const totalDaysInMonth = new Date(year, month + 1, 0).getDate(); - const firstDayOfMonth = new Date(year, month, 1).getDay(); // Sunday is 0, Monday is 1, ..., Saturday is 6 + const firstDayOfMonthRaw = new Date(year, month, 1).getDay(); // Sunday is 0, Monday is 1, ..., Saturday is 6 + + // Adjust firstDayOfMonth based on startOfWeek preference + // This calculates how many empty cells we need at the start of the calendar + const firstDayOfMonth = (firstDayOfMonthRaw - startOfWeek + 7) % 7; calendarData[`y-${year}`] ||= {}; - calendarData[`y-${year}`][`m-${month}`] ||= {}; + // Always reset the month data to ensure clean regeneration with correct startOfWeek + calendarData[`y-${year}`][`m-${month}`] = {}; const numWeeks = Math.ceil((totalDaysInMonth + firstDayOfMonth) / 7); @@ -50,7 +60,9 @@ export const generateCalendarData = (currentStructure: ICalendarPayload | null, }; } - calendarData[`y-${year}`][`m-${month}`][`w-${weekNumber}`] = currentWeekObject; + // Use sequential week index instead of calculated week number for the key + // This ensures weeks are grouped correctly regardless of startOfWeek preference + calendarData[`y-${year}`][`m-${month}`][`w-${week}`] = currentWeekObject; } return calendarData; From e710f5b278ed114b7e1f252333c129e885692e4b Mon Sep 17 00:00:00 2001 From: Aaron Date: Thu, 23 Oct 2025 04:03:32 -0700 Subject: [PATCH 006/212] [WEB-5196] chore: switch from isomorphic-dompurify to dompurify (#7983) * [WEB-5196] chore: switch from isomorphic-dompurify to dompurify Replace isomorphic-dompurify with dompurify package in utils. This change simplifies the dependency and uses the canonical DOMPurify package directly. * fix: removing dompurify from the space app as dependency * chore: remove unused import --------- Co-authored-by: sriramveeraghanta --- apps/space/helpers/string.helper.ts | 9 - apps/space/package.json | 1 - apps/web/package.json | 1 - packages/utils/package.json | 2 +- packages/utils/src/string.ts | 2 +- pnpm-lock.yaml | 706 ++++------------------------ 6 files changed, 100 insertions(+), 621 deletions(-) diff --git a/apps/space/helpers/string.helper.ts b/apps/space/helpers/string.helper.ts index 2cca3177a..3eec9b9a1 100644 --- a/apps/space/helpers/string.helper.ts +++ b/apps/space/helpers/string.helper.ts @@ -1,5 +1,3 @@ -import DOMPurify from "dompurify"; - export const addSpaceIfCamelCase = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2"); const fallbackCopyTextToClipboard = (text: string) => { @@ -50,13 +48,6 @@ export const checkEmailValidity = (email: string): boolean => { return isEmailValid; }; -export const isEmptyHtmlString = (htmlString: string, allowedHTMLTags: string[] = []) => { - // Remove HTML tags using regex - const cleanText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: allowedHTMLTags }); - // Trim the string and check if it's empty - return cleanText.trim() === ""; -}; - export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); diff --git a/apps/space/package.json b/apps/space/package.json index ea9e022c4..f582fb07f 100644 --- a/apps/space/package.json +++ b/apps/space/package.json @@ -31,7 +31,6 @@ "axios": "catalog:", "clsx": "^2.0.0", "date-fns": "^4.1.0", - "dompurify": "^3.0.11", "dotenv": "^16.3.1", "lodash-es": "catalog:", "lowlight": "^2.9.0", diff --git a/apps/web/package.json b/apps/web/package.json index 35d6c4d18..81e60e2b2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -42,7 +42,6 @@ "dotenv": "^16.0.3", "emoji-picker-react": "^4.5.16", "export-to-csv": "^1.4.0", - "isomorphic-dompurify": "^2.12.0", "lodash-es": "catalog:", "lucide-react": "catalog:", "mobx": "catalog:", diff --git a/packages/utils/package.json b/packages/utils/package.json index bb58a85f1..65e3165b8 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -27,7 +27,7 @@ "@plane/types": "workspace:*", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "isomorphic-dompurify": "^2.16.0", + "dompurify": "3.2.7", "lodash-es": "catalog:", "lucide-react": "catalog:", "react": "catalog:", diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index 7dfd28daa..cf5eb3df6 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -1,4 +1,4 @@ -import DOMPurify from "isomorphic-dompurify"; +import DOMPurify from "dompurify"; import type { Content, JSONContent } from "@plane/types"; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b7aedc27..bca8100f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -370,9 +370,6 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 - dompurify: - specifier: ^3.0.11 - version: 3.2.6 dotenv: specifier: ^16.3.1 version: 16.6.1 @@ -542,9 +539,6 @@ importers: export-to-csv: specifier: ^1.4.0 version: 1.4.0 - isomorphic-dompurify: - specifier: ^2.12.0 - version: 2.25.0 lodash-es: specifier: 'catalog:' version: 4.17.21 @@ -1368,9 +1362,9 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 - isomorphic-dompurify: - specifier: ^2.16.0 - version: 2.25.0 + dompurify: + specifier: 3.2.7 + version: 3.2.7 lodash-es: specifier: 'catalog:' version: 4.17.21 @@ -1418,9 +1412,6 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@asamuzakjp/css-color@3.2.0': - resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} - '@atlaskit/pragmatic-drag-and-drop-auto-scroll@1.4.0': resolution: {integrity: sha512-5GoikoTSW13UX76F9TDeWB8x3jbbGlp/Y+3aRkHe1MOBMkrWkwNpJ42MIVhhX/6NSeaZiPumP0KbGJVs2tOWSQ==} @@ -1576,34 +1567,6 @@ packages: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} - '@csstools/color-helpers@5.0.2': - resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} - engines: {node: '>=18'} - - '@csstools/css-calc@2.1.4': - resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} - engines: {node: '>=18'} - peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 - - '@csstools/css-color-parser@3.0.10': - resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==} - engines: {node: '>=18'} - peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 - - '@csstools/css-parser-algorithms@3.0.5': - resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} - engines: {node: '>=18'} - peerDependencies: - '@csstools/css-tokenizer': ^3.0.4 - - '@csstools/css-tokenizer@3.0.4': - resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} - engines: {node: '>=18'} - '@dabh/diagnostics@2.0.3': resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} @@ -2498,8 +2461,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 - '@oxc-project/types@0.89.0': - resolution: {integrity: sha512-yuo+ECPIW5Q9mSeNmCDC2im33bfKuwW18mwkaHMQh8KakHYDzj4ci/q7wxf2qS3dMlVVCIyrs3kFtH5LmnlYnw==} + '@oxc-project/runtime@0.82.3': + resolution: {integrity: sha512-LNh5GlJvYHAnMurO+EyA8jJwN1rki7l3PSHuosDh2I7h00T6/u9rCkUjg/SvPmT1CZzvhuW0y+gf7jcqUy/Usg==} + engines: {node: '>=6.9.0'} + + '@oxc-project/types@0.82.3': + resolution: {integrity: sha512-6nCUxBnGX0c6qfZW5MaF6/fmu5dHJDMiMPaioKHKs5mi5+8/FHQ7WGjgQIz1zxpmceMYfdIXkOaLYE+ejbuOtA==} '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -2771,91 +2738,78 @@ packages: '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} - '@rolldown/binding-android-arm64@1.0.0-beta.38': - resolution: {integrity: sha512-AE3HFQrjWCKLFZD1Vpiy+qsqTRwwoil1oM5WsKPSmfQ5fif/A+ZtOZetF32erZdsR7qyvns6qHEteEsF6g6rsQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rolldown/binding-android-arm64@1.0.0-beta.34': + resolution: {integrity: sha512-jf5GNe5jP3Sr1Tih0WKvg2bzvh5T/1TA0fn1u32xSH7ca/p5t+/QRr4VRFCV/na5vjwKEhwWrChsL2AWlY+eoA==} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-beta.38': - resolution: {integrity: sha512-RaoWOKc0rrFsVmKOjQpebMY6c6/I7GR1FBc25v7L/R7NlM0166mUotwGEv7vxu7ruXH4SJcFeVrfADFUUXUmmQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rolldown/binding-darwin-arm64@1.0.0-beta.34': + resolution: {integrity: sha512-2F/TqH4QuJQ34tgWxqBjFL3XV1gMzeQgUO8YRtCPGBSP0GhxtoFzsp7KqmQEothsxztlv+KhhT9Dbg3HHwHViQ==} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.38': - resolution: {integrity: sha512-Ymojqc2U35iUc8NFU2XX1WQPfBRRHN6xHcrxAf9WS8BFFBn8pDrH5QPvH1tYs3lDkw6UGGbanr1RGzARqdUp1g==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rolldown/binding-darwin-x64@1.0.0-beta.34': + resolution: {integrity: sha512-E1QuFslgLWbHQ8Qli/AqUKdfg0pockQPwRxVbhNQ74SciZEZpzLaujkdmOLSccMlSXDfFCF8RPnMoRAzQ9JV8Q==} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-beta.38': - resolution: {integrity: sha512-0ermTQ//WzSI0nOL3z/LUWMNiE9xeM5cLGxjewPFEexqxV/0uM8/lNp9QageQ8jfc/VO1OURsGw34HYO5PaL8w==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rolldown/binding-freebsd-x64@1.0.0-beta.34': + resolution: {integrity: sha512-VS8VInNCwnkpI9WeQaWu3kVBq9ty6g7KrHdLxYMzeqz24+w9hg712TcWdqzdY6sn+24lUoMD9jTZrZ/qfVpk0g==} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.38': - resolution: {integrity: sha512-GADxzVUTCTp6EWI52831A29Tt7PukFe94nhg/SUsfkI33oTiNQtPxyLIT/3oRegizGuPSZSlrdBurkjDwxyEUQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.34': + resolution: {integrity: sha512-4St4emjcnULnxJYb/5ZDrH/kK/j6PcUgc3eAqH5STmTrcF+I9m/X2xvSF2a2bWv1DOQhxBewThu0KkwGHdgu5w==} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.38': - resolution: {integrity: sha512-SKO7Exl5Yem/OSNoA5uLHzyrptUQ8Hg70kHDxuwEaH0+GUg+SQe9/7PWmc4hFKBMrJGdQtii8WZ0uIz9Dofg5Q==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.34': + resolution: {integrity: sha512-a737FTqhFUoWfnebS2SnQ2BS50p0JdukdkUBwy2J06j4hZ6Eej0zEB8vTfAqoCjn8BQKkXBy+3Sx0IRkgwz1gA==} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.38': - resolution: {integrity: sha512-SOo6+WqhXPBaShLxLT0eCgH17d3Yu1lMAe4mFP0M9Bvr/kfMSOPQXuLxBcbBU9IFM9w3N6qP9xWOHO+oUJvi8Q==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.34': + resolution: {integrity: sha512-NH+FeQWKyuw0k+PbXqpFWNfvD8RPvfJk766B/njdaWz4TmiEcSB0Nb6guNw1rBpM1FmltQYb3fFnTumtC6pRfA==} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.38': - resolution: {integrity: sha512-yvsQ3CyrodOX+lcoi+lejZGCOvJZa9xTsNB8OzpMDmHeZq3QzJfpYjXSAS6vie70fOkLVJb77UqYO193Cl8XBQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.34': + resolution: {integrity: sha512-Q3RSCivp8pNadYK8ke3hLnQk08BkpZX9BmMjgwae2FWzdxhxxUiUzd9By7kneUL0vRQ4uRnhD9VkFQ+Haeqdvw==} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.38': - resolution: {integrity: sha512-84qzKMwUwikfYeOuJ4Kxm/3z15rt0nFGGQArHYIQQNSTiQdxGHxOkqXtzPFqrVfBJUdxBAf+jYzR1pttFJuWyg==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rolldown/binding-linux-x64-musl@1.0.0-beta.34': + resolution: {integrity: sha512-wDd/HrNcVoBhWWBUW3evJHoo7GJE/RofssBy3Dsiip05YUBmokQVrYAyrboOY4dzs/lJ7HYeBtWQ9hj8wlyF0A==} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-beta.38': - resolution: {integrity: sha512-QrNiWlce01DYH0rL8K3yUBu+lNzY+B0DyCbIc2Atan6/S6flxOL0ow5DLQvMamOI/oKhrJ4xG+9MkMb9dDHbLQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rolldown/binding-openharmony-arm64@1.0.0-beta.34': + resolution: {integrity: sha512-dH3FTEV6KTNWpYSgjSXZzeX7vLty9oBYn6R3laEdhwZftQwq030LKL+5wyQdlbX5pnbh4h127hpv3Hl1+sj8dg==} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.38': - resolution: {integrity: sha512-fnLtHyjwEsG4/aNV3Uv3Qd1ZbdH+CopwJNoV0RgBqrcQB8V6/Qdikd5JKvnO23kb3QvIpP+dAMGZMv1c2PJMzw==} + '@rolldown/binding-wasm32-wasi@1.0.0-beta.34': + resolution: {integrity: sha512-y5BUf+QtO0JsIDKA51FcGwvhJmv89BYjUl8AmN7jqD6k/eU55mH6RJYnxwCsODq5m7KSSTigVb6O7/GqB8wbPw==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.38': - resolution: {integrity: sha512-19cTfnGedem+RY+znA9J6ARBOCEFD4YSjnx0p5jiTm9tR6pHafRfFIfKlTXhun+NL0WWM/M0eb2IfPPYUa8+wg==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.34': + resolution: {integrity: sha512-ga5hFhdTwpaNxEiuxZHWnD3ed0GBAzbgzS5tRHpe0ObptxM1a9Xrq6TVfNQirBLwb5Y7T/FJmJi3pmdLy95ljg==} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.38': - resolution: {integrity: sha512-HcICm4YzFJZV+fI0O0bFLVVlsWvRNo/AB9EfUXvNYbtAxakCnQZ15oq22deFdz6sfi9Y4/SagH2kPU723dhCFA==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.34': + resolution: {integrity: sha512-4/MBp9T9eRnZskxWr8EXD/xHvLhdjWaeX/qY9LPRG1JdCGV3DphkLTy5AWwIQ5jhAy2ZNJR5z2fYRlpWU0sIyQ==} cpu: [ia32] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.38': - resolution: {integrity: sha512-4Qx6cgEPXLb0XsCyLoQcUgYBpfL0sjugftob+zhUH0EOk/NVCAIT+h0NJhY+jn7pFpeKxhNMqhvTNx3AesxIAQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.34': + resolution: {integrity: sha512-7O5iUBX6HSBKlQU4WykpUoEmb0wQmonb6ziKFr3dJTHud2kzDnWMqk344T0qm3uGv9Ddq6Re/94pInxo1G2d4w==} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-beta.38': - resolution: {integrity: sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==} + '@rolldown/pluginutils@1.0.0-beta.34': + resolution: {integrity: sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA==} '@rollup/pluginutils@5.2.0': resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} @@ -2866,101 +2820,51 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.50.0': - resolution: {integrity: sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ==} - cpu: [arm] - os: [android] - '@rollup/rollup-android-arm-eabi@4.52.4': resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.50.0': - resolution: {integrity: sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw==} - cpu: [arm64] - os: [android] - '@rollup/rollup-android-arm64@4.52.4': resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.50.0': - resolution: {integrity: sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==} - cpu: [arm64] - os: [darwin] - '@rollup/rollup-darwin-arm64@4.52.4': resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.50.0': - resolution: {integrity: sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==} - cpu: [x64] - os: [darwin] - '@rollup/rollup-darwin-x64@4.52.4': resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.50.0': - resolution: {integrity: sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ==} - cpu: [arm64] - os: [freebsd] - '@rollup/rollup-freebsd-arm64@4.52.4': resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.50.0': - resolution: {integrity: sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA==} - cpu: [x64] - os: [freebsd] - '@rollup/rollup-freebsd-x64@4.52.4': resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.50.0': - resolution: {integrity: sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==} - cpu: [arm] - os: [linux] - '@rollup/rollup-linux-arm-gnueabihf@4.52.4': resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.50.0': - resolution: {integrity: sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==} - cpu: [arm] - os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.52.4': resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.50.0': - resolution: {integrity: sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==} - cpu: [arm64] - os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.52.4': resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.50.0': - resolution: {integrity: sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==} - cpu: [arm64] - os: [linux] - '@rollup/rollup-linux-arm64-musl@4.52.4': resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} cpu: [arm64] @@ -2971,96 +2875,46 @@ packages: cpu: [loong64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.50.0': - resolution: {integrity: sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-ppc64-gnu@4.50.0': - resolution: {integrity: sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==} - cpu: [ppc64] - os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.52.4': resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.50.0': - resolution: {integrity: sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==} - cpu: [riscv64] - os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.52.4': resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.50.0': - resolution: {integrity: sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==} - cpu: [riscv64] - os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.52.4': resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.50.0': - resolution: {integrity: sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==} - cpu: [s390x] - os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.52.4': resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.50.0': - resolution: {integrity: sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==} - cpu: [x64] - os: [linux] - '@rollup/rollup-linux-x64-gnu@4.52.4': resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.50.0': - resolution: {integrity: sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==} - cpu: [x64] - os: [linux] - '@rollup/rollup-linux-x64-musl@4.52.4': resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.50.0': - resolution: {integrity: sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==} - cpu: [arm64] - os: [openharmony] - '@rollup/rollup-openharmony-arm64@4.52.4': resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.50.0': - resolution: {integrity: sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg==} - cpu: [arm64] - os: [win32] - '@rollup/rollup-win32-arm64-msvc@4.52.4': resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.50.0': - resolution: {integrity: sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw==} - cpu: [ia32] - os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.52.4': resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==} cpu: [ia32] @@ -3071,11 +2925,6 @@ packages: cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.50.0': - resolution: {integrity: sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==} - cpu: [x64] - os: [win32] - '@rollup/rollup-win32-x64-msvc@4.52.4': resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==} cpu: [x64] @@ -4257,10 +4106,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -4318,6 +4163,10 @@ packages: resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} engines: {node: '>=14'} + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -4798,10 +4647,6 @@ packages: engines: {node: '>=4'} hasBin: true - cssstyle@4.6.0: - resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} - engines: {node: '>=18'} - csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -4852,10 +4697,6 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - data-urls@5.0.0: - resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} - engines: {node: '>=18'} - data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -4890,15 +4731,6 @@ packages: supports-color: optional: true - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -5048,8 +4880,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.2.6: - resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==} + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} @@ -5158,10 +4990,6 @@ packages: resolution: {integrity: sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==} engines: {node: '>=0.12'} - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} - error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -5766,10 +5594,6 @@ packages: hsl-to-rgb-for-reals@1.1.1: resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==} - html-encoding-sniffer@4.0.0: - resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} - engines: {node: '>=18'} - html-entities@2.6.0: resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} @@ -5797,14 +5621,6 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -5816,10 +5632,6 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - icss-utils@5.1.0: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -5993,9 +5805,6 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -6053,10 +5862,6 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} - isomorphic-dompurify@2.25.0: - resolution: {integrity: sha512-bcpJzu9DOjN21qaCVpcoCwUX1ytpvA6EFqCK5RNtPg5+F0Jz9PX50jl6jbEicBNeO87eDDfC7XtPs4zjDClZJg==} - engines: {node: '>=18'} - isomorphic.js@0.2.5: resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} @@ -6097,15 +5902,6 @@ packages: resolution: {integrity: sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==} engines: {node: '>=12.0.0'} - jsdom@26.1.0: - resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} - engines: {node: '>=18'} - peerDependencies: - canvas: ^3.0.0 - peerDependenciesMeta: - canvas: - optional: true - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -6601,9 +6397,6 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - nwsapi@2.2.21: - resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -6742,9 +6535,6 @@ packages: parse-svg-path@0.1.2: resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==} - parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -7455,14 +7245,8 @@ packages: vue-tsc: optional: true - rolldown@1.0.0-beta.38: - resolution: {integrity: sha512-58frPNX55Je1YsyrtPJv9rOSR3G5efUZpRqok94Efsj0EUa8dnqJV3BldShyI7A+bVPleucOtzXHwVpJRcR0kQ==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - rollup@4.50.0: - resolution: {integrity: sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + rolldown@1.0.0-beta.34: + resolution: {integrity: sha512-Wwh7EwalMzzX3Yy3VN58VEajeR2Si8+HDNMf706jPLIqU7CxneRW+dQVfznf5O0TWTnJyu4npelwg2bzTXB1Nw==} hasBin: true rollup@4.52.4: @@ -7473,9 +7257,6 @@ packages: rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} - rrweb-cssom@0.8.0: - resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -7505,10 +7286,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - saxes@6.0.0: - resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} - engines: {node: '>=v12.22.7'} - scheduler@0.17.0: resolution: {integrity: sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA==} @@ -7798,9 +7575,6 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 - symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} @@ -7901,13 +7675,6 @@ packages: peerDependencies: '@tiptap/core': ^2.0.3 - tldts-core@6.1.86: - resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} - - tldts@6.1.86: - resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} - hasBin: true - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -7920,17 +7687,9 @@ packages: resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==} engines: {node: '>=14.16'} - tough-cookie@5.1.2: - resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} - engines: {node: '>=16'} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} - tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -8268,10 +8027,6 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} - w3c-xmlserializer@5.0.0: - resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} - engines: {node: '>=18'} - warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} @@ -8285,10 +8040,6 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} - webpack-dev-middleware@6.1.3: resolution: {integrity: sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw==} engines: {node: '>= 14.15.0'} @@ -8318,18 +8069,6 @@ packages: webpack-cli: optional: true - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - - whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} - whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -8406,13 +8145,6 @@ packages: utf-8-validate: optional: true - xml-name-validator@5.0.0: - resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} - engines: {node: '>=18'} - - xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -8491,14 +8223,6 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@asamuzakjp/css-color@3.2.0': - dependencies: - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - lru-cache: 10.4.3 - '@atlaskit/pragmatic-drag-and-drop-auto-scroll@1.4.0': dependencies: '@atlaskit/pragmatic-drag-and-drop': 1.7.4 @@ -8536,7 +8260,7 @@ snapshots: '@babel/types': 7.28.4 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -8610,7 +8334,7 @@ snapshots: '@babel/parser': 7.28.4 '@babel/template': 7.27.2 '@babel/types': 7.28.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -8718,26 +8442,6 @@ snapshots: '@colors/colors@1.6.0': {} - '@csstools/color-helpers@5.0.2': {} - - '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': - dependencies: - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - - '@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': - dependencies: - '@csstools/color-helpers': 5.0.2 - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - - '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': - dependencies: - '@csstools/css-tokenizer': 3.0.4 - - '@csstools/css-tokenizer@3.0.4': {} - '@dabh/diagnostics@2.0.3': dependencies: colorspace: 1.1.4 @@ -9662,7 +9366,9 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@oxc-project/types@0.89.0': {} + '@oxc-project/runtime@0.82.3': {} + + '@oxc-project/types@0.82.3': {} '@pkgjs/parseargs@0.11.0': optional: true @@ -9996,51 +9702,51 @@ snapshots: '@remirror/core-constants@3.0.0': {} - '@rolldown/binding-android-arm64@1.0.0-beta.38': + '@rolldown/binding-android-arm64@1.0.0-beta.34': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-beta.38': + '@rolldown/binding-darwin-arm64@1.0.0-beta.34': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.38': + '@rolldown/binding-darwin-x64@1.0.0-beta.34': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.38': + '@rolldown/binding-freebsd-x64@1.0.0-beta.34': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.38': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.34': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.38': + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.34': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.38': + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.34': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.38': + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.34': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.38': + '@rolldown/binding-linux-x64-musl@1.0.0-beta.34': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.38': + '@rolldown/binding-openharmony-arm64@1.0.0-beta.34': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.38': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.34': dependencies: '@napi-rs/wasm-runtime': 1.0.5 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.38': + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.34': optional: true - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.38': + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.34': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.38': + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.34': optional: true - '@rolldown/pluginutils@1.0.0-beta.38': {} + '@rolldown/pluginutils@1.0.0-beta.34': {} '@rollup/pluginutils@5.2.0(rollup@4.52.4)': dependencies: @@ -10050,132 +9756,69 @@ snapshots: optionalDependencies: rollup: 4.52.4 - '@rollup/rollup-android-arm-eabi@4.50.0': - optional: true - '@rollup/rollup-android-arm-eabi@4.52.4': optional: true - '@rollup/rollup-android-arm64@4.50.0': - optional: true - '@rollup/rollup-android-arm64@4.52.4': optional: true - '@rollup/rollup-darwin-arm64@4.50.0': - optional: true - '@rollup/rollup-darwin-arm64@4.52.4': optional: true - '@rollup/rollup-darwin-x64@4.50.0': - optional: true - '@rollup/rollup-darwin-x64@4.52.4': optional: true - '@rollup/rollup-freebsd-arm64@4.50.0': - optional: true - '@rollup/rollup-freebsd-arm64@4.52.4': optional: true - '@rollup/rollup-freebsd-x64@4.50.0': - optional: true - '@rollup/rollup-freebsd-x64@4.52.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.50.0': - optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.52.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.50.0': - optional: true - '@rollup/rollup-linux-arm-musleabihf@4.52.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.50.0': - optional: true - '@rollup/rollup-linux-arm64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.50.0': - optional: true - '@rollup/rollup-linux-arm64-musl@4.52.4': optional: true '@rollup/rollup-linux-loong64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.50.0': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.50.0': - optional: true - '@rollup/rollup-linux-ppc64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.50.0': - optional: true - '@rollup/rollup-linux-riscv64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.50.0': - optional: true - '@rollup/rollup-linux-riscv64-musl@4.52.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.50.0': - optional: true - '@rollup/rollup-linux-s390x-gnu@4.52.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.50.0': - optional: true - '@rollup/rollup-linux-x64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-x64-musl@4.50.0': - optional: true - '@rollup/rollup-linux-x64-musl@4.52.4': optional: true - '@rollup/rollup-openharmony-arm64@4.50.0': - optional: true - '@rollup/rollup-openharmony-arm64@4.52.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.50.0': - optional: true - '@rollup/rollup-win32-arm64-msvc@4.52.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.50.0': - optional: true - '@rollup/rollup-win32-ia32-msvc@4.52.4': optional: true '@rollup/rollup-win32-x64-gnu@4.52.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.50.0': - optional: true - '@rollup/rollup-win32-x64-msvc@4.52.4': optional: true @@ -10541,7 +10184,7 @@ snapshots: '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.101.3(@swc/core@1.13.5(@swc/helpers@0.5.17))(esbuild@0.25.0))': dependencies: - debug: 4.4.1 + debug: 4.4.3 endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -11325,7 +10968,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.8.3) '@typescript-eslint/types': 8.44.0 - debug: 4.4.1 + debug: 4.4.3 typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -11609,8 +11252,6 @@ snapshots: acorn@8.15.0: {} - agent-base@7.1.4: {} - ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -11658,6 +11299,8 @@ snapshots: ansis@4.1.0: {} + ansis@4.2.0: {} + any-promise@1.3.0: {} anymatch@3.1.3: @@ -12197,11 +11840,6 @@ snapshots: cssesc@3.0.0: {} - cssstyle@4.6.0: - dependencies: - '@asamuzakjp/css-color': 3.2.0 - rrweb-cssom: 0.8.0 - csstype@3.1.3: {} d3-array@3.2.4: @@ -12244,11 +11882,6 @@ snapshots: damerau-levenshtein@1.0.8: {} - data-urls@5.0.0: - dependencies: - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 - data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -12279,10 +11912,6 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.1: - dependencies: - ms: 2.1.3 - debug@4.4.3: dependencies: ms: 2.1.3 @@ -12407,7 +12036,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.2.6: + dompurify@3.2.7: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -12501,8 +12130,6 @@ snapshots: entities@5.0.0: {} - entities@6.0.1: {} - error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -13320,10 +12947,6 @@ snapshots: hsl-to-rgb-for-reals@1.1.1: {} - html-encoding-sniffer@4.0.0: - dependencies: - whatwg-encoding: 3.1.1 - html-entities@2.6.0: {} html-minifier-terser@6.1.0: @@ -13361,20 +12984,6 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 - http-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.4 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - human-signals@2.1.0: {} hyphen@1.10.6: {} @@ -13383,10 +12992,6 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -13445,7 +13050,7 @@ snapshots: dependencies: '@ioredis/commands': 1.3.0 cluster-key-slot: 1.1.2 - debug: 4.4.1 + debug: 4.4.3 denque: 1.5.1 lodash.defaults: 4.2.0 lodash.flatten: 4.4.0 @@ -13570,8 +13175,6 @@ snapshots: is-plain-obj@4.1.0: {} - is-potential-custom-element-name@1.0.1: {} - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -13625,16 +13228,6 @@ snapshots: isexe@3.1.1: {} - isomorphic-dompurify@2.25.0: - dependencies: - dompurify: 3.2.6 - jsdom: 26.1.0 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - utf-8-validate - isomorphic.js@0.2.5: {} iterator.prototype@1.1.5: @@ -13680,33 +13273,6 @@ snapshots: jsdoc-type-pratt-parser@4.8.0: {} - jsdom@26.1.0: - dependencies: - cssstyle: 4.6.0 - data-urls: 5.0.0 - decimal.js: 10.6.0 - html-encoding-sniffer: 4.0.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.21 - parse5: 7.3.0 - rrweb-cssom: 0.8.0 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 5.1.2 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 3.1.1 - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 - ws: 8.18.3 - xml-name-validator: 5.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -14077,7 +13643,7 @@ snapshots: micromark@3.2.0: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.3 decode-named-character-reference: 1.2.0 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -14249,8 +13815,6 @@ snapshots: dependencies: boolbase: 1.0.0 - nwsapi@2.2.21: {} - object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -14403,10 +13967,6 @@ snapshots: parse-svg-path@0.1.2: {} - parse5@7.3.0: - dependencies: - entities: 6.0.1 - parseurl@1.3.3: {} pascal-case@3.1.2: @@ -15124,7 +14684,7 @@ snapshots: dependencies: glob: 7.2.3 - rolldown-plugin-dts@0.16.11(rolldown@1.0.0-beta.38)(typescript@5.8.3): + rolldown-plugin-dts@0.16.11(rolldown@1.0.0-beta.34)(typescript@5.8.3): dependencies: '@babel/generator': 7.28.3 '@babel/parser': 7.28.4 @@ -15135,60 +14695,34 @@ snapshots: dts-resolver: 2.1.2 get-tsconfig: 4.10.1 magic-string: 0.30.19 - rolldown: 1.0.0-beta.38 + rolldown: 1.0.0-beta.34 optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: - oxc-resolver - supports-color - rolldown@1.0.0-beta.38: + rolldown@1.0.0-beta.34: dependencies: - '@oxc-project/types': 0.89.0 - '@rolldown/pluginutils': 1.0.0-beta.38 + '@oxc-project/runtime': 0.82.3 + '@oxc-project/types': 0.82.3 + '@rolldown/pluginutils': 1.0.0-beta.34 ansis: 4.1.0 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-beta.38 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.38 - '@rolldown/binding-darwin-x64': 1.0.0-beta.38 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.38 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.38 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.38 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.38 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.38 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.38 - '@rolldown/binding-openharmony-arm64': 1.0.0-beta.38 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.38 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.38 - '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.38 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.38 - - rollup@4.50.0: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.50.0 - '@rollup/rollup-android-arm64': 4.50.0 - '@rollup/rollup-darwin-arm64': 4.50.0 - '@rollup/rollup-darwin-x64': 4.50.0 - '@rollup/rollup-freebsd-arm64': 4.50.0 - '@rollup/rollup-freebsd-x64': 4.50.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.50.0 - '@rollup/rollup-linux-arm-musleabihf': 4.50.0 - '@rollup/rollup-linux-arm64-gnu': 4.50.0 - '@rollup/rollup-linux-arm64-musl': 4.50.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.50.0 - '@rollup/rollup-linux-ppc64-gnu': 4.50.0 - '@rollup/rollup-linux-riscv64-gnu': 4.50.0 - '@rollup/rollup-linux-riscv64-musl': 4.50.0 - '@rollup/rollup-linux-s390x-gnu': 4.50.0 - '@rollup/rollup-linux-x64-gnu': 4.50.0 - '@rollup/rollup-linux-x64-musl': 4.50.0 - '@rollup/rollup-openharmony-arm64': 4.50.0 - '@rollup/rollup-win32-arm64-msvc': 4.50.0 - '@rollup/rollup-win32-ia32-msvc': 4.50.0 - '@rollup/rollup-win32-x64-msvc': 4.50.0 - fsevents: 2.3.3 + '@rolldown/binding-android-arm64': 1.0.0-beta.34 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.34 + '@rolldown/binding-darwin-x64': 1.0.0-beta.34 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.34 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.34 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.34 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.34 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.34 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.34 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.34 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.34 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.34 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.34 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.34 rollup@4.52.4: dependencies: @@ -15217,12 +14751,9 @@ snapshots: '@rollup/rollup-win32-x64-gnu': 4.52.4 '@rollup/rollup-win32-x64-msvc': 4.52.4 fsevents: 2.3.3 - optional: true rope-sequence@1.3.4: {} - rrweb-cssom@0.8.0: {} - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -15256,10 +14787,6 @@ snapshots: safer-buffer@2.1.2: {} - saxes@6.0.0: - dependencies: - xmlchars: 2.2.0 - scheduler@0.17.0: dependencies: loose-envify: 1.4.0 @@ -15637,8 +15164,6 @@ snapshots: react: 18.3.1 use-sync-external-store: 1.5.0(react@18.3.1) - symbol-tree@3.2.4: {} - tabbable@6.2.0: {} tailwind-merge@2.6.0: {} @@ -15744,12 +15269,6 @@ snapshots: markdown-it-task-lists: 2.1.1 prosemirror-markdown: 1.13.2 - tldts-core@6.1.86: {} - - tldts@6.1.86: - dependencies: - tldts-core: 6.1.86 - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -15762,16 +15281,8 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - tough-cookie@5.1.2: - dependencies: - tldts: 6.1.86 - tr46@0.0.3: {} - tr46@5.1.1: - dependencies: - punycode: 2.3.1 - tree-kill@1.2.2: {} trim-lines@3.0.1: {} @@ -15803,15 +15314,15 @@ snapshots: tsdown@0.15.5(typescript@5.8.3): dependencies: - ansis: 4.1.0 + ansis: 4.2.0 cac: 6.7.14 chokidar: 3.6.0 debug: 4.4.3 diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.0-beta.38 - rolldown-plugin-dts: 0.16.11(rolldown@1.0.0-beta.38)(typescript@5.8.3) + rolldown: 1.0.0-beta.34 + rolldown-plugin-dts: 0.16.11(rolldown@1.0.0-beta.34)(typescript@5.8.3) semver: 7.7.2 tinyexec: 1.0.1 tinyglobby: 0.2.15 @@ -16124,7 +15635,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.50.0 + rollup: 4.52.4 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 22.12.0 @@ -16135,10 +15646,6 @@ snapshots: w3c-keyname@2.2.8: {} - w3c-xmlserializer@5.0.0: - dependencies: - xml-name-validator: 5.0.0 - warning@4.0.3: dependencies: loose-envify: 1.4.0 @@ -16152,8 +15659,6 @@ snapshots: webidl-conversions@3.0.1: {} - webidl-conversions@7.0.0: {} - webpack-dev-middleware@6.1.3(webpack@5.101.3(@swc/core@1.13.5(@swc/helpers@0.5.17))(esbuild@0.25.0)): dependencies: colorette: 2.0.20 @@ -16206,17 +15711,6 @@ snapshots: - esbuild - uglify-js - whatwg-encoding@3.1.1: - dependencies: - iconv-lite: 0.6.3 - - whatwg-mimetype@4.0.0: {} - - whatwg-url@14.2.0: - dependencies: - tr46: 5.1.1 - webidl-conversions: 7.0.0 - whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -16311,10 +15805,6 @@ snapshots: ws@8.18.3: {} - xml-name-validator@5.0.0: {} - - xmlchars@2.2.0: {} - xtend@4.0.2: {} y-indexeddb@9.0.12(yjs@13.6.27): From 96fa9ab15b72eba8959cfada907e66f2b5aca4c6 Mon Sep 17 00:00:00 2001 From: Akshat Jain Date: Thu, 23 Oct 2025 19:26:27 +0530 Subject: [PATCH 007/212] [INFRA-252] fix: Add missing Redis environment variables in live service Docker Compose --- deployments/cli/community/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployments/cli/community/docker-compose.yml b/deployments/cli/community/docker-compose.yml index 3833b96ba..d4de33a74 100644 --- a/deployments/cli/community/docker-compose.yml +++ b/deployments/cli/community/docker-compose.yml @@ -94,7 +94,7 @@ services: live: image: artifacts.plane.so/makeplane/plane-live:${APP_RELEASE:-stable} environment: - <<: [*live-env] + <<: [*live-env, *redis-env] deploy: replicas: ${LIVE_REPLICAS:-1} restart_policy: From e5063cd280c3713f955c19983c58c0eabc135081 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Thu, 23 Oct 2025 21:37:30 +0530 Subject: [PATCH 008/212] fix: get work item activity external endpoint (#8005) * fix: activity get endpoint fixes * chore: missing pk added --- apps/api/plane/api/views/issue.py | 40 +++++++++++++++++-------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/apps/api/plane/api/views/issue.py b/apps/api/plane/api/views/issue.py index d3686ceea..c6fd073bf 100644 --- a/apps/api/plane/api/views/issue.py +++ b/apps/api/plane/api/views/issue.py @@ -987,12 +987,12 @@ class LabelDetailAPIEndpoint(LabelListCreateAPIEndpoint): serializer = LabelCreateUpdateSerializer(label, data=request.data, partial=True) if serializer.is_valid(): if ( - str(request.data.get("external_id")) - and (label.external_id != str(request.data.get("external_id"))) + request.data.get("external_id") + and request.data.get("external_source") and Label.objects.filter( project_id=project_id, workspace__slug=slug, - external_source=request.data.get("external_source", label.external_source), + external_source=request.data.get("external_source"), external_id=request.data.get("external_id"), ) .exclude(id=pk) @@ -1695,23 +1695,27 @@ class IssueActivityDetailAPIEndpoint(BaseAPIView): Retrieve details of a specific activity. Excludes comment, vote, reaction, and draft activities. """ - issue_activities = ( - IssueActivity.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id) - .filter( - ~Q(field__in=["comment", "vote", "reaction", "draft"]), - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, + issue_activity = ( + ( + IssueActivity.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id, id=pk) + .filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .filter(project__archived_at__isnull=True) + .select_related("actor", "workspace", "issue", "project") ) - .filter(project__archived_at__isnull=True) - .select_related("actor", "workspace", "issue", "project") - ).order_by(request.GET.get("order_by", "created_at")) + .order_by(request.GET.get("order_by", "created_at")) + .first() + ) - return self.paginate( - request=request, - queryset=(issue_activities), - on_results=lambda issue_activity: IssueActivitySerializer( - issue_activity, many=True, fields=self.fields, expand=self.expand - ).data, + if not issue_activity: + return Response({"message": "Activity not found.", "code": "NOT_FOUND"}, status=status.HTTP_404_NOT_FOUND) + + return Response( + IssueActivitySerializer(issue_activity, fields=self.fields, expand=self.expand).data, + status=status.HTTP_200_OK, ) From b65b1b482841cb883571085a48557b6d6978771e Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Thu, 23 Oct 2025 21:45:25 +0530 Subject: [PATCH 009/212] fix: use issue store was not loading up properly (#8006) * use issue store was not loading up properly , and hence created data issues for rendering filters options * Added React.FC for better type suggestions * chore: observer implementation change --------- Co-authored-by: shivam-jainn --- .../(detail)/[projectId]/cycles/(detail)/mobile-header.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx index e97104c5f..f56a32079 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useState } from "react"; +import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // icons import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; @@ -25,7 +26,7 @@ const SUPPORTED_LAYOUTS = [ { key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: Calendar }, ]; -export const CycleIssuesMobileHeader = () => { +export const CycleIssuesMobileHeader = observer(() => { // router const { workspaceSlug, projectId, cycleId } = useParams(); // states @@ -151,4 +152,4 @@ export const CycleIssuesMobileHeader = () => {
); -}; +}); From 76ffe52cd50486528f3758bbbae8ffe0a89ee637 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:05:46 +0530 Subject: [PATCH 010/212] [WEB-5220] fix: intake actions btn ui #7995 --- apps/web/core/components/inbox/content/inbox-issue-header.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/core/components/inbox/content/inbox-issue-header.tsx b/apps/web/core/components/inbox/content/inbox-issue-header.tsx index 7f375664e..d442c85a4 100644 --- a/apps/web/core/components/inbox/content/inbox-issue-header.tsx +++ b/apps/web/core/components/inbox/content/inbox-issue-header.tsx @@ -327,7 +327,7 @@ export const InboxIssueActionsHeader: FC = observer((p variant="neutral-primary" size="sm" prependIcon={} - className="text-green-500 border-0.5 border-green-500 bg-green-500/20 focus:bg-green-500/20 focus:text-green-500 hover:bg-green-500/40 bg-opacity-20" + className="text-green-500 border border-green-500 bg-green-500/20 focus:bg-green-500/20 focus:text-green-500 hover:bg-green-500/40 bg-opacity-20" onClick={() => handleActionWithPermission( isProjectAdmin, @@ -347,7 +347,7 @@ export const InboxIssueActionsHeader: FC = observer((p variant="neutral-primary" size="sm" prependIcon={} - className="text-red-500 border-0.5 border-red-500 bg-red-500/20 focus:bg-red-500/20 focus:text-red-500 hover:bg-red-500/40 bg-opacity-20" + className="text-red-500 border border-red-500 bg-red-500/20 focus:bg-red-500/20 focus:text-red-500 hover:bg-red-500/40 bg-opacity-20" onClick={() => handleActionWithPermission( isProjectAdmin, From a60d74a3c02d4d35bf9f9c3a59c7a2edf46af714 Mon Sep 17 00:00:00 2001 From: Vihar Kurama Date: Fri, 24 Oct 2025 11:47:51 +0100 Subject: [PATCH 011/212] [WEB-5235] Fix activity timeline ordering for edited comments #8002 --- apps/web/ce/store/issue/issue-details/activity.store.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/ce/store/issue/issue-details/activity.store.ts b/apps/web/ce/store/issue/issue-details/activity.store.ts index ab86bc1c1..b9a70b772 100644 --- a/apps/web/ce/store/issue/issue-details/activity.store.ts +++ b/apps/web/ce/store/issue/issue-details/activity.store.ts @@ -112,10 +112,11 @@ export class IssueActivityStore implements IIssueActivityStore { comments.forEach((commentId) => { const comment = currentStore.comment.getCommentById(commentId); if (!comment) return; + const commentTimestamp = comment.edited_at ?? comment.updated_at ?? comment.created_at; activityComments.push({ id: comment.id, activity_type: EActivityFilterType.COMMENT, - created_at: comment.created_at, + created_at: commentTimestamp, }); }); From 68fd2463f4781f25663fbd924c5f5cddaa1e3913 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 24 Oct 2025 17:21:14 +0530 Subject: [PATCH 012/212] [WEB-5230 | WEB-5231] chore: new empty state implementation (#7972) --- .gitignore | 2 + .../[projectId]/cycles/(list)/page.tsx | 31 +- .../settings/account/api-tokens/page.tsx | 33 +- .../analytics/insight-table/data-table.tsx | 19 +- .../analytics/overview/project-insights.tsx | 14 +- .../work-items/created-vs-resolved.tsx | 14 +- .../analytics/work-items/priority-chart.tsx | 14 +- apps/web/core/components/estimates/root.tsx | 17 +- .../home/widgets/empty-states/links.tsx | 13 +- .../home/widgets/empty-states/recents.tsx | 23 +- .../home/widgets/empty-states/stickies.tsx | 10 +- apps/web/core/components/inbox/root.tsx | 6 +- .../core/components/inbox/sidebar/root.tsx | 43 +-- .../issue-layouts/empty-states/cycle.tsx | 81 +++-- .../empty-states/global-view.tsx | 73 ++--- .../issue-layouts/empty-states/module.tsx | 71 ++--- .../empty-states/project-issues.tsx | 63 ++-- .../empty-states/project-view.tsx | 41 ++- .../issues/workspace-draft/empty-state.tsx | 28 +- .../issues/workspace-draft/root.tsx | 28 +- .../labels/project-setting-label-list.tsx | 29 +- .../components/modules/modules-list-view.tsx | 61 ++-- .../pages/pages-list-main-content.tsx | 124 +++----- .../components/profile/overview/activity.tsx | 7 +- .../overview/priority-distribution.tsx | 17 +- .../profile/overview/state-distribution.tsx | 13 +- .../web/core/components/project/card-list.tsx | 79 +++-- apps/web/core/components/views/views-list.tsx | 54 ++-- .../workspace-notifications/root.tsx | 6 +- .../sidebar/empty-state.tsx | 36 +-- .../workspace-notifications/sidebar/root.tsx | 2 +- .../layouts/auth-layout/project-wrapper.tsx | 31 +- packages/i18n/src/constants/language.ts | 1 + packages/i18n/src/locales/cs/empty-state.ts | 181 +++++++++++ packages/i18n/src/locales/de/empty-state.ts | 190 +++++++++++ packages/i18n/src/locales/en/empty-state.ts | 176 +++++++++++ packages/i18n/src/locales/es/empty-state.ts | 185 +++++++++++ packages/i18n/src/locales/fr/empty-state.ts | 188 +++++++++++ packages/i18n/src/locales/id/empty-state.ts | 185 +++++++++++ packages/i18n/src/locales/index.ts | 20 ++ packages/i18n/src/locales/it/empty-state.ts | 185 +++++++++++ packages/i18n/src/locales/ja/empty-state.ts | 175 +++++++++++ packages/i18n/src/locales/ko/empty-state.ts | 174 +++++++++++ packages/i18n/src/locales/pl/empty-state.ts | 186 +++++++++++ .../i18n/src/locales/pt-BR/empty-state.ts | 185 +++++++++++ packages/i18n/src/locales/ro/empty-state.ts | 184 +++++++++++ packages/i18n/src/locales/ru/empty-state.ts | 187 +++++++++++ packages/i18n/src/locales/sk/empty-state.ts | 184 +++++++++++ .../i18n/src/locales/tr-TR/empty-state.ts | 183 +++++++++++ packages/i18n/src/locales/ua/empty-state.ts | 186 +++++++++++ .../i18n/src/locales/vi-VN/empty-state.ts | 184 +++++++++++ .../i18n/src/locales/zh-CN/empty-state.ts | 170 ++++++++++ .../i18n/src/locales/zh-TW/empty-state.ts | 170 ++++++++++ .../empty-state/assets-showcase.stories.tsx | 204 ++++++++++++ .../src/empty-state/assets/asset-registry.tsx | 130 ++++++++ .../src/empty-state/assets/asset-types.ts | 52 +++ .../assets/horizontal-stack/constant.tsx | 5 + .../assets/horizontal-stack/index.ts | 1 + .../assets/horizontal-stack/project.tsx | 69 ++++ .../assets/illustration/constant.tsx | 12 + .../empty-state/assets/illustration/inbox.tsx | 134 ++++++++ .../empty-state/assets/illustration/index.ts | 2 + .../assets/illustration/search.tsx | 58 ++++ .../propel/src/empty-state/assets/index.ts | 7 +- .../compact-empty-state.stories.tsx | 142 +++++++++ .../src/empty-state/compact-empty-state.tsx | 60 ++++ .../detailed-empty-state.stories.tsx | 295 ++++++++++++++++++ .../src/empty-state/detailed-empty-state.tsx | 52 +++ .../src/empty-state/empty-state.stories.tsx | 100 ------ .../propel/src/empty-state/empty-state.tsx | 93 +++--- packages/propel/src/empty-state/index.ts | 4 + packages/propel/src/empty-state/types.ts | 24 ++ 72 files changed, 5260 insertions(+), 746 deletions(-) create mode 100644 packages/i18n/src/locales/cs/empty-state.ts create mode 100644 packages/i18n/src/locales/de/empty-state.ts create mode 100644 packages/i18n/src/locales/en/empty-state.ts create mode 100644 packages/i18n/src/locales/es/empty-state.ts create mode 100644 packages/i18n/src/locales/fr/empty-state.ts create mode 100644 packages/i18n/src/locales/id/empty-state.ts create mode 100644 packages/i18n/src/locales/it/empty-state.ts create mode 100644 packages/i18n/src/locales/ja/empty-state.ts create mode 100644 packages/i18n/src/locales/ko/empty-state.ts create mode 100644 packages/i18n/src/locales/pl/empty-state.ts create mode 100644 packages/i18n/src/locales/pt-BR/empty-state.ts create mode 100644 packages/i18n/src/locales/ro/empty-state.ts create mode 100644 packages/i18n/src/locales/ru/empty-state.ts create mode 100644 packages/i18n/src/locales/sk/empty-state.ts create mode 100644 packages/i18n/src/locales/tr-TR/empty-state.ts create mode 100644 packages/i18n/src/locales/ua/empty-state.ts create mode 100644 packages/i18n/src/locales/vi-VN/empty-state.ts create mode 100644 packages/i18n/src/locales/zh-CN/empty-state.ts create mode 100644 packages/i18n/src/locales/zh-TW/empty-state.ts create mode 100644 packages/propel/src/empty-state/assets-showcase.stories.tsx create mode 100644 packages/propel/src/empty-state/assets/asset-registry.tsx create mode 100644 packages/propel/src/empty-state/assets/asset-types.ts create mode 100644 packages/propel/src/empty-state/assets/horizontal-stack/project.tsx create mode 100644 packages/propel/src/empty-state/assets/illustration/constant.tsx create mode 100644 packages/propel/src/empty-state/assets/illustration/inbox.tsx create mode 100644 packages/propel/src/empty-state/assets/illustration/index.ts create mode 100644 packages/propel/src/empty-state/assets/illustration/search.tsx create mode 100644 packages/propel/src/empty-state/compact-empty-state.stories.tsx create mode 100644 packages/propel/src/empty-state/compact-empty-state.tsx create mode 100644 packages/propel/src/empty-state/detailed-empty-state.stories.tsx create mode 100644 packages/propel/src/empty-state/detailed-empty-state.tsx delete mode 100644 packages/propel/src/empty-state/empty-state.stories.tsx create mode 100644 packages/propel/src/empty-state/types.ts diff --git a/.gitignore b/.gitignore index 0edc47dcc..053db6d29 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,5 @@ dev-editor storybook-static CLAUDE.md + +temp/ \ No newline at end of file diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx index 681ec22e0..643ee2850 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx @@ -6,6 +6,7 @@ import { useParams } from "next/navigation"; // plane imports import { EUserPermissionsLevel, CYCLE_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import type { TCycleFilters } from "@plane/types"; import { EUserProjectRoles } from "@plane/types"; // components @@ -15,7 +16,6 @@ import { PageHead } from "@/components/core/page-title"; import { CycleAppliedFiltersList } from "@/components/cycles/applied-filters"; import { CyclesView } from "@/components/cycles/cycles-view"; import { CycleCreateUpdateModal } from "@/components/cycles/modal"; -import { ComicBoxButton } from "@/components/empty-state/comic-box-button"; import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader"; // hooks @@ -96,22 +96,19 @@ const ProjectCyclesPage = observer(() => { /> {totalCycles === 0 ? (
- { - setCreateModal(true); - }} - disabled={!hasMemberLevelPermission} - /> - } + setCreateModal(true), + variant: "primary", + disabled: !hasMemberLevelPermission, + "data-ph-element": CYCLE_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON, + }, + ]} />
) : ( diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx index d37711e38..0cf57fa18 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx @@ -7,18 +7,17 @@ import useSWR from "swr"; import { PROFILE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // component +import { EmptyStateCompact } from "@plane/propel/empty-state"; import { APITokenService } from "@plane/services"; import { CreateApiTokenModal } from "@/components/api-token/modal/create-token-modal"; import { ApiTokenListItem } from "@/components/api-token/token-list-item"; import { PageHead } from "@/components/core/page-title"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { SettingsHeading } from "@/components/settings/heading"; import { APITokenSettingsLoader } from "@/components/ui/loader/settings/api-token"; import { API_TOKENS_LIST } from "@/constants/fetch-keys"; // store hooks import { captureClick } from "@/helpers/event-tracker.helper"; import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; const apiTokenService = new APITokenService(); @@ -30,8 +29,6 @@ const ApiTokensPage = observer(() => { const { t } = useTranslation(); // store hooks const { currentWorkspace } = useWorkspace(); - // derived values - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/api-tokens" }); const { data: tokens } = useSWR(API_TOKENS_LIST, () => apiTokenService.list()); @@ -70,7 +67,7 @@ const ApiTokensPage = observer(() => { ) : ( -
+
{ }, }} /> -
- { captureClick({ elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_PAT_BUTTON, }); setIsCreateTokenModalOpen(true); }, - }} - /> -
+ }, + ]} + align="start" + rootClassName="py-20" + />
)} diff --git a/apps/web/core/components/analytics/insight-table/data-table.tsx b/apps/web/core/components/analytics/insight-table/data-table.tsx index be802457e..b2729411d 100644 --- a/apps/web/core/components/analytics/insight-table/data-table.tsx +++ b/apps/web/core/components/analytics/insight-table/data-table.tsx @@ -14,18 +14,16 @@ import { getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, - getPaginationRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; import { Search, X } from "lucide-react"; // plane package imports import { useTranslation } from "@plane/i18n"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; 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 AnalyticsEmptyState from "../empty-state"; interface DataTableProps { columns: ColumnDef[]; @@ -42,7 +40,6 @@ export function DataTable({ columns, data, searchPlaceholder, act const { t } = useTranslation(); const inputRef = React.useRef(null); const [isSearchOpen, setIsSearchOpen] = React.useState(false); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-table" }); const table = useReactTable({ data, @@ -156,14 +153,12 @@ export function DataTable({ columns, data, searchPlaceholder, act ) : ( -
- -
+
)} diff --git a/apps/web/core/components/analytics/overview/project-insights.tsx b/apps/web/core/components/analytics/overview/project-insights.tsx index a72c79b83..c1fd6584e 100644 --- a/apps/web/core/components/analytics/overview/project-insights.tsx +++ b/apps/web/core/components/analytics/overview/project-insights.tsx @@ -4,15 +4,14 @@ import { useParams } from "next/navigation"; import useSWR from "swr"; // plane package imports import { useTranslation } from "@plane/i18n"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; import type { TChartData } from "@plane/types"; // hooks import { useAnalytics } from "@/hooks/store/use-analytics"; // services -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { AnalyticsService } from "@/services/analytics.service"; // plane web components import AnalyticsSectionWrapper from "../analytics-section-wrapper"; -import AnalyticsEmptyState from "../empty-state"; import { ProjectInsightsLoader } from "../loaders"; const RadarChart = dynamic(() => @@ -29,7 +28,6 @@ const ProjectInsights = observer(() => { const workspaceSlug = params.workspaceSlug.toString(); const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } = useAnalytics(); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-radar" }); const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR( `radar-chart-project-insights-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`, @@ -56,11 +54,11 @@ const ProjectInsights = observer(() => { {isLoadingProjectInsight ? ( ) : projectInsightsData && projectInsightsData?.length == 0 ? ( - ) : (
diff --git a/apps/web/core/components/analytics/work-items/created-vs-resolved.tsx b/apps/web/core/components/analytics/work-items/created-vs-resolved.tsx index 56c7bbd80..7b40ccdb9 100644 --- a/apps/web/core/components/analytics/work-items/created-vs-resolved.tsx +++ b/apps/web/core/components/analytics/work-items/created-vs-resolved.tsx @@ -5,16 +5,15 @@ import useSWR from "swr"; // plane package imports import { useTranslation } from "@plane/i18n"; import { AreaChart } from "@plane/propel/charts/area-chart"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; import type { IChartResponse, TChartData } from "@plane/types"; import { renderFormattedDate } from "@plane/utils"; // hooks import { useAnalytics } from "@/hooks/store/use-analytics"; // services -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { AnalyticsService } from "@/services/analytics.service"; // plane web components import AnalyticsSectionWrapper from "../analytics-section-wrapper"; -import AnalyticsEmptyState from "../empty-state"; import { ChartLoader } from "../loaders"; const analyticsService = new AnalyticsService(); @@ -31,7 +30,6 @@ const CreatedVsResolved = observer(() => { const params = useParams(); const { t } = useTranslation(); const workspaceSlug = params.workspaceSlug.toString(); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-area" }); const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR( `created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}-${isEpic}`, () => @@ -121,11 +119,11 @@ const CreatedVsResolved = observer(() => { }} /> ) : ( - )} diff --git a/apps/web/core/components/analytics/work-items/priority-chart.tsx b/apps/web/core/components/analytics/work-items/priority-chart.tsx index f77248d49..857eca203 100644 --- a/apps/web/core/components/analytics/work-items/priority-chart.tsx +++ b/apps/web/core/components/analytics/work-items/priority-chart.tsx @@ -11,15 +11,14 @@ import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, CHART_COLOR_PALETTES, import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; import { BarChart } from "@plane/propel/charts/bar-chart"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; import type { TBarItem, TChart, TChartDatum, ChartXAxisProperty, ChartYAxisMetric } from "@plane/types"; // plane web components import { generateExtendedColors, parseChartData } from "@/components/chart/utils"; // hooks import { useAnalytics } from "@/hooks/store/use-analytics"; import { useProjectState } from "@/hooks/store/use-project-state"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { AnalyticsService } from "@/services/analytics.service"; -import AnalyticsEmptyState from "../empty-state"; import { exportCSV } from "../export"; import { DataTable } from "../insight-table/data-table"; import { ChartLoader } from "../loaders"; @@ -46,7 +45,6 @@ const analyticsService = new AnalyticsService(); const PriorityChart = observer((props: Props) => { const { x_axis, y_axis, group_by } = props; const { t } = useTranslation(); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-bar" }); // store hooks const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView, isEpic } = useAnalytics(); const { workspaceStates } = useProjectState(); @@ -232,11 +230,11 @@ const PriorityChart = observer((props: Props) => { /> ) : ( - )}
diff --git a/apps/web/core/components/estimates/root.tsx b/apps/web/core/components/estimates/root.tsx index f562d924c..01ac55a6a 100644 --- a/apps/web/core/components/estimates/root.tsx +++ b/apps/web/core/components/estimates/root.tsx @@ -5,6 +5,7 @@ import useSWR from "swr"; // plane imports import { useTranslation } from "@plane/i18n"; // hooks +import { EmptyStateCompact } from "@plane/propel/empty-state"; import { useProjectEstimates } from "@/hooks/store/estimates"; import { useProject } from "@/hooks/store/use-project"; // plane web components @@ -13,7 +14,6 @@ import { UpdateEstimateModal } from "@/plane-web/components/estimates"; import { SettingsHeading } from "../settings/heading"; import { CreateEstimateModal } from "./create/modal"; import { DeleteEstimateModal } from "./delete/modal"; -import { EstimateEmptyScreen } from "./empty-screen"; import { EstimateDisableSwitch } from "./estimate-disable-switch"; import { EstimateList } from "./estimate-list"; import { EstimateLoaderScreen } from "./loader-screen"; @@ -76,7 +76,20 @@ export const EstimateRoot: FC = observer((props) => { />
) : ( - setIsEstimateCreateModalOpen(true)} /> + setIsEstimateCreateModalOpen(true), + }, + ]} + align="start" + rootClassName="py-20" + /> )} {/* archived estimates section */} diff --git a/apps/web/core/components/home/widgets/empty-states/links.tsx b/apps/web/core/components/home/widgets/empty-states/links.tsx index 37e04fea7..d44293432 100644 --- a/apps/web/core/components/home/widgets/empty-states/links.tsx +++ b/apps/web/core/components/home/widgets/empty-states/links.tsx @@ -1,14 +1,15 @@ -import { Link2 } from "lucide-react"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; export const LinksEmptyState = () => { const { t } = useTranslation(); return ( -
-
- -
{t("home.quick_links.empty")}
-
+
+
); }; diff --git a/apps/web/core/components/home/widgets/empty-states/recents.tsx b/apps/web/core/components/home/widgets/empty-states/recents.tsx index 4839e9bfa..bca5bf603 100644 --- a/apps/web/core/components/home/widgets/empty-states/recents.tsx +++ b/apps/web/core/components/home/widgets/empty-states/recents.tsx @@ -1,41 +1,40 @@ -import { History } from "lucide-react"; import { useTranslation } from "@plane/i18n"; -import { PageIcon, ProjectIcon, WorkItemsIcon } from "@plane/propel/icons"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; +import type { CompactAssetType } from "@plane/propel/empty-state"; -const getDisplayContent = (type: string) => { +const getDisplayContent = (type: string): { assetKey: CompactAssetType; text: string } => { switch (type) { case "project": return { - icon: , + assetKey: "project", text: "home.recents.empty.project", }; case "page": return { - icon: , + assetKey: "note", text: "home.recents.empty.page", }; case "issue": return { - icon: , + assetKey: "work-item", text: "home.recents.empty.issue", }; default: return { - icon: , + assetKey: "work-item", text: "home.recents.empty.default", }; } }; + export const RecentsEmptyState = ({ type }: { type: string }) => { const { t } = useTranslation(); - const { icon, text } = getDisplayContent(type); + const { assetKey, text } = getDisplayContent(type); return ( -
-
- {icon}
{t(text)}
-
+
+
); }; diff --git a/apps/web/core/components/home/widgets/empty-states/stickies.tsx b/apps/web/core/components/home/widgets/empty-states/stickies.tsx index 5a639ceef..37a3299da 100644 --- a/apps/web/core/components/home/widgets/empty-states/stickies.tsx +++ b/apps/web/core/components/home/widgets/empty-states/stickies.tsx @@ -1,15 +1,11 @@ -// plane ui import { useTranslation } from "@plane/i18n"; -import { RecentStickyIcon } from "@plane/propel/icons"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; export const StickiesEmptyState = () => { const { t } = useTranslation(); return ( -
-
- -
{t("stickies.empty_state.simple")}
-
+
+
); }; diff --git a/apps/web/core/components/inbox/root.tsx b/apps/web/core/components/inbox/root.tsx index 4327c3aef..a8660ecce 100644 --- a/apps/web/core/components/inbox/root.tsx +++ b/apps/web/core/components/inbox/root.tsx @@ -4,11 +4,11 @@ import { observer } from "mobx-react"; import { PanelLeft } from "lucide-react"; // plane imports import { useTranslation } from "@plane/i18n"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; import { IntakeIcon } from "@plane/propel/icons"; import { EInboxIssueCurrentTab } from "@plane/types"; import { cn } from "@plane/utils"; // components -import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root"; import { InboxContentRoot } from "@/components/inbox/content"; import { InboxSidebar } from "@/components/inbox/sidebar"; import { InboxLayoutLoader } from "@/components/ui/loader/layouts/project-inbox/inbox-layout-loader"; @@ -101,9 +101,7 @@ export const InboxIssueRoot: FC = observer((props) => { inboxIssueId={inboxIssueId.toString()} /> ) : ( -
- -
+ )}
diff --git a/apps/web/core/components/inbox/sidebar/root.tsx b/apps/web/core/components/inbox/sidebar/root.tsx index b76c63834..88b59dc62 100644 --- a/apps/web/core/components/inbox/sidebar/root.tsx +++ b/apps/web/core/components/inbox/sidebar/root.tsx @@ -4,20 +4,19 @@ import type { FC } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import type { TInboxIssueCurrentTab } from "@plane/types"; import { EInboxIssueCurrentTab } from "@plane/types"; // plane imports import { Header, Loader, EHeaderVariant } from "@plane/ui"; import { cn } from "@plane/utils"; // components -import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root"; import { InboxSidebarLoader } from "@/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader"; // hooks import { useProject } from "@/hooks/store/use-project"; import { useProjectInbox } from "@/hooks/store/use-project-inbox"; import { useAppRouter } from "@/hooks/use-app-router"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // local imports import { FiltersRoot } from "../inbox-filter"; import { InboxIssueAppliedFilters } from "../inbox-filter/applied-filters/root"; @@ -62,11 +61,6 @@ export const InboxSidebar: FC = observer((props) => { getAppliedFiltersCount, } = useProjectInbox(); // derived values - const sidebarAssetPath = useResolvedAssetPath({ basePath: "/empty-state/intake/intake-issue" }); - const sidebarFilterAssetPath = useResolvedAssetPath({ - basePath: "/empty-state/intake/filter-issue", - }); - const fetchNextPages = useCallback(() => { if (!workspaceSlug || !projectId) return; fetchInboxPaginationIssues(workspaceSlug.toString(), projectId.toString()); @@ -141,22 +135,33 @@ export const InboxSidebar: FC = observer((props) => { ) : (
{getAppliedFiltersCount > 0 ? ( - ) : currentTab === EInboxIssueCurrentTab.OPEN ? ( - router.push(`/${workspaceSlug}/projects/${projectId}/intake`), + variant: "primary", + }, + ]} /> ) : ( - )}
diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/cycle.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/cycle.tsx index 30a82d476..c7ae0e89f 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/cycle.tsx @@ -7,19 +7,18 @@ import { useParams } from "next/navigation"; // plane imports import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { ISearchIssueResponse } from "@plane/types"; import { EIssuesStoreType, EUserProjectRoles } from "@plane/types"; // components import { ExistingIssuesListModal } from "@/components/core/modals/existing-issues-list-modal"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { captureClick } from "@/helpers/event-tracker.helper"; import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useCycle } from "@/hooks/store/use-cycle"; import { useIssues } from "@/hooks/store/use-issues"; import { useUserPermissions } from "@/hooks/store/user"; import { useWorkItemFilterInstance } from "@/hooks/store/work-item-filters/use-work-item-filter-instance"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const CycleEmptyState: React.FC = observer(() => { // router @@ -33,31 +32,18 @@ export const CycleEmptyState: React.FC = observer(() => { const { t } = useTranslation(); // store hooks const { getCycleById } = useCycle(); - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { issues } = useIssues(EIssuesStoreType.CYCLE); const { toggleCreateIssueModal } = useCommandPalette(); const { allowPermissions } = useUserPermissions(); // derived values const cycleWorkItemFilter = cycleId ? useWorkItemFilterInstance(EIssuesStoreType.CYCLE, cycleId) : undefined; const cycleDetails = cycleId ? getCycleById(cycleId) : undefined; - const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {}); const isCompletedAndEmpty = isCompletedCycleSnapshotAvailable || cycleDetails?.status?.toLowerCase() === "completed"; - const additionalPath = activeLayout ?? "list"; const canPerformEmptyStateActions = allowPermissions( [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], EUserPermissionsLevel.PROJECT ); - const emptyFilterResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/empty-filters/", - additionalPath: additionalPath, - }); - const noIssueResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/cycle-issues/", - additionalPath: additionalPath, - }); - const completedNoIssuesResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/cycle/completed-no-issues", - }); const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !cycleId) return; @@ -94,39 +80,50 @@ export const CycleEmptyState: React.FC = observer(() => { />
{isCompletedAndEmpty ? ( - ) : cycleWorkItemFilter?.hasActiveFilters ? ( - ) : ( - { - captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.CYCLE }); - toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); + { + captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.CYCLE }); + toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); + }, + disabled: !canPerformEmptyStateActions, + variant: "primary", + "data-ph-element": WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.CYCLE, }, - disabled: !canPerformEmptyStateActions, - }} - secondaryButton={{ - text: t("project_cycles.empty_state.no_issues.secondary_button.text"), - onClick: () => setCycleIssuesListModal(true), - disabled: !canPerformEmptyStateActions, - }} + { + label: t("project.cycle_work_items.cta_secondary"), + onClick: () => setCycleIssuesListModal(true), + disabled: !canPerformEmptyStateActions, + variant: "outline-primary", + "data-ph-element": WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.CYCLE, + }, + ]} /> )}
diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/global-view.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/global-view.tsx index 00935f1cd..e8ded291e 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/global-view.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/global-view.tsx @@ -1,21 +1,16 @@ import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; // plane imports import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { EIssuesStoreType, EUserWorkspaceRoles } from "@plane/types"; -// components -import { ComicBoxButton } from "@/components/empty-state/comic-box-button"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; // hooks import { captureClick } from "@/helpers/event-tracker.helper"; import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useProject } from "@/hooks/store/use-project"; import { useUserPermissions } from "@/hooks/store/user"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const GlobalViewEmptyState: React.FC = observer(() => { - const { globalViewId } = useParams(); // plane imports const { t } = useTranslation(); // store hooks @@ -27,56 +22,46 @@ export const GlobalViewEmptyState: React.FC = observer(() => { [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], EUserPermissionsLevel.WORKSPACE ); - const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId?.toString() ?? ""); - const currentView = isDefaultView && globalViewId ? globalViewId : "custom-view"; - const resolvedCurrentView = currentView?.toString(); - const noProjectResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" }); - const globalViewsResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/all-issues/", - additionalPath: resolvedCurrentView, - }); if (workspaceProjectIds?.length === 0) { return ( - { + assetKey="project" + assetClassName="size-40" + actions={[ + { + label: t("workspace_projects.empty_state.no_projects.primary_button.text"), + onClick: () => { toggleCreateProjectModal(true); captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.GLOBAL_VIEW }); - }} - disabled={!hasMemberLevelPermission} - /> - } + }, + disabled: !hasMemberLevelPermission, + variant: "primary", + }, + ]} /> ); } return ( - { - captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.GLOBAL_VIEW }); - toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - }, - disabled: !hasMemberLevelPermission, - } - : undefined - } + { + captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.GLOBAL_VIEW }); + toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); + }, + disabled: !hasMemberLevelPermission, + variant: "primary", + }, + ]} /> ); }); diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/module.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/module.tsx index 2510f5b5d..f884de82d 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/module.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/module.tsx @@ -6,19 +6,18 @@ import { useParams } from "next/navigation"; // plane imports import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { ISearchIssueResponse } from "@plane/types"; import { EIssuesStoreType, EUserProjectRoles } from "@plane/types"; // components import { ExistingIssuesListModal } from "@/components/core/modals/existing-issues-list-modal"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { captureClick } from "@/helpers/event-tracker.helper"; // hooks import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useIssues } from "@/hooks/store/use-issues"; import { useUserPermissions } from "@/hooks/store/user"; import { useWorkItemFilterInstance } from "@/hooks/store/work-item-filters/use-work-item-filter-instance"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const ModuleEmptyState: React.FC = observer(() => { // router @@ -31,25 +30,15 @@ export const ModuleEmptyState: React.FC = observer(() => { // plane hooks const { t } = useTranslation(); // store hooks - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); + const { issues } = useIssues(EIssuesStoreType.MODULE); const { toggleCreateIssueModal } = useCommandPalette(); const { allowPermissions } = useUserPermissions(); // derived values const moduleWorkItemFilter = moduleId ? useWorkItemFilterInstance(EIssuesStoreType.MODULE, moduleId) : undefined; - const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const additionalPath = activeLayout ?? "list"; const canPerformEmptyStateActions = allowPermissions( [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], EUserPermissionsLevel.PROJECT ); - const emptyFilterResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/empty-filters/", - additionalPath: additionalPath, - }); - const moduleIssuesResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/module-issues/", - additionalPath: additionalPath, - }); const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !moduleId) return; @@ -85,33 +74,41 @@ export const ModuleEmptyState: React.FC = observer(() => { />
{moduleWorkItemFilter?.hasActiveFilters ? ( - ) : ( - { - captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.MODULE }); - toggleCreateIssueModal(true, EIssuesStoreType.MODULE); + { + captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.MODULE }); + toggleCreateIssueModal(true, EIssuesStoreType.MODULE); + }, + disabled: !canPerformEmptyStateActions, + variant: "primary", }, - disabled: !canPerformEmptyStateActions, - }} - secondaryButton={{ - text: t("project_module.empty_state.no_issues.secondary_button.text"), - onClick: () => setModuleIssuesListModal(true), - disabled: !canPerformEmptyStateActions, - }} + { + label: t("project.module_work_items.cta_secondary"), + onClick: () => setModuleIssuesListModal(true), + disabled: !canPerformEmptyStateActions, + variant: "outline-primary", + }, + ]} /> )}
diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx index c8a776850..dd52019d6 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -3,17 +3,14 @@ import { useParams } from "next/navigation"; // plane imports import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { EIssuesStoreType, EUserProjectRoles } from "@plane/types"; // components -import { ComicBoxButton } from "@/components/empty-state/comic-box-button"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { captureClick } from "@/helpers/event-tracker.helper"; // hooks import { useCommandPalette } from "@/hooks/store/use-command-palette"; -import { useIssues } from "@/hooks/store/use-issues"; import { useUserPermissions } from "@/hooks/store/user"; import { useWorkItemFilterInstance } from "@/hooks/store/work-item-filters/use-work-item-filter-instance"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const ProjectEmptyState: React.FC = observer(() => { // router @@ -23,53 +20,47 @@ export const ProjectEmptyState: React.FC = observer(() => { const { t } = useTranslation(); // store hooks const { toggleCreateIssueModal } = useCommandPalette(); - const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); const { allowPermissions } = useUserPermissions(); // derived values const projectWorkItemFilter = projectId ? useWorkItemFilterInstance(EIssuesStoreType.PROJECT, projectId) : undefined; - const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const additionalPath = projectWorkItemFilter?.hasActiveFilters ? (activeLayout ?? "list") : undefined; + const canPerformEmptyStateActions = allowPermissions( [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], EUserPermissionsLevel.PROJECT ); - const emptyFilterResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/empty-filters/", - additionalPath: additionalPath, - }); - const projectIssuesResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/onboarding/issues", - }); return (
{projectWorkItemFilter?.hasActiveFilters ? ( - ) : ( - { + { captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.WORK_ITEMS }); toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - }} - disabled={!canPerformEmptyStateActions} - /> - } + }, + disabled: !canPerformEmptyStateActions, + variant: "primary", + }, + ]} /> )}
diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/project-view.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/project-view.tsx index 6b03a61e0..165263177 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/project-view.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/project-view.tsx @@ -1,15 +1,12 @@ import { observer } from "mobx-react"; -import { PlusIcon } from "lucide-react"; // components import { EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { EIssuesStoreType } from "@plane/types"; -import { EmptyState } from "@/components/common/empty-state"; import { captureClick } from "@/helpers/event-tracker.helper"; // hooks import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useUserPermissions } from "@/hooks/store/user"; -// assets -import emptyIssue from "@/public/empty-state/issue.svg"; export const ProjectViewEmptyState: React.FC = observer(() => { // store hooks @@ -23,24 +20,22 @@ export const ProjectViewEmptyState: React.FC = observer(() => { ); return ( -
- , - onClick: () => { - captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.PROJECT_VIEW }); - toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW); - }, - } - : undefined - } - /> -
+ // TODO: Add translation + { + captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.PROJECT_VIEW }); + toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW); + }, + disabled: !isCreatingIssueAllowed, + variant: "primary", + }, + ]} + /> ); }); diff --git a/apps/web/core/components/issues/workspace-draft/empty-state.tsx b/apps/web/core/components/issues/workspace-draft/empty-state.tsx index 4d6a195a5..89ff7cb14 100644 --- a/apps/web/core/components/issues/workspace-draft/empty-state.tsx +++ b/apps/web/core/components/issues/workspace-draft/empty-state.tsx @@ -6,12 +6,11 @@ import { Fragment, useState } from "react"; import { observer } from "mobx-react"; import { EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { EIssuesStoreType, EUserWorkspaceRoles } from "@plane/types"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { CreateUpdateIssueModal } from "@/components/issues/issue-modal/modal"; // constants import { useUserPermissions } from "@/hooks/store/user"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const WorkspaceDraftEmptyState: FC = observer(() => { // state @@ -24,7 +23,6 @@ export const WorkspaceDraftEmptyState: FC = observer(() => { [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], EUserPermissionsLevel.WORKSPACE ); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/cycles" }); return ( @@ -35,17 +33,21 @@ export const WorkspaceDraftEmptyState: FC = observer(() => { isDraft />
- { - setIsDraftIssueModalOpen(true); + { + setIsDraftIssueModalOpen(true); + }, + disabled: !canPerformEmptyStateActions, + variant: "primary", }, - disabled: !canPerformEmptyStateActions, - }} + ]} />
diff --git a/apps/web/core/components/issues/workspace-draft/root.tsx b/apps/web/core/components/issues/workspace-draft/root.tsx index cf74383a9..636b898e3 100644 --- a/apps/web/core/components/issues/workspace-draft/root.tsx +++ b/apps/web/core/components/issues/workspace-draft/root.tsx @@ -7,11 +7,10 @@ import useSWR from "swr"; // plane imports import { EUserPermissionsLevel, EDraftIssuePaginationType, PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { EUserWorkspaceRoles } from "@plane/types"; // components import { cn } from "@plane/utils"; -import { ComicBoxButton } from "@/components/empty-state/comic-box-button"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { captureClick } from "@/helpers/event-tracker.helper"; // constants @@ -70,23 +69,22 @@ export const WorkspaceDraftIssuesRoot: FC = observer( if (workspaceProjectIds?.length === 0) return ( - { + assetKey="project" + assetClassName="size-40" + actions={[ + { + label: t("workspace_projects.empty_state.no_projects.primary_button.text"), + onClick: () => { toggleCreateProjectModal(true); captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON }); - }} - disabled={!hasMemberLevelPermission} - /> - } + }, + disabled: !hasMemberLevelPermission, + variant: "primary", + }, + ]} /> ); diff --git a/apps/web/core/components/labels/project-setting-label-list.tsx b/apps/web/core/components/labels/project-setting-label-list.tsx index d634e248b..62f60250e 100644 --- a/apps/web/core/components/labels/project-setting-label-list.tsx +++ b/apps/web/core/components/labels/project-setting-label-list.tsx @@ -6,9 +6,9 @@ import { useParams } from "next/navigation"; // plane imports import { EUserPermissions, EUserPermissionsLevel, PROJECT_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; import type { IIssueLabel } from "@plane/types"; import { Loader } from "@plane/ui"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import type { TLabelOperationsCallbacks } from "@/components/labels"; import { CreateUpdateLabelInline, @@ -20,7 +20,6 @@ import { import { captureClick } from "@/helpers/event-tracker.helper"; import { useLabel } from "@/hooks/store/use-label"; import { useUserPermissions } from "@/hooks/store/user"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // local imports import { SettingsHeading } from "../settings/heading"; @@ -40,7 +39,6 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { const { allowPermissions } = useUserPermissions(); // derived values const isEditable = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/project-settings/labels" }); const labelOperationsCallbacks: TLabelOperationsCallbacks = { createLabel: (data: Partial) => createLabel(workspaceSlug?.toString(), projectId?.toString(), data), updateLabel: (labelId: string, data: Partial) => @@ -111,24 +109,25 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { )} {projectLabels ? ( projectLabels.length === 0 && !showLabelForm ? ( -
- { newLabel(); captureClick({ elementName: PROJECT_SETTINGS_TRACKER_ELEMENTS.LABELS_EMPTY_STATE_CREATE_BUTTON, }); }, - }} - assetPath={resolvedPath} - className="w-full !px-0 !py-0" - size="md" - /> -
+ }, + ]} + align="start" + rootClassName="py-20" + /> ) : ( projectLabelsTree && (
diff --git a/apps/web/core/components/modules/modules-list-view.tsx b/apps/web/core/components/modules/modules-list-view.tsx index 877855c8e..5d80ccf2c 100644 --- a/apps/web/core/components/modules/modules-list-view.tsx +++ b/apps/web/core/components/modules/modules-list-view.tsx @@ -1,14 +1,12 @@ import { observer } from "mobx-react"; -import Image from "next/image"; import { useParams, useSearchParams } from "next/navigation"; // components import { EUserPermissionsLevel, MODULE_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { EUserProjectRoles } from "@plane/types"; import { ContentWrapper, Row, ERowVariant } from "@plane/ui"; import { ListLayout } from "@/components/core/list"; -import { ComicBoxButton } from "@/components/empty-state/comic-box-button"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "@/components/modules"; import { CycleModuleBoardLayoutLoader } from "@/components/ui/loader/cycle-module-board-loader"; import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader"; @@ -18,9 +16,6 @@ import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useModule } from "@/hooks/store/use-module"; import { useModuleFilter } from "@/hooks/store/use-module-filter"; import { useUserPermissions } from "@/hooks/store/user"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -import AllFiltersImage from "@/public/empty-state/module/all-filters.svg"; -import NameFilterImage from "@/public/empty-state/module/name-filter.svg"; export const ModulesListView: React.FC = observer(() => { // router @@ -32,7 +27,7 @@ export const ModulesListView: React.FC = observer(() => { // store hooks const { toggleCreateModuleModal } = useCommandPalette(); const { getProjectModuleIds, getFilteredModuleIds, loader } = useModule(); - const { currentProjectDisplayFilters: displayFilters, searchQuery } = useModuleFilter(); + const { currentProjectDisplayFilters: displayFilters } = useModuleFilter(); const { allowPermissions } = useUserPermissions(); // derived values const projectModuleIds = projectId ? getProjectModuleIds(projectId.toString()) : undefined; @@ -41,9 +36,6 @@ export const ModulesListView: React.FC = observer(() => { [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], EUserPermissionsLevel.PROJECT ); - const generalViewResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/onboarding/modules", - }); if (loader || !projectModuleIds || !filteredModuleIds) return ( @@ -56,42 +48,29 @@ export const ModulesListView: React.FC = observer(() => { if (projectModuleIds.length === 0) return ( - { - toggleCreateModuleModal(true); - }} - disabled={!canPerformEmptyStateActions} - /> - } + toggleCreateModuleModal(true), + disabled: !canPerformEmptyStateActions, + variant: "primary", + "data-ph-element": MODULE_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON, + }, + ]} /> ); if (filteredModuleIds.length === 0) return ( -
-
- No matching modules -
No matching modules
-

- {searchQuery.trim() === "" - ? "Remove the filters to see all modules" - : "Remove the search criteria to see all modules"} -

-
-
+ ); return ( diff --git a/apps/web/core/components/pages/pages-list-main-content.tsx b/apps/web/core/components/pages/pages-list-main-content.tsx index ee39f38c0..42fbf1cce 100644 --- a/apps/web/core/components/pages/pages-list-main-content.tsx +++ b/apps/web/core/components/pages/pages-list-main-content.tsx @@ -1,7 +1,6 @@ "use client"; import { useState } from "react"; import { observer } from "mobx-react"; -import Image from "next/image"; // plane imports import { useParams, useRouter } from "next/navigation"; import { @@ -11,16 +10,15 @@ import { PROJECT_PAGE_TRACKER_EVENTS, } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { TPage, TPageNavigationTabs } from "@plane/types"; import { EUserProjectRoles } from "@plane/types"; // components -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { PageLoader } from "@/components/pages/loaders/page-loader"; import { captureClick, captureError, captureSuccess } from "@/helpers/event-tracker.helper"; import { useProject } from "@/hooks/store/use-project"; import { useUserPermissions } from "@/hooks/store/user"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // plane web hooks import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store"; @@ -52,23 +50,6 @@ export const PagesListMainContent: React.FC = observer((props) => { [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], EUserPermissionsLevel.PROJECT ); - const generalPageResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/onboarding/pages", - }); - const publicPageResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/wiki/public", - }); - const privatePageResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/wiki/private", - }); - const archivedPageResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/wiki/archived", - }); - const resolvedFiltersImage = useResolvedAssetPath({ basePath: "/empty-state/wiki/all-filters", extension: "svg" }); - const resolvedNameFilterImage = useResolvedAssetPath({ - basePath: "/empty-state/wiki/name-filter", - extension: "svg", - }); // handle page create const handleCreatePage = async () => { @@ -111,80 +92,79 @@ export const PagesListMainContent: React.FC = observer((props) => { if (!isAnyPageAvailable || pageIds?.length === 0) { if (!isAnyPageAvailable) { return ( - { - handleCreatePage(); - captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON }); + { + handleCreatePage(); + captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON }); + }, + variant: "primary", + disabled: !canPerformEmptyStateActions || isCreatingPage, }, - disabled: !canPerformEmptyStateActions || isCreatingPage, - }} + ]} /> ); } if (pageType === "public") return ( - { - handleCreatePage(); - captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON }); + { + handleCreatePage(); + captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON }); + }, + variant: "primary", + disabled: !canPerformEmptyStateActions || isCreatingPage, }, - disabled: !canPerformEmptyStateActions || isCreatingPage, - }} + ]} /> ); if (pageType === "private") return ( - { - handleCreatePage(); - captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON }); + { + handleCreatePage(); + captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON }); + }, + variant: "primary", + disabled: !canPerformEmptyStateActions || isCreatingPage, }, - disabled: !canPerformEmptyStateActions || isCreatingPage, - }} + ]} /> ); if (pageType === "archived") return ( - ); } // if no pages match the filter criteria if (filteredPageIds?.length === 0) return ( -
-
- 0 ? resolvedNameFilterImage : resolvedFiltersImage} - className="h-36 sm:h-48 w-36 sm:w-48 mx-auto" - alt="No matching modules" - /> -
No matching pages
-

- {filters.searchQuery.length > 0 - ? "Remove the search criteria to see all pages" - : "Remove the filters to see all pages"} -

-
-
+ ); return
{children}
; diff --git a/apps/web/core/components/profile/overview/activity.tsx b/apps/web/core/components/profile/overview/activity.tsx index f52255400..2af5e2079 100644 --- a/apps/web/core/components/profile/overview/activity.tsx +++ b/apps/web/core/components/profile/overview/activity.tsx @@ -5,6 +5,7 @@ import { useParams } from "next/navigation"; import useSWR from "swr"; // ui import { useTranslation } from "@plane/i18n"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; import { Loader, Card } from "@plane/ui"; import { calculateTimeAgo, getFileURL } from "@plane/utils"; // components @@ -83,11 +84,7 @@ export const ProfileActivity = observer(() => { ))}
) : ( - + ) ) : ( diff --git a/apps/web/core/components/profile/overview/priority-distribution.tsx b/apps/web/core/components/profile/overview/priority-distribution.tsx index d97a68732..98f79a518 100644 --- a/apps/web/core/components/profile/overview/priority-distribution.tsx +++ b/apps/web/core/components/profile/overview/priority-distribution.tsx @@ -3,13 +3,10 @@ // plane imports import { useTranslation } from "@plane/i18n"; import { BarChart } from "@plane/propel/charts/bar-chart"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; import type { IUserProfileData } from "@plane/types"; import { Loader, Card } from "@plane/ui"; import { capitalizeFirstLetter } from "@plane/utils"; -// components -import { ProfileEmptyState } from "@/components/ui/profile-empty-state"; -// assets -import emptyBarGraph from "@/public/empty-state/empty_bar_graph.svg"; type Props = { userProfile: IUserProfileData | undefined; @@ -62,13 +59,11 @@ export const ProfilePriorityDistribution: React.FC = ({ userProfile }) => barSize={20} /> ) : ( -
- -
+ )} ) : ( diff --git a/apps/web/core/components/profile/overview/state-distribution.tsx b/apps/web/core/components/profile/overview/state-distribution.tsx index 9b7c15f42..0b4024f21 100644 --- a/apps/web/core/components/profile/overview/state-distribution.tsx +++ b/apps/web/core/components/profile/overview/state-distribution.tsx @@ -2,13 +2,10 @@ import { STATE_GROUPS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { PieChart } from "@plane/propel/charts/pie-chart"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; import type { IUserProfileData, IUserStateDistribution } from "@plane/types"; import { Card } from "@plane/ui"; import { capitalizeFirstLetter } from "@plane/utils"; -// components -import { ProfileEmptyState } from "@/components/ui/profile-empty-state"; -// assets -import stateGraph from "@/public/empty-state/state_graph.svg"; type Props = { stateDistribution: IUserStateDistribution[]; @@ -74,10 +71,10 @@ export const ProfileStateDistribution: React.FC = ({ stateDistribution, u
) : ( - )} diff --git a/apps/web/core/components/project/card-list.tsx b/apps/web/core/components/project/card-list.tsx index ba8f7a6bb..05a74028b 100644 --- a/apps/web/core/components/project/card-list.tsx +++ b/apps/web/core/components/project/card-list.tsx @@ -1,12 +1,11 @@ import { observer } from "mobx-react"; -import Image from "next/image"; // plane imports import { EUserPermissionsLevel, EUserPermissions, PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { ContentWrapper } from "@plane/ui"; // components -import { ComicBoxButton } from "@/components/empty-state/comic-box-button"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; +import { calculateTotalFilters } from "@plane/utils"; import { ProjectsLoader } from "@/components/ui/loader/projects-loader"; import { captureClick } from "@/helpers/event-tracker.helper"; // hooks @@ -14,7 +13,6 @@ import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useProject } from "@/hooks/store/use-project"; import { useProjectFilter } from "@/hooks/store/use-project-filter"; import { useUserPermissions } from "@/hooks/store/user"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // local imports import { ProjectCard } from "./card"; @@ -36,20 +34,9 @@ export const ProjectCardList = observer((props: TProjectCardListProps) => { filteredProjectIds: storeFilteredProjectIds, getProjectById, } = useProject(); - const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter(); + const { currentWorkspaceDisplayFilters, currentWorkspaceFilters } = useProjectFilter(); const { allowPermissions } = useUserPermissions(); - // helper hooks - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" }); - const resolvedFiltersImage = useResolvedAssetPath({ - basePath: "/empty-state/project/all-filters", - extension: "svg", - }); - const resolvedNameFilterImage = useResolvedAssetPath({ - basePath: "/empty-state/project/name-filter", - extension: "svg", - }); - // derived values const workspaceProjectIds = totalProjectIdsProps ?? storeWorkspaceProjectIds; const filteredProjectIds = filteredProjectIdsProps ?? storeFilteredProjectIds; @@ -65,42 +52,48 @@ export const ProjectCardList = observer((props: TProjectCardListProps) => { if (workspaceProjectIds?.length === 0 && !currentWorkspaceDisplayFilters?.archived_projects) return ( - { + assetKey="project" + assetClassName="size-40" + actions={[ + { + label: t("workspace_projects.empty_state.general.primary_button.text"), + onClick: () => { toggleCreateProjectModal(true); captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON }); - }} - disabled={!canPerformEmptyStateActions} - /> - } + }, + disabled: !canPerformEmptyStateActions, + variant: "primary", + }, + ]} /> ); if (filteredProjectIds.length === 0) return ( -
-
- No matching projects -
{t("workspace_projects.empty_state.filter.title")}
-

- {searchQuery.trim() === "" - ? t("workspace_projects.empty_state.filter.description") - : t("workspace_projects.empty_state.search.description")} -

-
-
+ ); return ( diff --git a/apps/web/core/components/views/views-list.tsx b/apps/web/core/components/views/views-list.tsx index 33d19cbe0..e078131b3 100644 --- a/apps/web/core/components/views/views-list.tsx +++ b/apps/web/core/components/views/views-list.tsx @@ -1,21 +1,17 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // plane imports -import { EUserPermissionsLevel, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants"; +import { EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { EUserProjectRoles } from "@plane/types"; // components import { ListLayout } from "@/components/core/list"; -import { ComicBoxButton } from "@/components/empty-state/comic-box-button"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; -import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root"; import { ViewListLoader } from "@/components/ui/loader/view-list-loader"; // hooks -import { captureClick } from "@/helpers/event-tracker.helper"; import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useProjectView } from "@/hooks/store/use-project-view"; import { useUserPermissions } from "@/hooks/store/user"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // local imports import { ProjectViewListItem } from "./view-list-item"; @@ -34,24 +30,16 @@ export const ProjectViewsList = observer(() => { [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER, EUserProjectRoles.GUEST], EUserPermissionsLevel.PROJECT ); - const generalViewResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/onboarding/views", - }); - const filteredViewResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/search/views", - }); if (loader || !projectViews || !filteredProjectViews) return ; if (filteredProjectViews.length === 0 && projectViews.length > 0) { return ( -
- -
+ ); } @@ -68,22 +56,18 @@ export const ProjectViewsList = observer(() => { ) : ( - { - toggleCreateViewModal(true); - captureClick({ elementName: PROJECT_VIEW_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON }); - }} - disabled={!canPerformEmptyStateActions} - /> - } + toggleCreateViewModal(true), + disabled: !canPerformEmptyStateActions, + variant: "primary", + }, + ]} /> )} diff --git a/apps/web/core/components/workspace-notifications/root.tsx b/apps/web/core/components/workspace-notifications/root.tsx index 108bcb66c..406f8beae 100644 --- a/apps/web/core/components/workspace-notifications/root.tsx +++ b/apps/web/core/components/workspace-notifications/root.tsx @@ -6,10 +6,10 @@ import useSWR from "swr"; // plane imports import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; import { cn } from "@plane/utils"; // components import { LogoSpinner } from "@/components/common/logo-spinner"; -import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root"; // hooks import { useWorkspaceNotifications } from "@/hooks/store/notifications"; import { useWorkspace } from "@/hooks/store/use-workspace"; @@ -87,8 +87,8 @@ export const NotificationsRoot = observer(({ workspaceSlug }: NotificationsRootP return (
{!currentSelectedNotificationId ? ( -
- +
+
) : ( <> diff --git a/apps/web/core/components/workspace-notifications/sidebar/empty-state.tsx b/apps/web/core/components/workspace-notifications/sidebar/empty-state.tsx index 7d61fcc6b..0c81aed47 100644 --- a/apps/web/core/components/workspace-notifications/sidebar/empty-state.tsx +++ b/apps/web/core/components/workspace-notifications/sidebar/empty-state.tsx @@ -4,33 +4,29 @@ import type { FC } from "react"; import { observer } from "mobx-react"; // plane imports import { ENotificationTab } from "@plane/constants"; -// components import { useTranslation } from "@plane/i18n"; -import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root"; -// constants -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; -export const NotificationEmptyState: FC = observer(() => { +type TNotificationEmptyStateProps = { + currentNotificationTab: ENotificationTab; +}; + +export const NotificationEmptyState: FC = observer(({ currentNotificationTab }) => { // plane imports const { t } = useTranslation(); - // derived values - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/notification" }); return ( <> - {ENotificationTab.ALL ? ( - - ) : ( - - )} + ); }); diff --git a/apps/web/core/components/workspace-notifications/sidebar/root.tsx b/apps/web/core/components/workspace-notifications/sidebar/root.tsx index 39478550c..080f5afe7 100644 --- a/apps/web/core/components/workspace-notifications/sidebar/root.tsx +++ b/apps/web/core/components/workspace-notifications/sidebar/root.tsx @@ -107,7 +107,7 @@ export const NotificationsSidebarRoot: FC = observer(() => { ) : (
- +
)} diff --git a/apps/web/core/layouts/auth-layout/project-wrapper.tsx b/apps/web/core/layouts/auth-layout/project-wrapper.tsx index 77d305065..a341f202d 100644 --- a/apps/web/core/layouts/auth-layout/project-wrapper.tsx +++ b/apps/web/core/layouts/auth-layout/project-wrapper.tsx @@ -7,12 +7,11 @@ import useSWR from "swr"; // plane imports import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { EProjectNetwork } from "@plane/types"; // components import { JoinProject } from "@/components/auth-screens/project/join-project"; import { LogoSpinner } from "@/components/common/logo-spinner"; -import { ComicBoxButton } from "@/components/empty-state/comic-box-button"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { ETimeLineTypeType } from "@/components/gantt-chart/contexts"; import { captureClick } from "@/helpers/event-tracker.helper"; // hooks @@ -26,7 +25,6 @@ import { useProject } from "@/hooks/store/use-project"; import { useProjectState } from "@/hooks/store/use-project-state"; import { useProjectView } from "@/hooks/store/use-project-view"; import { useUserPermissions } from "@/hooks/store/user"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { useTimeLineChart } from "@/hooks/use-timeline-chart"; // local import { persistence } from "@/local-db/storage.sqlite"; @@ -58,9 +56,6 @@ export const ProjectAuthWrapper: FC = observer((props) => { const { fetchProjectLabels } = useLabel(); const { getProjectEstimates } = useProjectEstimates(); - // helper hooks - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" }); - // derived values const projectExists = projectId ? getProjectById(projectId.toString()) : null; const projectMemberInfo = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId); @@ -184,22 +179,22 @@ export const ProjectAuthWrapper: FC = observer((props) => { if (loader === "loaded" && projectId && !!hasPermissionToCurrentProject === false) return (
- { + assetKey="project" + assetClassName="size-40" + actions={[ + { + label: t("workspace_projects.empty_state.general.primary_button.text"), + onClick: () => { toggleCreateProjectModal(true); captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON }); - }} - disabled={!canPerformEmptyStateActions} - /> - } + }, + disabled: !canPerformEmptyStateActions, + variant: "primary", + }, + ]} />
); diff --git a/packages/i18n/src/constants/language.ts b/packages/i18n/src/constants/language.ts index 4dc0408bd..efcc9aada 100644 --- a/packages/i18n/src/constants/language.ts +++ b/packages/i18n/src/constants/language.ts @@ -32,6 +32,7 @@ export enum ETranslationFiles { TRANSLATIONS = "translations", ACCESSIBILITY = "accessibility", EDITOR = "editor", + EMPTY_STATE = "emptyState", } export const LANGUAGE_STORAGE_KEY = "userLanguage"; diff --git a/packages/i18n/src/locales/cs/empty-state.ts b/packages/i18n/src/locales/cs/empty-state.ts new file mode 100644 index 000000000..3c1641a0f --- /dev/null +++ b/packages/i18n/src/locales/cs/empty-state.ts @@ -0,0 +1,181 @@ +export default { + common: { + progress: { + title: "Zatím nejsou k dispozici žádné metriky pokroku.", + description: "Začněte nastavovat hodnoty vlastností v pracovních položkách, abyste zde viděli metriky pokroku.", + }, + updates: { + title: "Zatím žádné aktualizace.", + description: "Jakmile členové projektu přidají aktualizace, zobrazí se zde", + }, + search: { + title: "Žádné odpovídající výsledky.", + description: "Nebyly nalezeny žádné výsledky. Zkuste upravit vyhledávací výrazy.", + }, + not_found: { + title: "Jejda! Něco se zdá být v nepořádku", + description: "Momentálně se nám nedaří načíst váš účet plane. Může se jednat o chybu sítě.", + cta_primary: "Zkuste znovu načíst", + }, + server_error: { + title: "Chyba serveru", + description: "Nemůžeme se připojit a načíst data z našeho serveru. Nebojte se, pracujeme na tom.", + cta_primary: "Zkuste znovu načíst", + }, + }, + project: { + work_items: { + title: "Začněte s vaší první pracovní položkou.", + description: + "Pracovní položky jsou stavebními kameny vašeho projektu — přiřazujte vlastníky, nastavujte priority a snadno sledujte pokrok.", + cta_primary: "Vytvořte svou první pracovní položku", + }, + cycles: { + title: "Seskupujte a časově omezte svou práci v cyklech.", + description: + "Rozdělte práci do časově omezených bloků, pracujte zpětně od termínu projektu pro nastavení dat a dosahujte hmatatelného pokroku jako tým.", + cta_primary: "Nastavte svůj první cyklus", + }, + cycle_work_items: { + title: "V tomto cyklu nejsou žádné pracovní položky k zobrazení", + description: + "Vytvořte pracovní položky pro zahájení sledování pokroku vašeho týmu v tomto cyklu a dosažení vašich cílů včas.", + cta_primary: "Vytvořit pracovní položku", + cta_secondary: "Přidat existující pracovní položku", + }, + modules: { + title: "Namapujte cíle vašeho projektu na moduly a snadno sledujte.", + description: + "Moduly se skládají z propojených pracovních položek. Pomáhají sledovat pokrok prostřednictvím fází projektu, z nichž každá má specifické termíny a analytiku, která ukazuje, jak blízko jste dosažení těchto fází.", + cta_primary: "Nastavte svůj první modul", + }, + module_work_items: { + title: "V tomto modulu nejsou žádné pracovní položky k zobrazení", + description: "Vytvořte pracovní položky pro zahájení sledování tohoto modulu.", + cta_primary: "Vytvořit pracovní položku", + cta_secondary: "Přidat existující pracovní položku", + }, + views: { + title: "Uložte vlastní pohledy pro váš projekt", + description: + "Pohledy jsou uložené filtry, které vám pomáhají rychle přistupovat k informacím, které používáte nejčastěji. Spolupracujte bez námahy, zatímco spolupracovníci sdílejí a přizpůsobují pohledy svým specifickým potřebám.", + cta_primary: "Vytvořit pohled", + }, + no_work_items_in_project: { + title: "V projektu zatím nejsou žádné pracovní položky", + description: + "Přidejte pracovní položky do svého projektu a rozdělte svou práci na sledovatelné části pomocí pohledů.", + cta_primary: "Přidat pracovní položku", + }, + work_item_filter: { + title: "Nebyly nalezeny žádné pracovní položky", + description: "Váš aktuální filtr nevrátil žádné výsledky. Zkuste změnit filtry.", + cta_primary: "Přidat pracovní položku", + }, + pages: { + title: "Dokumentujte vše — od poznámek po PRD", + description: + "Stránky vám umožňují zachytit a organizovat informace na jednom místě. Pište poznámky ze schůzek, projektovou dokumentaci a PRD, vkládejte pracovní položky a strukturujte je pomocí připravených komponent.", + cta_primary: "Vytvořte svou první stránku", + }, + archive_pages: { + title: "Zatím žádné archivované stránky", + description: "Archivujte stránky, které nejsou na vašem radaru. Přistupte k nim zde, když budete potřebovat.", + }, + intake_sidebar: { + title: "Zaznamenejte příchozí požadavky", + description: + "Odesílejte nové požadavky k přezkoumání, stanovení priorit a sledování v rámci pracovního postupu vašeho projektu.", + cta_primary: "Vytvořit příchozí požadavek", + }, + intake_main: { + title: "Vyberte příchozí pracovní položku pro zobrazení jejích podrobností", + }, + }, + workspace: { + archive_work_items: { + title: "Zatím žádné archivované pracovní položky", + description: + "Ručně nebo pomocí automatizace můžete archivovat dokončené nebo zrušené pracovní položky. Najdete je zde, jakmile budou archivovány.", + cta_primary: "Nastavit automatizaci", + }, + archive_cycles: { + title: "Zatím žádné archivované cykly", + description: "Pro úklid vašeho projektu archivujte dokončené cykly. Najdete je zde, jakmile budou archivovány.", + }, + archive_modules: { + title: "Zatím žádné archivované moduly", + description: + "Pro úklid vašeho projektu archivujte dokončené nebo zrušené moduly. Najdete je zde, jakmile budou archivovány.", + }, + home_widget_quick_links: { + title: "Mějte po ruce důležité odkazy, zdroje nebo dokumenty pro vaši práci", + }, + inbox_sidebar_all: { + title: "Aktualizace pro vaše odebírané pracovní položky se zobrazí zde", + }, + inbox_sidebar_mentions: { + title: "Zmínky o vašich pracovních položkách se zobrazí zde", + }, + your_work_by_priority: { + title: "Zatím není přiřazena žádná pracovní položka", + }, + your_work_by_state: { + title: "Zatím není přiřazena žádná pracovní položka", + }, + views: { + title: "Zatím žádné pohledy", + description: + "Přidejte pracovní položky do svého projektu a používejte pohledy pro snadné filtrování, třídění a sledování pokroku.", + cta_primary: "Přidat pracovní položku", + }, + drafts: { + title: "Napůl napsané pracovní položky", + description: + "Chcete-li to vyzkoušet, začněte přidávat pracovní položku a nechte ji nedokončenou nebo vytvořte svůj první koncept níže. 😉", + cta_primary: "Vytvořit koncept pracovní položky", + }, + projects_archived: { + title: "Žádné archivované projekty", + description: "Vypadá to, že všechny vaše projekty jsou stále aktivní—skvělá práce!", + }, + analytics_projects: { + title: "Vytvořte projekty pro vizualizaci metrik projektu zde.", + }, + analytics_work_items: { + title: + "Vytvořte projekty s pracovními položkami a přiřazenými osobami pro zahájení sledování výkonu, pokroku a dopadu týmu zde.", + }, + analytics_no_cycle: { + title: "Vytvořte cykly pro organizaci práce do časově omezených fází a sledování pokroku napříč sprinty.", + }, + analytics_no_module: { + title: "Vytvořte moduly pro organizaci své práce a sledování pokroku napříč různými fázemi.", + }, + analytics_no_intake: { + title: "Nastavte příjem pro správu příchozích požadavků a sledování, jak jsou přijímány a odmítány", + }, + }, + settings: { + estimates: { + title: "Zatím žádné odhady", + description: "Definujte, jak váš tým měří úsilí, a sledujte to konzistentně napříč všemi pracovními položkami.", + cta_primary: "Přidat systém odhadů", + }, + labels: { + title: "Zatím žádné štítky", + description: "Vytvořte personalizované štítky pro efektivní kategorizaci a správu vašich pracovních položek.", + cta_primary: "Vytvořte svůj první štítek", + }, + exports: { + title: "Zatím žádné exporty", + description: "Momentálně nemáte žádné záznamy exportu. Jakmile exportujete data, všechny záznamy se zobrazí zde.", + }, + tokens: { + title: "Zatím žádný osobní token", + description: + "Generujte bezpečné API tokeny pro připojení vašeho pracovního prostoru s externími systémy a aplikacemi.", + cta_primary: "Přidat API token", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/de/empty-state.ts b/packages/i18n/src/locales/de/empty-state.ts new file mode 100644 index 000000000..0c7a27eb7 --- /dev/null +++ b/packages/i18n/src/locales/de/empty-state.ts @@ -0,0 +1,190 @@ +export default { + common: { + progress: { + title: "Es gibt noch keine Fortschrittsmetriken anzuzeigen.", + description: + "Beginnen Sie mit dem Festlegen von Eigenschaftswerten in Arbeitselementen, um hier Fortschrittsmetriken zu sehen.", + }, + updates: { + title: "Noch keine Updates.", + description: "Sobald Projektmitglieder Updates hinzufügen, werden sie hier angezeigt", + }, + search: { + title: "Keine passenden Ergebnisse.", + description: "Keine Ergebnisse gefunden. Versuchen Sie, Ihre Suchbegriffe anzupassen.", + }, + not_found: { + title: "Hoppla! Etwas scheint nicht zu stimmen", + description: "Wir können Ihr Plane-Konto derzeit nicht abrufen. Dies könnte ein Netzwerkfehler sein.", + cta_primary: "Versuchen Sie neu zu laden", + }, + server_error: { + title: "Serverfehler", + description: + "Wir können keine Verbindung herstellen und Daten von unserem Server abrufen. Keine Sorge, wir arbeiten daran.", + cta_primary: "Versuchen Sie neu zu laden", + }, + }, + project: { + work_items: { + title: "Beginnen Sie mit Ihrem ersten Arbeitselement.", + description: + "Arbeitselemente sind die Bausteine Ihres Projekts — weisen Sie Eigentümer zu, setzen Sie Prioritäten und verfolgen Sie den Fortschritt einfach.", + cta_primary: "Erstellen Sie Ihr erstes Arbeitselement", + }, + cycles: { + title: "Gruppieren und zeitlich begrenzen Sie Ihre Arbeit in Zyklen.", + description: + "Teilen Sie die Arbeit in zeitlich begrenzte Blöcke auf, arbeiten Sie rückwärts von Ihrer Projektfrist, um Termine festzulegen, und machen Sie greifbare Fortschritte als Team.", + cta_primary: "Legen Sie Ihren ersten Zyklus fest", + }, + cycle_work_items: { + title: "Keine Arbeitselemente in diesem Zyklus anzuzeigen", + description: + "Erstellen Sie Arbeitselemente, um den Fortschritt Ihres Teams in diesem Zyklus zu überwachen und Ihre Ziele rechtzeitig zu erreichen.", + cta_primary: "Arbeitselement erstellen", + cta_secondary: "Bestehendes Arbeitselement hinzufügen", + }, + modules: { + title: "Ordnen Sie Ihre Projektziele Modulen zu und verfolgen Sie sie einfach.", + description: + "Module bestehen aus miteinander verbundenen Arbeitselementen. Sie helfen bei der Überwachung des Fortschritts durch Projektphasen, jede mit spezifischen Fristen und Analysen, um anzuzeigen, wie nahe Sie dem Erreichen dieser Phasen sind.", + cta_primary: "Legen Sie Ihr erstes Modul fest", + }, + module_work_items: { + title: "Keine Arbeitselemente in diesem Modul anzuzeigen", + description: "Erstellen Sie Arbeitselemente, um dieses Modul zu überwachen.", + cta_primary: "Arbeitselement erstellen", + cta_secondary: "Bestehendes Arbeitselement hinzufügen", + }, + views: { + title: "Speichern Sie benutzerdefinierte Ansichten für Ihr Projekt", + description: + "Ansichten sind gespeicherte Filter, die Ihnen helfen, schnell auf die Informationen zuzugreifen, die Sie am häufigsten verwenden. Arbeiten Sie mühelos zusammen, während Teammitglieder Ansichten teilen und an ihre spezifischen Bedürfnisse anpassen.", + cta_primary: "Ansicht erstellen", + }, + no_work_items_in_project: { + title: "Noch keine Arbeitselemente im Projekt", + description: + "Fügen Sie Arbeitselemente zu Ihrem Projekt hinzu und unterteilen Sie Ihre Arbeit in nachverfolgbare Teile mit Ansichten.", + cta_primary: "Arbeitselement hinzufügen", + }, + work_item_filter: { + title: "Keine Arbeitselemente gefunden", + description: "Ihr aktueller Filter hat keine Ergebnisse zurückgegeben. Versuchen Sie, die Filter zu ändern.", + cta_primary: "Arbeitselement hinzufügen", + }, + pages: { + title: "Dokumentieren Sie alles — von Notizen bis PRDs", + description: + "Seiten ermöglichen es Ihnen, Informationen an einem Ort zu erfassen und zu organisieren. Schreiben Sie Besprechungsnotizen, Projektdokumentationen und PRDs, betten Sie Arbeitselemente ein und strukturieren Sie sie mit gebrauchsfertigen Komponenten.", + cta_primary: "Erstellen Sie Ihre erste Seite", + }, + archive_pages: { + title: "Noch keine archivierten Seiten", + description: "Archivieren Sie Seiten, die nicht auf Ihrem Radar sind. Greifen Sie bei Bedarf hier darauf zu.", + }, + intake_sidebar: { + title: "Intake-Anfragen protokollieren", + description: + "Senden Sie neue Anfragen zur Überprüfung, Priorisierung und Verfolgung innerhalb Ihres Projekt-Workflows.", + cta_primary: "Intake-Anfrage erstellen", + }, + intake_main: { + title: "Wählen Sie ein Intake-Arbeitselement aus, um seine Details anzuzeigen", + }, + }, + workspace: { + archive_work_items: { + title: "Noch keine archivierten Arbeitselemente", + description: + "Manuell oder durch Automatisierung können Sie abgeschlossene oder stornierte Arbeitselemente archivieren. Finden Sie sie hier, sobald sie archiviert sind.", + cta_primary: "Automatisierung einrichten", + }, + archive_cycles: { + title: "Noch keine archivierten Zyklen", + description: + "Um Ihr Projekt aufzuräumen, archivieren Sie abgeschlossene Zyklen. Finden Sie sie hier, sobald sie archiviert sind.", + }, + archive_modules: { + title: "Noch keine archivierten Module", + description: + "Um Ihr Projekt aufzuräumen, archivieren Sie abgeschlossene oder stornierte Module. Finden Sie sie hier, sobald sie archiviert sind.", + }, + home_widget_quick_links: { + title: "Halten Sie wichtige Referenzen, Ressourcen oder Dokumente für Ihre Arbeit griffbereit", + }, + inbox_sidebar_all: { + title: "Updates für Ihre abonnierten Arbeitselemente werden hier angezeigt", + }, + inbox_sidebar_mentions: { + title: "Erwähnungen für Ihre Arbeitselemente werden hier angezeigt", + }, + your_work_by_priority: { + title: "Noch kein Arbeitselement zugewiesen", + }, + your_work_by_state: { + title: "Noch kein Arbeitselement zugewiesen", + }, + views: { + title: "Noch keine Ansichten", + description: + "Fügen Sie Arbeitselemente zu Ihrem Projekt hinzu und verwenden Sie Ansichten, um mühelos zu filtern, zu sortieren und den Fortschritt zu überwachen.", + cta_primary: "Arbeitselement hinzufügen", + }, + drafts: { + title: "Halb geschriebene Arbeitselemente", + description: + "Um dies auszuprobieren, beginnen Sie ein Arbeitselement hinzuzufügen und lassen Sie es auf halbem Weg liegen oder erstellen Sie unten Ihren ersten Entwurf. 😉", + cta_primary: "Entwurf-Arbeitselement erstellen", + }, + projects_archived: { + title: "Keine Projekte archiviert", + description: "Es sieht so aus, als wären alle Ihre Projekte noch aktiv—gute Arbeit!", + }, + analytics_projects: { + title: "Erstellen Sie Projekte, um hier Projektmetriken zu visualisieren.", + }, + analytics_work_items: { + title: + "Erstellen Sie Projekte mit Arbeitselementen und Zugewiesenen, um hier Leistung, Fortschritt und Teameinfluss zu verfolgen.", + }, + analytics_no_cycle: { + title: + "Erstellen Sie Zyklen, um Arbeit in zeitlich begrenzte Phasen zu organisieren und Fortschritte über Sprints hinweg zu verfolgen.", + }, + analytics_no_module: { + title: + "Erstellen Sie Module, um Ihre Arbeit zu organisieren und Fortschritte über verschiedene Phasen hinweg zu verfolgen.", + }, + analytics_no_intake: { + title: + "Richten Sie Intake ein, um eingehende Anfragen zu verwalten und zu verfolgen, wie sie akzeptiert und abgelehnt werden", + }, + }, + settings: { + estimates: { + title: "Noch keine Schätzungen", + description: + "Definieren Sie, wie Ihr Team den Aufwand misst, und verfolgen Sie ihn konsistent über alle Arbeitselemente hinweg.", + cta_primary: "Schätzsystem hinzufügen", + }, + labels: { + title: "Noch keine Labels", + description: + "Erstellen Sie personalisierte Labels, um Ihre Arbeitselemente effektiv zu kategorisieren und zu verwalten.", + cta_primary: "Erstellen Sie Ihr erstes Label", + }, + exports: { + title: "Noch keine Exporte", + description: + "Sie haben derzeit keine Exportaufzeichnungen. Sobald Sie Daten exportieren, werden alle Aufzeichnungen hier angezeigt.", + }, + tokens: { + title: "Noch kein persönliches Token", + description: + "Generieren Sie sichere API-Tokens, um Ihren Workspace mit externen Systemen und Anwendungen zu verbinden.", + cta_primary: "API-Token hinzufügen", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/en/empty-state.ts b/packages/i18n/src/locales/en/empty-state.ts new file mode 100644 index 000000000..ef8265b12 --- /dev/null +++ b/packages/i18n/src/locales/en/empty-state.ts @@ -0,0 +1,176 @@ +export default { + common: { + progress: { + title: "There're no progress metrics to show yet.", + description: "Start setting property values in work items to see progress metrics here.", + }, + updates: { + title: "No updates yet.", + description: "Once project members add updates it will appear here", + }, + search: { + title: "No matching results.", + description: "No results found. Try adjusting your search terms.", + }, + not_found: { + title: "Oops! Something seems wrong", + description: "We are unable to fetch your plane account currently. This might be a network error.", + cta_primary: "Try reloading", + }, + server_error: { + title: "Server error", + description: "We are unable to connect and fetch data from our server. Don't worry, we are working on it.", + cta_primary: "Try reloading", + }, + }, + project: { + work_items: { + title: "Start with your first work item.", + description: + "Work items are the building blocks of your project — assign owners, set priorities, and track progress easily.", + cta_primary: "Create your first work item", + }, + cycles: { + title: "Group and timebox your work in Cycles.", + description: + "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.", + cta_primary: "Set your first cycle", + }, + cycle_work_items: { + title: "No work items to show in this cycle", + description: + "Create work items to begin monitoring your team's progress this cycle and achieve your goals on time.", + cta_primary: "Create work item", + cta_secondary: "Add existing work item", + }, + modules: { + title: "Map your project goals to Modules and track easily.", + description: + "Modules are made up of interconnected work items. They assist in monitoring progress through project phases, each with specific deadlines and analytics to indicate how close you are to achieving those phases.", + cta_primary: "Set your first module", + }, + module_work_items: { + title: "No work items to show in this Module", + description: "Create work items to begin monitoring this module.", + cta_primary: "Create work item", + cta_secondary: "Add existing work item", + }, + views: { + title: "Save custom views for your project", + description: + "Views are saved filters that help you quickly access the information you use most. Collaborate effortlessly as teammates share and tailor views to their specific needs.", + cta_primary: "Create view", + }, + no_work_items_in_project: { + title: "No work items in the project yet", + description: "Add work items to your project and slice your work into trackable pieces with views.", + cta_primary: "Add work item", + }, + work_item_filter: { + title: "No work items found", + description: "Your current filter didn't return any results. Try changing the filters.", + cta_primary: "Add work item", + }, + pages: { + title: "Document everything — from notes to PRDs", + description: + "Pages let you capture and organize information in one place. Write meeting notes, project documentation, and PRDs, embed work items, and structure them with ready-to-use components.", + cta_primary: "Create your first Page", + }, + archive_pages: { + title: "No archived pages yet", + description: "Archive pages not on your radar. Access them here when needed.", + }, + intake_sidebar: { + title: "Log Intake requests", + description: "Submit new requests to be reviewed, prioritized, and tracked within your project's workflow.", + cta_primary: "Create Intake request", + }, + intake_main: { + title: "Select an Intake work item to view its details", + }, + }, + workspace: { + archive_work_items: { + title: "No archived work items yet", + description: + "Manually or through automation, you can archive work items that are completed or cancelled. Find them here once archived.", + cta_primary: "Set automation", + }, + archive_cycles: { + title: "No archived cycles yet", + description: "To tidy up your project, archive completed cycles. Find them here once archived.", + }, + archive_modules: { + title: "No archived Modules yet", + description: "To tidy up your project, archive completed or cancelled modules. Find them here once archived.", + }, + home_widget_quick_links: { + title: "Keep important references, resources, or docs handy for your work", + }, + inbox_sidebar_all: { + title: "Updates for your subscribed work items will appear here", + }, + inbox_sidebar_mentions: { + title: "Mentions for your work items will appear here", + }, + your_work_by_priority: { + title: "No work item assigned yet", + }, + your_work_by_state: { + title: "No work item assigned yet", + }, + views: { + title: "No Views yet", + description: "Add work items to your project and use views to filter, sort, and monitor progress effortlessly.", + cta_primary: "Add work item", + }, + drafts: { + title: "Half-written work items", + description: + "To try this out, start adding a work item and leave it mid-way or create your first draft below. 😉", + cta_primary: "Create draft work item", + }, + projects_archived: { + title: "No projects archived", + description: "Looks like all your projects are still active—great job!", + }, + analytics_projects: { + title: "Create projects to visualize project metrics here.", + }, + analytics_work_items: { + title: + "Create projects with work items and assignees to start tracking performance, progress, and team impact here.", + }, + analytics_no_cycle: { + title: "Create cycles to organise work into time-bound phases and track progress across sprints.", + }, + analytics_no_module: { + title: "Create modules to organize your work and track progress across different stages.", + }, + analytics_no_intake: { + title: "Set up intake to manage incoming requests and track how they're accepted and rejected", + }, + }, + settings: { + estimates: { + title: "No estimates yet", + description: "Define how your team measures effort and track it consistently across all work items.", + cta_primary: "Add estimate system", + }, + labels: { + title: "No labels yet", + description: "Create personalized labels to effectively categorize and manage your work items.", + cta_primary: "Create your first label", + }, + exports: { + title: "No exports yet", + description: "You don't have any export records right now. Once you export data, all records will appear here.", + }, + tokens: { + title: "No Personal token yet", + description: "Generate secure API tokens to connect your workspace with external systems and applications.", + cta_primary: "Add API token", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/es/empty-state.ts b/packages/i18n/src/locales/es/empty-state.ts new file mode 100644 index 000000000..3ee97c54f --- /dev/null +++ b/packages/i18n/src/locales/es/empty-state.ts @@ -0,0 +1,185 @@ +export default { + common: { + progress: { + title: "Todavía no hay métricas de progreso para mostrar.", + description: + "Comienza a establecer valores de propiedades en los elementos de trabajo para ver las métricas de progreso aquí.", + }, + updates: { + title: "Aún no hay actualizaciones.", + description: "Una vez que los miembros del proyecto agreguen actualizaciones, aparecerán aquí", + }, + search: { + title: "No se encontraron resultados coincidentes.", + description: "No se encontraron resultados. Intenta ajustar tus términos de búsqueda.", + }, + not_found: { + title: "¡Ups! Algo parece estar mal", + description: "No podemos obtener tu cuenta de Plane actualmente. Esto podría ser un error de red.", + cta_primary: "Intentar recargar", + }, + server_error: { + title: "Error del servidor", + description: + "No podemos conectarnos y obtener datos de nuestro servidor. No te preocupes, estamos trabajando en ello.", + cta_primary: "Intentar recargar", + }, + }, + project: { + work_items: { + title: "Comienza con tu primer elemento de trabajo.", + description: + "Los elementos de trabajo son los bloques de construcción de tu proyecto — asigna responsables, establece prioridades y realiza un seguimiento del progreso fácilmente.", + cta_primary: "Crea tu primer elemento de trabajo", + }, + cycles: { + title: "Agrupa y delimita tu trabajo en Ciclos.", + description: + "Divide el trabajo en bloques con tiempo definido, trabaja hacia atrás desde la fecha límite de tu proyecto para establecer fechas y haz un progreso tangible como equipo.", + cta_primary: "Establece tu primer ciclo", + }, + cycle_work_items: { + title: "No hay elementos de trabajo para mostrar en este ciclo", + description: + "Crea elementos de trabajo para comenzar a monitorear el progreso de tu equipo en este ciclo y alcanzar tus objetivos a tiempo.", + cta_primary: "Crear elemento de trabajo", + cta_secondary: "Agregar elemento de trabajo existente", + }, + modules: { + title: "Asigna los objetivos de tu proyecto a Módulos y rastrea fácilmente.", + description: + "Los módulos están compuestos de elementos de trabajo interconectados. Ayudan a monitorear el progreso a través de las fases del proyecto, cada una con fechas límite específicas y análisis para indicar qué tan cerca estás de alcanzar esas fases.", + cta_primary: "Establece tu primer módulo", + }, + module_work_items: { + title: "No hay elementos de trabajo para mostrar en este Módulo", + description: "Crea elementos de trabajo para comenzar a monitorear este módulo.", + cta_primary: "Crear elemento de trabajo", + cta_secondary: "Agregar elemento de trabajo existente", + }, + views: { + title: "Guarda vistas personalizadas para tu proyecto", + description: + "Las vistas son filtros guardados que te ayudan a acceder rápidamente a la información que más usas. Colabora sin esfuerzo mientras los compañeros de equipo comparten y adaptan las vistas a sus necesidades específicas.", + cta_primary: "Crear vista", + }, + no_work_items_in_project: { + title: "Aún no hay elementos de trabajo en el proyecto", + description: "Agrega elementos de trabajo a tu proyecto y divide tu trabajo en piezas rastreables con vistas.", + cta_primary: "Agregar elemento de trabajo", + }, + work_item_filter: { + title: "No se encontraron elementos de trabajo", + description: "Tu filtro actual no devolvió ningún resultado. Intenta cambiar los filtros.", + cta_primary: "Agregar elemento de trabajo", + }, + pages: { + title: "Documenta todo — desde notas hasta PRDs", + description: + "Las páginas te permiten capturar y organizar información en un solo lugar. Escribe notas de reuniones, documentación de proyectos y PRDs, incrusta elementos de trabajo y estructúralos con componentes listos para usar.", + cta_primary: "Crea tu primera Página", + }, + archive_pages: { + title: "Aún no hay páginas archivadas", + description: "Archiva las páginas que no están en tu radar. Accede a ellas aquí cuando las necesites.", + }, + intake_sidebar: { + title: "Registra solicitudes de Entrada", + description: + "Envía nuevas solicitudes para ser revisadas, priorizadas y rastreadas dentro del flujo de trabajo de tu proyecto.", + cta_primary: "Crear solicitud de Entrada", + }, + intake_main: { + title: "Selecciona un elemento de trabajo de Entrada para ver sus detalles", + }, + }, + workspace: { + archive_work_items: { + title: "Aún no hay elementos de trabajo archivados", + description: + "Manualmente o mediante automatización, puedes archivar elementos de trabajo que estén completados o cancelados. Encuéntralos aquí una vez archivados.", + cta_primary: "Configurar automatización", + }, + archive_cycles: { + title: "Aún no hay ciclos archivados", + description: "Para ordenar tu proyecto, archiva los ciclos completados. Encuéntralos aquí una vez archivados.", + }, + archive_modules: { + title: "Aún no hay Módulos archivados", + description: + "Para ordenar tu proyecto, archiva los módulos completados o cancelados. Encuéntralos aquí una vez archivados.", + }, + home_widget_quick_links: { + title: "Mantén a mano referencias importantes, recursos o documentos para tu trabajo", + }, + inbox_sidebar_all: { + title: "Las actualizaciones de tus elementos de trabajo suscritos aparecerán aquí", + }, + inbox_sidebar_mentions: { + title: "Las menciones a tus elementos de trabajo aparecerán aquí", + }, + your_work_by_priority: { + title: "Aún no hay elementos de trabajo asignados", + }, + your_work_by_state: { + title: "Aún no hay elementos de trabajo asignados", + }, + views: { + title: "Aún no hay Vistas", + description: + "Agrega elementos de trabajo a tu proyecto y usa vistas para filtrar, ordenar y monitorear el progreso sin esfuerzo.", + cta_primary: "Agregar elemento de trabajo", + }, + drafts: { + title: "Elementos de trabajo a medio escribir", + description: + "Para probarlo, comienza a agregar un elemento de trabajo y déjalo a medias o crea tu primer borrador a continuación. 😉", + cta_primary: "Crear borrador de elemento de trabajo", + }, + projects_archived: { + title: "No hay proyectos archivados", + description: "Parece que todos tus proyectos siguen activos — ¡buen trabajo!", + }, + analytics_projects: { + title: "Crea proyectos para visualizar las métricas del proyecto aquí.", + }, + analytics_work_items: { + title: + "Crea proyectos con elementos de trabajo y responsables para comenzar a rastrear el rendimiento, progreso e impacto del equipo aquí.", + }, + analytics_no_cycle: { + title: + "Crea ciclos para organizar el trabajo en fases con límite de tiempo y rastrear el progreso en los sprints.", + }, + analytics_no_module: { + title: "Crea módulos para organizar tu trabajo y rastrear el progreso en diferentes etapas.", + }, + analytics_no_intake: { + title: "Configura la entrada para gestionar las solicitudes entrantes y rastrear cómo se aceptan y rechazan", + }, + }, + settings: { + estimates: { + title: "Aún no hay estimaciones", + description: + "Define cómo tu equipo mide el esfuerzo y rastréalo de manera consistente en todos los elementos de trabajo.", + cta_primary: "Agregar sistema de estimación", + }, + labels: { + title: "Aún no hay etiquetas", + description: "Crea etiquetas personalizadas para categorizar y gestionar efectivamente tus elementos de trabajo.", + cta_primary: "Crea tu primera etiqueta", + }, + exports: { + title: "Aún no hay exportaciones", + description: + "No tienes ningún registro de exportación en este momento. Una vez que exportes datos, todos los registros aparecerán aquí.", + }, + tokens: { + title: "Aún no hay tokens Personales", + description: + "Genera tokens API seguros para conectar tu espacio de trabajo con sistemas y aplicaciones externos.", + cta_primary: "Agregar token API", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/fr/empty-state.ts b/packages/i18n/src/locales/fr/empty-state.ts new file mode 100644 index 000000000..cf86b413c --- /dev/null +++ b/packages/i18n/src/locales/fr/empty-state.ts @@ -0,0 +1,188 @@ +export default { + common: { + progress: { + title: "Il n'y a pas encore de métriques de progression à afficher.", + description: + "Commencez à définir des valeurs de propriété dans les éléments de travail pour voir les métriques de progression ici.", + }, + updates: { + title: "Pas encore de mises à jour.", + description: "Une fois que les membres du projet ajoutent des mises à jour, elles apparaîtront ici", + }, + search: { + title: "Aucun résultat correspondant.", + description: "Aucun résultat trouvé. Essayez d'ajuster vos termes de recherche.", + }, + not_found: { + title: "Oups ! Quelque chose semble incorrect", + description: + "Nous ne sommes actuellement pas en mesure de récupérer votre compte plane. Il pourrait s'agir d'une erreur réseau.", + cta_primary: "Essayer de recharger", + }, + server_error: { + title: "Erreur du serveur", + description: + "Nous ne sommes pas en mesure de nous connecter et de récupérer les données de notre serveur. Ne vous inquiétez pas, nous y travaillons.", + cta_primary: "Essayer de recharger", + }, + }, + project: { + work_items: { + title: "Commencez avec votre premier élément de travail.", + description: + "Les éléments de travail sont les éléments constitutifs de votre projet — attribuez des propriétaires, définissez des priorités et suivez facilement les progrès.", + cta_primary: "Créer votre premier élément de travail", + }, + cycles: { + title: "Regroupez et définissez des délais pour votre travail dans les Cycles.", + description: + "Décomposez le travail en morceaux délimités dans le temps, travaillez à rebours à partir de la date limite de votre projet pour définir des dates, et faites des progrès tangibles en équipe.", + cta_primary: "Définir votre premier cycle", + }, + cycle_work_items: { + title: "Aucun élément de travail à afficher dans ce cycle", + description: + "Créez des éléments de travail pour commencer à surveiller la progression de votre équipe ce cycle et atteindre vos objectifs à temps.", + cta_primary: "Créer un élément de travail", + cta_secondary: "Ajouter un élément de travail existant", + }, + modules: { + title: "Associez vos objectifs de projet aux Modules et suivez facilement.", + description: + "Les modules sont composés d'éléments de travail interconnectés. Ils aident à surveiller les progrès à travers les phases du projet, chacune avec des délais spécifiques et des analyses pour indiquer à quel point vous êtes proche de la réalisation de ces phases.", + cta_primary: "Définir votre premier module", + }, + module_work_items: { + title: "Aucun élément de travail à afficher dans ce Module", + description: "Créez des éléments de travail pour commencer à surveiller ce module.", + cta_primary: "Créer un élément de travail", + cta_secondary: "Ajouter un élément de travail existant", + }, + views: { + title: "Enregistrez des vues personnalisées pour votre projet", + description: + "Les vues sont des filtres enregistrés qui vous aident à accéder rapidement aux informations que vous utilisez le plus. Collaborez sans effort pendant que les coéquipiers partagent et adaptent les vues à leurs besoins spécifiques.", + cta_primary: "Créer une vue", + }, + no_work_items_in_project: { + title: "Aucun élément de travail dans le projet pour le moment", + description: + "Ajoutez des éléments de travail à votre projet et découpez votre travail en éléments traçables avec des vues.", + cta_primary: "Ajouter un élément de travail", + }, + work_item_filter: { + title: "Aucun élément de travail trouvé", + description: "Votre filtre actuel n'a renvoyé aucun résultat. Essayez de modifier les filtres.", + cta_primary: "Ajouter un élément de travail", + }, + pages: { + title: "Documentez tout — des notes aux PRD", + description: + "Les pages vous permettent de capturer et d'organiser des informations en un seul endroit. Rédigez des notes de réunion, de la documentation de projet et des PRD, intégrez des éléments de travail et structurez-les avec des composants prêts à l'emploi.", + cta_primary: "Créer votre première Page", + }, + archive_pages: { + title: "Aucune page archivée pour le moment", + description: "Archivez les pages qui ne sont pas sur votre radar. Accédez-y ici si nécessaire.", + }, + intake_sidebar: { + title: "Enregistrer les demandes d'Intake", + description: + "Soumettez de nouvelles demandes à examiner, prioriser et suivre dans le flux de travail de votre projet.", + cta_primary: "Créer une demande d'Intake", + }, + intake_main: { + title: "Sélectionnez un élément de travail Intake pour voir ses détails", + }, + }, + workspace: { + archive_work_items: { + title: "Aucun élément de travail archivé pour le moment", + description: + "Manuellement ou par automatisation, vous pouvez archiver des éléments de travail qui sont terminés ou annulés. Retrouvez-les ici une fois archivés.", + cta_primary: "Définir l'automatisation", + }, + archive_cycles: { + title: "Aucun cycle archivé pour le moment", + description: "Pour organiser votre projet, archivez les cycles terminés. Retrouvez-les ici une fois archivés.", + }, + archive_modules: { + title: "Aucun Module archivé pour le moment", + description: + "Pour organiser votre projet, archivez les modules terminés ou annulés. Retrouvez-les ici une fois archivés.", + }, + home_widget_quick_links: { + title: "Gardez les références, ressources ou documents importants à portée de main pour votre travail", + }, + inbox_sidebar_all: { + title: "Les mises à jour pour vos éléments de travail auxquels vous êtes abonné apparaîtront ici", + }, + inbox_sidebar_mentions: { + title: "Les mentions pour vos éléments de travail apparaîtront ici", + }, + your_work_by_priority: { + title: "Aucun élément de travail attribué pour le moment", + }, + your_work_by_state: { + title: "Aucun élément de travail attribué pour le moment", + }, + views: { + title: "Aucune vue pour le moment", + description: + "Ajoutez des éléments de travail à votre projet et utilisez les vues pour filtrer, trier et surveiller les progrès sans effort.", + cta_primary: "Ajouter un élément de travail", + }, + drafts: { + title: "Éléments de travail à moitié écrits", + description: + "Pour l'essayer, commencez à ajouter un élément de travail et laissez-le à mi-chemin ou créez votre premier brouillon ci-dessous. 😉", + cta_primary: "Créer un brouillon d'élément de travail", + }, + projects_archived: { + title: "Aucun projet archivé", + description: "On dirait que tous vos projets sont toujours actifs—excellent travail !", + }, + analytics_projects: { + title: "Créez des projets pour visualiser les métriques de projet ici.", + }, + analytics_work_items: { + title: + "Créez des projets avec des éléments de travail et des assignés pour commencer à suivre les performances, les progrès et l'impact de l'équipe ici.", + }, + analytics_no_cycle: { + title: + "Créez des cycles pour organiser le travail en phases délimitées dans le temps et suivre les progrès à travers les sprints.", + }, + analytics_no_module: { + title: "Créez des modules pour organiser votre travail et suivre les progrès à travers différentes étapes.", + }, + analytics_no_intake: { + title: "Configurez l'intake pour gérer les demandes entrantes et suivre comment elles sont acceptées et rejetées", + }, + }, + settings: { + estimates: { + title: "Aucune estimation pour le moment", + description: + "Définissez comment votre équipe mesure l'effort et suivez-le de manière cohérente sur tous les éléments de travail.", + cta_primary: "Ajouter un système d'estimation", + }, + labels: { + title: "Aucune étiquette pour le moment", + description: + "Créez des étiquettes personnalisées pour catégoriser et gérer efficacement vos éléments de travail.", + cta_primary: "Créer votre première étiquette", + }, + exports: { + title: "Aucune exportation pour le moment", + description: + "Vous n'avez aucun enregistrement d'exportation pour le moment. Une fois que vous exportez des données, tous les enregistrements apparaîtront ici.", + }, + tokens: { + title: "Aucun jeton personnel pour le moment", + description: + "Générez des jetons API sécurisés pour connecter votre espace de travail avec des systèmes et applications externes.", + cta_primary: "Ajouter un jeton API", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/id/empty-state.ts b/packages/i18n/src/locales/id/empty-state.ts new file mode 100644 index 000000000..c6238e184 --- /dev/null +++ b/packages/i18n/src/locales/id/empty-state.ts @@ -0,0 +1,185 @@ +export default { + common: { + progress: { + title: "Belum ada metrik progres untuk ditampilkan.", + description: "Mulai mengatur nilai properti dalam item kerja untuk melihat metrik progres di sini.", + }, + updates: { + title: "Belum ada pembaruan.", + description: "Setelah anggota proyek menambahkan pembaruan, itu akan muncul di sini", + }, + search: { + title: "Tidak ada hasil yang cocok.", + description: "Tidak ada hasil yang ditemukan. Coba sesuaikan istilah pencarian Anda.", + }, + not_found: { + title: "Ups! Ada yang salah", + description: "Kami tidak dapat mengambil akun Plane Anda saat ini. Ini mungkin kesalahan jaringan.", + cta_primary: "Coba muat ulang", + }, + server_error: { + title: "Kesalahan server", + description: + "Kami tidak dapat terhubung dan mengambil data dari server kami. Jangan khawatir, kami sedang menanganinya.", + cta_primary: "Coba muat ulang", + }, + }, + project: { + work_items: { + title: "Mulai dengan item kerja pertama Anda.", + description: + "Item kerja adalah blok bangunan proyek Anda — tetapkan pemilik, atur prioritas, dan lacak kemajuan dengan mudah.", + cta_primary: "Buat item kerja pertama Anda", + }, + cycles: { + title: "Kelompokkan dan batasi waktu pekerjaan Anda dalam Siklus.", + description: + "Pecah pekerjaan menjadi potongan dengan batas waktu, bekerja mundur dari tenggat proyek Anda untuk menetapkan tanggal, dan buat kemajuan nyata sebagai tim.", + cta_primary: "Atur siklus pertama Anda", + }, + cycle_work_items: { + title: "Tidak ada item kerja untuk ditampilkan dalam siklus ini", + description: + "Buat item kerja untuk mulai memantau kemajuan tim Anda dalam siklus ini dan capai tujuan Anda tepat waktu.", + cta_primary: "Buat item kerja", + cta_secondary: "Tambahkan item kerja yang ada", + }, + modules: { + title: "Petakan tujuan proyek Anda ke Modul dan lacak dengan mudah.", + description: + "Modul terdiri dari item kerja yang saling berhubungan. Mereka membantu dalam memantau kemajuan melalui fase proyek, masing-masing dengan tenggat waktu dan analitik khusus untuk menunjukkan seberapa dekat Anda dengan mencapai fase tersebut.", + cta_primary: "Atur modul pertama Anda", + }, + module_work_items: { + title: "Tidak ada item kerja untuk ditampilkan dalam Modul ini", + description: "Buat item kerja untuk mulai memantau modul ini.", + cta_primary: "Buat item kerja", + cta_secondary: "Tambahkan item kerja yang ada", + }, + views: { + title: "Simpan tampilan kustom untuk proyek Anda", + description: + "Tampilan adalah filter yang disimpan yang membantu Anda mengakses informasi yang paling sering Anda gunakan dengan cepat. Berkolaborasi dengan mudah saat rekan tim berbagi dan menyesuaikan tampilan dengan kebutuhan spesifik mereka.", + cta_primary: "Buat tampilan", + }, + no_work_items_in_project: { + title: "Belum ada item kerja dalam proyek", + description: + "Tambahkan item kerja ke proyek Anda dan potong pekerjaan Anda menjadi bagian yang dapat dilacak dengan tampilan.", + cta_primary: "Tambahkan item kerja", + }, + work_item_filter: { + title: "Tidak ada item kerja yang ditemukan", + description: "Filter Anda saat ini tidak mengembalikan hasil apapun. Coba ubah filter.", + cta_primary: "Tambahkan item kerja", + }, + pages: { + title: "Dokumentasikan segalanya — dari catatan hingga PRD", + description: + "Halaman memungkinkan Anda menangkap dan mengorganisir informasi di satu tempat. Tulis catatan rapat, dokumentasi proyek, dan PRD, sematkan item kerja, dan strukturkan dengan komponen siap pakai.", + cta_primary: "Buat Halaman pertama Anda", + }, + archive_pages: { + title: "Belum ada halaman yang diarsipkan", + description: "Arsipkan halaman yang tidak ada dalam radar Anda. Akses di sini saat dibutuhkan.", + }, + intake_sidebar: { + title: "Catat permintaan Masuk", + description: "Kirim permintaan baru untuk ditinjau, diprioritaskan, dan dilacak dalam alur kerja proyek Anda.", + cta_primary: "Buat permintaan Masuk", + }, + intake_main: { + title: "Pilih item kerja Masuk untuk melihat detailnya", + }, + }, + workspace: { + archive_work_items: { + title: "Belum ada item kerja yang diarsipkan", + description: + "Secara manual atau melalui otomasi, Anda dapat mengarsipkan item kerja yang selesai atau dibatalkan. Temukan di sini setelah diarsipkan.", + cta_primary: "Atur otomasi", + }, + archive_cycles: { + title: "Belum ada siklus yang diarsipkan", + description: + "Untuk merapikan proyek Anda, arsipkan siklus yang telah selesai. Temukan di sini setelah diarsipkan.", + }, + archive_modules: { + title: "Belum ada Modul yang diarsipkan", + description: + "Untuk merapikan proyek Anda, arsipkan modul yang selesai atau dibatalkan. Temukan di sini setelah diarsipkan.", + }, + home_widget_quick_links: { + title: "Simpan referensi penting, sumber daya, atau dokumen untuk pekerjaan Anda", + }, + inbox_sidebar_all: { + title: "Pembaruan untuk item kerja yang Anda langgani akan muncul di sini", + }, + inbox_sidebar_mentions: { + title: "Penyebutan untuk item kerja Anda akan muncul di sini", + }, + your_work_by_priority: { + title: "Belum ada item kerja yang ditugaskan", + }, + your_work_by_state: { + title: "Belum ada item kerja yang ditugaskan", + }, + views: { + title: "Belum ada Tampilan", + description: + "Tambahkan item kerja ke proyek Anda dan gunakan tampilan untuk memfilter, mengurutkan, dan memantau kemajuan dengan mudah.", + cta_primary: "Tambahkan item kerja", + }, + drafts: { + title: "Item kerja setengah jadi", + description: + "Untuk mencoba ini, mulai menambahkan item kerja dan tinggalkan di tengah jalan atau buat draf pertama Anda di bawah ini. 😉", + cta_primary: "Buat item kerja draf", + }, + projects_archived: { + title: "Tidak ada proyek yang diarsipkan", + description: "Sepertinya semua proyek Anda masih aktif—kerja bagus!", + }, + analytics_projects: { + title: "Buat proyek untuk memvisualisasikan metrik proyek di sini.", + }, + analytics_work_items: { + title: + "Buat proyek dengan item kerja dan penerima tugas untuk mulai melacak kinerja, kemajuan, dan dampak tim di sini.", + }, + analytics_no_cycle: { + title: + "Buat siklus untuk mengorganisir pekerjaan ke dalam fase berbatas waktu dan melacak kemajuan di seluruh sprint.", + }, + analytics_no_module: { + title: "Buat modul untuk mengorganisir pekerjaan Anda dan melacak kemajuan di berbagai tahap.", + }, + analytics_no_intake: { + title: "Siapkan masukan untuk mengelola permintaan masuk dan melacak bagaimana mereka diterima dan ditolak", + }, + }, + settings: { + estimates: { + title: "Belum ada estimasi", + description: "Tentukan bagaimana tim Anda mengukur upaya dan lacak secara konsisten di semua item kerja.", + cta_primary: "Tambahkan sistem estimasi", + }, + labels: { + title: "Belum ada label", + description: + "Buat label yang dipersonalisasi untuk mengkategorikan dan mengelola item kerja Anda secara efektif.", + cta_primary: "Buat label pertama Anda", + }, + exports: { + title: "Belum ada ekspor", + description: + "Anda tidak memiliki catatan ekspor saat ini. Setelah Anda mengekspor data, semua catatan akan muncul di sini.", + }, + tokens: { + title: "Belum ada token Pribadi", + description: + "Hasilkan token API yang aman untuk menghubungkan ruang kerja Anda dengan sistem dan aplikasi eksternal.", + cta_primary: "Tambahkan token API", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/index.ts b/packages/i18n/src/locales/index.ts index 0bdf18a64..50188889e 100644 --- a/packages/i18n/src/locales/index.ts +++ b/packages/i18n/src/locales/index.ts @@ -3,6 +3,7 @@ export { default as enCore } from "./en/core"; export { default as enTranslations } from "./en/translations"; export { default as enAccessibility } from "./en/accessibility"; export { default as enEditor } from "./en/editor"; +export { default as enEmptyState } from "./en/empty-state"; // Export locale data for all supported languages export const locales = { @@ -11,95 +12,114 @@ export const locales = { translations: () => import("./en/translations"), accessibility: () => import("./en/accessibility"), editor: () => import("./en/editor"), + emptyState: () => import("./en/empty-state"), }, fr: { translations: () => import("./fr/translations"), accessibility: () => import("./fr/accessibility"), editor: () => import("./fr/editor"), + emptyState: () => import("./fr/empty-state"), }, es: { translations: () => import("./es/translations"), accessibility: () => import("./es/accessibility"), editor: () => import("./es/editor"), + emptyState: () => import("./es/empty-state"), }, ja: { translations: () => import("./ja/translations"), accessibility: () => import("./ja/accessibility"), editor: () => import("./ja/editor"), + emptyState: () => import("./ja/empty-state"), }, "zh-CN": { translations: () => import("./zh-CN/translations"), accessibility: () => import("./zh-CN/accessibility"), editor: () => import("./zh-CN/editor"), + emptyState: () => import("./zh-CN/empty-state"), }, "zh-TW": { translations: () => import("./zh-TW/translations"), accessibility: () => import("./zh-TW/accessibility"), editor: () => import("./zh-TW/editor"), + emptyState: () => import("./zh-TW/empty-state"), }, ru: { translations: () => import("./ru/translations"), accessibility: () => import("./ru/accessibility"), editor: () => import("./ru/editor"), + emptyState: () => import("./ru/empty-state"), }, it: { translations: () => import("./it/translations"), accessibility: () => import("./it/accessibility"), editor: () => import("./it/editor"), + emptyState: () => import("./it/empty-state"), }, cs: { translations: () => import("./cs/translations"), accessibility: () => import("./cs/accessibility"), editor: () => import("./cs/editor"), + emptyState: () => import("./cs/empty-state"), }, sk: { translations: () => import("./sk/translations"), accessibility: () => import("./sk/accessibility"), editor: () => import("./sk/editor"), + emptyState: () => import("./sk/empty-state"), }, de: { translations: () => import("./de/translations"), accessibility: () => import("./de/accessibility"), editor: () => import("./de/editor"), + emptyState: () => import("./de/empty-state"), }, ua: { translations: () => import("./ua/translations"), accessibility: () => import("./ua/accessibility"), editor: () => import("./ua/editor"), + emptyState: () => import("./ua/empty-state"), }, pl: { translations: () => import("./pl/translations"), accessibility: () => import("./pl/accessibility"), editor: () => import("./pl/editor"), + emptyState: () => import("./pl/empty-state"), }, ko: { translations: () => import("./ko/translations"), accessibility: () => import("./ko/accessibility"), editor: () => import("./ko/editor"), + emptyState: () => import("./ko/empty-state"), }, "pt-BR": { translations: () => import("./pt-BR/translations"), accessibility: () => import("./pt-BR/accessibility"), editor: () => import("./pt-BR/editor"), + emptyState: () => import("./pt-BR/empty-state"), }, id: { translations: () => import("./id/translations"), accessibility: () => import("./id/accessibility"), editor: () => import("./id/editor"), + emptyState: () => import("./id/empty-state"), }, ro: { translations: () => import("./ro/translations"), accessibility: () => import("./ro/accessibility"), editor: () => import("./ro/editor"), + emptyState: () => import("./ro/empty-state"), }, "vi-VN": { translations: () => import("./vi-VN/translations"), accessibility: () => import("./vi-VN/accessibility"), editor: () => import("./vi-VN/editor"), + emptyState: () => import("./vi-VN/empty-state"), }, "tr-TR": { translations: () => import("./tr-TR/translations"), accessibility: () => import("./tr-TR/accessibility"), editor: () => import("./tr-TR/editor"), + emptyState: () => import("./tr-TR/empty-state"), }, }; diff --git a/packages/i18n/src/locales/it/empty-state.ts b/packages/i18n/src/locales/it/empty-state.ts new file mode 100644 index 000000000..3417eab8d --- /dev/null +++ b/packages/i18n/src/locales/it/empty-state.ts @@ -0,0 +1,185 @@ +export default { + common: { + progress: { + title: "Non ci sono ancora metriche di progresso da mostrare.", + description: + "Inizia a impostare i valori delle proprietà negli elementi di lavoro per vedere qui le metriche di progresso.", + }, + updates: { + title: "Nessun aggiornamento ancora.", + description: "Una volta che i membri del progetto aggiungono aggiornamenti, appariranno qui", + }, + search: { + title: "Nessun risultato corrispondente.", + description: "Nessun risultato trovato. Prova a modificare i termini di ricerca.", + }, + not_found: { + title: "Oops! Qualcosa sembra andare storto", + description: + "Al momento non riusciamo a recuperare il tuo account plane. Potrebbe trattarsi di un errore di rete.", + cta_primary: "Prova a ricaricare", + }, + server_error: { + title: "Errore del server", + description: + "Non riusciamo a connetterci e recuperare dati dal nostro server. Non preoccuparti, ci stiamo lavorando.", + cta_primary: "Prova a ricaricare", + }, + }, + project: { + work_items: { + title: "Inizia con il tuo primo elemento di lavoro.", + description: + "Gli elementi di lavoro sono i mattoni del tuo progetto — assegna proprietari, imposta priorità e traccia facilmente i progressi.", + cta_primary: "Crea il tuo primo elemento di lavoro", + }, + cycles: { + title: "Raggruppa e delimita temporalmente il tuo lavoro in Cicli.", + description: + "Suddividi il lavoro in blocchi temporali, lavora a ritroso dalla scadenza del progetto per impostare le date e fai progressi tangibili come team.", + cta_primary: "Imposta il tuo primo ciclo", + }, + cycle_work_items: { + title: "Nessun elemento di lavoro da mostrare in questo ciclo", + description: + "Crea elementi di lavoro per iniziare a monitorare il progresso del tuo team in questo ciclo e raggiungere i tuoi obiettivi in tempo.", + cta_primary: "Crea elemento di lavoro", + cta_secondary: "Aggiungi elemento di lavoro esistente", + }, + modules: { + title: "Mappa gli obiettivi del tuo progetto ai Moduli e traccia facilmente.", + description: + "I moduli sono costituiti da elementi di lavoro interconnessi. Aiutano a monitorare i progressi attraverso le fasi del progetto, ciascuna con scadenze specifiche e analitiche per indicare quanto sei vicino al raggiungimento di quelle fasi.", + cta_primary: "Imposta il tuo primo modulo", + }, + module_work_items: { + title: "Nessun elemento di lavoro da mostrare in questo Modulo", + description: "Crea elementi di lavoro per iniziare a monitorare questo modulo.", + cta_primary: "Crea elemento di lavoro", + cta_secondary: "Aggiungi elemento di lavoro esistente", + }, + views: { + title: "Salva viste personalizzate per il tuo progetto", + description: + "Le viste sono filtri salvati che ti aiutano ad accedere rapidamente alle informazioni che usi di più. Collabora senza sforzo mentre i colleghi condividono e personalizzano le viste secondo le loro esigenze specifiche.", + cta_primary: "Crea vista", + }, + no_work_items_in_project: { + title: "Nessun elemento di lavoro nel progetto ancora", + description: + "Aggiungi elementi di lavoro al tuo progetto e suddividi il lavoro in pezzi tracciabili con le viste.", + cta_primary: "Aggiungi elemento di lavoro", + }, + work_item_filter: { + title: "Nessun elemento di lavoro trovato", + description: "Il filtro attuale non ha restituito risultati. Prova a modificare i filtri.", + cta_primary: "Aggiungi elemento di lavoro", + }, + pages: { + title: "Documenta tutto — dalle note ai PRD", + description: + "Le pagine ti permettono di catturare e organizzare informazioni in un unico posto. Scrivi note di riunioni, documentazione di progetto e PRD, incorpora elementi di lavoro e strutturali con componenti pronti all'uso.", + cta_primary: "Crea la tua prima Pagina", + }, + archive_pages: { + title: "Nessuna pagina archiviata ancora", + description: "Archivia le pagine non nel tuo radar. Accedi a esse qui quando necessario.", + }, + intake_sidebar: { + title: "Registra richieste di Intake", + description: + "Invia nuove richieste da rivedere, dare priorità e tracciare all'interno del flusso di lavoro del tuo progetto.", + cta_primary: "Crea richiesta di Intake", + }, + intake_main: { + title: "Seleziona un elemento di lavoro di Intake per visualizzarne i dettagli", + }, + }, + workspace: { + archive_work_items: { + title: "Nessun elemento di lavoro archiviato ancora", + description: + "Manualmente o tramite automazione, puoi archiviare elementi di lavoro completati o annullati. Trovali qui una volta archiviati.", + cta_primary: "Imposta automazione", + }, + archive_cycles: { + title: "Nessun ciclo archiviato ancora", + description: "Per riordinare il tuo progetto, archivia i cicli completati. Trovali qui una volta archiviati.", + }, + archive_modules: { + title: "Nessun Modulo archiviato ancora", + description: + "Per riordinare il tuo progetto, archivia i moduli completati o annullati. Trovali qui una volta archiviati.", + }, + home_widget_quick_links: { + title: "Mantieni a portata di mano riferimenti importanti, risorse o documenti per il tuo lavoro", + }, + inbox_sidebar_all: { + title: "Gli aggiornamenti per i tuoi elementi di lavoro sottoscritti appariranno qui", + }, + inbox_sidebar_mentions: { + title: "Le menzioni per i tuoi elementi di lavoro appariranno qui", + }, + your_work_by_priority: { + title: "Nessun elemento di lavoro assegnato ancora", + }, + your_work_by_state: { + title: "Nessun elemento di lavoro assegnato ancora", + }, + views: { + title: "Nessuna Vista ancora", + description: + "Aggiungi elementi di lavoro al tuo progetto e usa le viste per filtrare, ordinare e monitorare i progressi senza sforzo.", + cta_primary: "Aggiungi elemento di lavoro", + }, + drafts: { + title: "Elementi di lavoro semi-scritti", + description: + "Per provare questo, inizia ad aggiungere un elemento di lavoro e lascialo a metà o crea la tua prima bozza qui sotto. 😉", + cta_primary: "Crea elemento di lavoro bozza", + }, + projects_archived: { + title: "Nessun progetto archiviato", + description: "Sembra che tutti i tuoi progetti siano ancora attivi—ottimo lavoro!", + }, + analytics_projects: { + title: "Crea progetti per visualizzare qui le metriche del progetto.", + }, + analytics_work_items: { + title: + "Crea progetti con elementi di lavoro e assegnatari per iniziare a tracciare prestazioni, progressi e impatto del team qui.", + }, + analytics_no_cycle: { + title: "Crea cicli per organizzare il lavoro in fasi temporali e tracciare i progressi attraverso gli sprint.", + }, + analytics_no_module: { + title: "Crea moduli per organizzare il tuo lavoro e tracciare i progressi attraverso diverse fasi.", + }, + analytics_no_intake: { + title: "Imposta intake per gestire le richieste in arrivo e tracciare come vengono accettate e rifiutate", + }, + }, + settings: { + estimates: { + title: "Nessuna stima ancora", + description: + "Definisci come il tuo team misura lo sforzo e traccialo in modo coerente su tutti gli elementi di lavoro.", + cta_primary: "Aggiungi sistema di stima", + }, + labels: { + title: "Nessuna etichetta ancora", + description: "Crea etichette personalizzate per categorizzare e gestire efficacemente i tuoi elementi di lavoro.", + cta_primary: "Crea la tua prima etichetta", + }, + exports: { + title: "Nessuna esportazione ancora", + description: + "Al momento non hai record di esportazione. Una volta esportati i dati, tutti i record appariranno qui.", + }, + tokens: { + title: "Nessun token personale ancora", + description: "Genera token API sicuri per connettere il tuo workspace con sistemi e applicazioni esterne.", + cta_primary: "Aggiungi token API", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/ja/empty-state.ts b/packages/i18n/src/locales/ja/empty-state.ts new file mode 100644 index 000000000..90a3af7e1 --- /dev/null +++ b/packages/i18n/src/locales/ja/empty-state.ts @@ -0,0 +1,175 @@ +export default { + common: { + progress: { + title: "表示する進捗メトリクスがまだありません。", + description: "作業項目にプロパティ値を設定して、ここに進捗メトリクスを表示します。", + }, + updates: { + title: "更新はまだありません。", + description: "プロジェクトメンバーが更新を追加すると、ここに表示されます", + }, + search: { + title: "一致する結果が見つかりません。", + description: "結果が見つかりませんでした。検索条件を調整してください。", + }, + not_found: { + title: "おっと!何か問題があるようです", + description: "現在、Planeアカウントを取得できません。ネットワークエラーの可能性があります。", + cta_primary: "再読み込みを試す", + }, + server_error: { + title: "サーバーエラー", + description: "サーバーに接続してデータを取得できません。ご心配なく、対応中です。", + cta_primary: "再読み込みを試す", + }, + }, + project: { + work_items: { + title: "最初の作業項目から始めましょう。", + description: + "作業項目はプロジェクトの構成要素です — 担当者の割り当て、優先度の設定、進捗の追跡が簡単に行えます。", + cta_primary: "最初の作業項目を作成", + }, + cycles: { + title: "サイクルで作業をグループ化してタイムボックス化します。", + description: + "作業をタイムボックスで区切り、プロジェクトの締め切りから逆算して日付を設定し、チームとして具体的な進捗を達成します。", + cta_primary: "最初のサイクルを設定", + }, + cycle_work_items: { + title: "このサイクルに表示する作業項目はありません", + description: "作業項目を作成して、このサイクルでチームの進捗を監視し、目標を時間内に達成しましょう。", + cta_primary: "作業項目を作成", + cta_secondary: "既存の作業項目を追加", + }, + modules: { + title: "プロジェクトの目標をモジュールにマッピングして簡単に追跡します。", + description: + "モジュールは相互接続された作業項目で構成されています。プロジェクトフェーズを通じた進捗の監視を支援し、それぞれに特定の締め切りと分析があり、それらのフェーズをどれだけ達成に近づいているかを示します。", + cta_primary: "最初のモジュールを設定", + }, + module_work_items: { + title: "このモジュールに表示する作業項目はありません", + description: "作業項目を作成して、このモジュールの監視を開始します。", + cta_primary: "作業項目を作成", + cta_secondary: "既存の作業項目を追加", + }, + views: { + title: "プロジェクトのカスタムビューを保存", + description: + "ビューは保存されたフィルターで、最も頻繁に使用する情報に素早くアクセスできます。チームメイトがビューを共有し、それぞれのニーズに合わせて調整することで、簡単に協力できます。", + cta_primary: "ビューを作成", + }, + no_work_items_in_project: { + title: "プロジェクトにはまだ作業項目がありません", + description: "プロジェクトに作業項目を追加し、ビューを使用して作業を追跡可能な部分に分割します。", + cta_primary: "作業項目を追加", + }, + work_item_filter: { + title: "作業項目が見つかりません", + description: "現在のフィルターでは結果が返されませんでした。フィルターを変更してみてください。", + cta_primary: "作業項目を追加", + }, + pages: { + title: "メモからPRDまで、すべてを文書化", + description: + "ページを使用すると、情報を1か所でキャプチャして整理できます。会議のメモ、プロジェクトドキュメント、PRDを作成し、作業項目を埋め込み、すぐに使えるコンポーネントで構造化します。", + cta_primary: "最初のページを作成", + }, + archive_pages: { + title: "アーカイブされたページはまだありません", + description: "注目していないページをアーカイブします。必要に応じてここでアクセスできます。", + }, + intake_sidebar: { + title: "インテークリクエストを記録", + description: "新しいリクエストを送信して、プロジェクトのワークフロー内でレビュー、優先順位付け、追跡を行います。", + cta_primary: "インテークリクエストを作成", + }, + intake_main: { + title: "インテーク作業項目を選択して詳細を表示", + }, + }, + workspace: { + archive_work_items: { + title: "アーカイブされた作業項目はまだありません", + description: + "手動または自動化により、完了またはキャンセルされた作業項目をアーカイブできます。アーカイブされると、ここで見つけられます。", + cta_primary: "自動化を設定", + }, + archive_cycles: { + title: "アーカイブされたサイクルはまだありません", + description: + "プロジェクトを整理するために、完了したサイクルをアーカイブします。アーカイブされると、ここで見つけられます。", + }, + archive_modules: { + title: "アーカイブされたモジュールはまだありません", + description: + "プロジェクトを整理するために、完了またはキャンセルされたモジュールをアーカイブします。アーカイブされると、ここで見つけられます。", + }, + home_widget_quick_links: { + title: "作業に重要な参照、リソース、またはドキュメントを手元に保管", + }, + inbox_sidebar_all: { + title: "購読している作業項目の更新がここに表示されます", + }, + inbox_sidebar_mentions: { + title: "作業項目でのメンションがここに表示されます", + }, + your_work_by_priority: { + title: "割り当てられた作業項目はまだありません", + }, + your_work_by_state: { + title: "割り当てられた作業項目はまだありません", + }, + views: { + title: "ビューはまだありません", + description: "プロジェクトに作業項目を追加し、ビューを使用してフィルター、ソート、進捗の監視を簡単に行います。", + cta_primary: "作業項目を追加", + }, + drafts: { + title: "途中の作業項目", + description: "これを試すには、作業項目の追加を開始して途中で離れるか、以下で最初の下書きを作成してください。😉", + cta_primary: "下書き作業項目を作成", + }, + projects_archived: { + title: "アーカイブされたプロジェクトはありません", + description: "すべてのプロジェクトがまだアクティブです — 素晴らしい!", + }, + analytics_projects: { + title: "プロジェクトを作成して、ここでプロジェクトメトリクスを視覚化します。", + }, + analytics_work_items: { + title: "作業項目と担当者を含むプロジェクトを作成して、パフォーマンス、進捗、チームの影響をここで追跡開始します。", + }, + analytics_no_cycle: { + title: "サイクルを作成して、作業を期限付きフェーズに整理し、スプリント全体の進捗を追跡します。", + }, + analytics_no_module: { + title: "モジュールを作成して、作業を整理し、さまざまな段階での進捗を追跡します。", + }, + analytics_no_intake: { + title: "インテークを設定して、受信リクエストを管理し、承認と拒否を追跡します", + }, + }, + settings: { + estimates: { + title: "まだ見積もりはありません", + description: "チームが労力をどのように測定するかを定義し、すべての作業項目で一貫して追跡します。", + cta_primary: "見積もりシステムを追加", + }, + labels: { + title: "まだラベルはありません", + description: "作業項目を効果的に分類および管理するためのパーソナライズされたラベルを作成します。", + cta_primary: "最初のラベルを作成", + }, + exports: { + title: "まだエクスポートはありません", + description: "現在、エクスポート記録はありません。データをエクスポートすると、すべての記録がここに表示されます。", + }, + tokens: { + title: "まだ個人トークンはありません", + description: "ワークスペースを外部システムおよびアプリケーションと接続するための安全なAPIトークンを生成します。", + cta_primary: "APIトークンを追加", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/ko/empty-state.ts b/packages/i18n/src/locales/ko/empty-state.ts new file mode 100644 index 000000000..c85b87e2f --- /dev/null +++ b/packages/i18n/src/locales/ko/empty-state.ts @@ -0,0 +1,174 @@ +export default { + common: { + progress: { + title: "아직 표시할 진행 지표가 없습니다.", + description: "작업 항목에서 속성 값을 설정하여 여기에서 진행 지표를 확인하세요.", + }, + updates: { + title: "아직 업데이트가 없습니다.", + description: "프로젝트 멤버가 업데이트를 추가하면 여기에 표시됩니다", + }, + search: { + title: "일치하는 결과가 없습니다.", + description: "결과를 찾을 수 없습니다. 검색어를 조정해 보세요.", + }, + not_found: { + title: "앗! 문제가 발생한 것 같습니다", + description: "현재 Plane 계정을 가져올 수 없습니다. 네트워크 오류일 수 있습니다.", + cta_primary: "다시 로드 시도", + }, + server_error: { + title: "서버 오류", + description: "서버에 연결하여 데이터를 가져올 수 없습니다. 걱정하지 마세요, 작업 중입니다.", + cta_primary: "다시 로드 시도", + }, + }, + project: { + work_items: { + title: "첫 번째 작업 항목으로 시작하세요.", + description: + "작업 항목은 프로젝트의 구성 요소입니다. 소유자를 할당하고 우선순위를 설정하며 진행 상황을 쉽게 추적할 수 있습니다.", + cta_primary: "첫 번째 작업 항목 생성", + }, + cycles: { + title: "사이클로 작업을 그룹화하고 시간을 정하세요.", + description: + "시간 제한이 있는 단위로 작업을 나누고, 프로젝트 마감일로부터 역으로 날짜를 설정하며, 팀으로서 구체적인 진전을 이루세요.", + cta_primary: "첫 번째 사이클 설정", + }, + cycle_work_items: { + title: "이 사이클에 표시할 작업 항목이 없습니다", + description: "작업 항목을 생성하여 이 사이클 동안 팀의 진행 상황을 모니터링하고 제시간에 목표를 달성하세요.", + cta_primary: "작업 항목 생성", + cta_secondary: "기존 작업 항목 추가", + }, + modules: { + title: "프로젝트 목표를 모듈에 매핑하고 쉽게 추적하세요.", + description: + "모듈은 상호 연결된 작업 항목으로 구성됩니다. 특정 기한과 분석을 통해 프로젝트 단계를 통한 진행 상황을 모니터링하여 해당 단계 달성에 얼마나 가까운지 나타냅니다.", + cta_primary: "첫 번째 모듈 설정", + }, + module_work_items: { + title: "이 모듈에 표시할 작업 항목이 없습니다", + description: "작업 항목을 생성하여 이 모듈을 모니터링하기 시작하세요.", + cta_primary: "작업 항목 생성", + cta_secondary: "기존 작업 항목 추가", + }, + views: { + title: "프로젝트를 위한 사용자 정의 보기 저장", + description: + "보기는 가장 자주 사용하는 정보에 빠르게 액세스하는 데 도움이 되는 저장된 필터입니다. 팀원들이 특정 요구 사항에 맞게 보기를 공유하고 조정하면서 손쉽게 협업하세요.", + cta_primary: "보기 생성", + }, + no_work_items_in_project: { + title: "프로젝트에 아직 작업 항목이 없습니다", + description: "프로젝트에 작업 항목을 추가하고 보기를 사용하여 작업을 추적 가능한 조각으로 나누세요.", + cta_primary: "작업 항목 추가", + }, + work_item_filter: { + title: "작업 항목을 찾을 수 없습니다", + description: "현재 필터가 결과를 반환하지 않았습니다. 필터를 변경해 보세요.", + cta_primary: "작업 항목 추가", + }, + pages: { + title: "메모부터 PRD까지 모든 것을 문서화하세요", + description: + "페이지를 사용하면 정보를 한 곳에서 캡처하고 구성할 수 있습니다. 회의록, 프로젝트 문서 및 PRD를 작성하고, 작업 항목을 삽입하며, 바로 사용할 수 있는 구성 요소로 구조화하세요.", + cta_primary: "첫 번째 페이지 생성", + }, + archive_pages: { + title: "아직 보관된 페이지가 없습니다", + description: "주목하지 않는 페이지를 보관하세요. 필요할 때 여기에서 액세스하세요.", + }, + intake_sidebar: { + title: "접수 요청 기록", + description: "프로젝트 워크플로우 내에서 검토, 우선순위 지정 및 추적할 새로운 요청을 제출하세요.", + cta_primary: "접수 요청 생성", + }, + intake_main: { + title: "접수 작업 항목을 선택하여 세부 정보 보기", + }, + }, + workspace: { + archive_work_items: { + title: "아직 보관된 작업 항목이 없습니다", + description: + "수동으로 또는 자동화를 통해 완료되거나 취소된 작업 항목을 보관할 수 있습니다. 보관되면 여기에서 찾을 수 있습니다.", + cta_primary: "자동화 설정", + }, + archive_cycles: { + title: "아직 보관된 사이클이 없습니다", + description: "프로젝트를 정리하려면 완료된 사이클을 보관하세요. 보관되면 여기에서 찾을 수 있습니다.", + }, + archive_modules: { + title: "아직 보관된 모듈이 없습니다", + description: "프로젝트를 정리하려면 완료되거나 취소된 모듈을 보관하세요. 보관되면 여기에서 찾을 수 있습니다.", + }, + home_widget_quick_links: { + title: "작업에 편리한 중요한 참조 자료, 리소스 또는 문서 보관", + }, + inbox_sidebar_all: { + title: "구독한 작업 항목에 대한 업데이트가 여기에 표시됩니다", + }, + inbox_sidebar_mentions: { + title: "작업 항목에 대한 언급이 여기에 표시됩니다", + }, + your_work_by_priority: { + title: "아직 할당된 작업 항목이 없습니다", + }, + your_work_by_state: { + title: "아직 할당된 작업 항목이 없습니다", + }, + views: { + title: "아직 보기가 없습니다", + description: "프로젝트에 작업 항목을 추가하고 보기를 사용하여 쉽게 필터링, 정렬 및 진행 상황을 모니터링하세요.", + cta_primary: "작업 항목 추가", + }, + drafts: { + title: "작성 중인 작업 항목", + description: "이를 시도하려면 작업 항목 추가를 시작하고 중간에 남겨두거나 아래에 첫 번째 초안을 만드세요. 😉", + cta_primary: "초안 작업 항목 생성", + }, + projects_archived: { + title: "보관된 프로젝트가 없습니다", + description: "모든 프로젝트가 여전히 활성 상태인 것 같습니다. 잘하셨습니다!", + }, + analytics_projects: { + title: "여기에서 프로젝트 지표를 시각화하려면 프로젝트를 생성하세요.", + }, + analytics_work_items: { + title: + "작업 항목 및 담당자가 있는 프로젝트를 생성하여 여기에서 성과, 진행 상황 및 팀 영향을 추적하기 시작하세요.", + }, + analytics_no_cycle: { + title: "사이클을 생성하여 작업을 시간 제한 단계로 구성하고 스프린트 전반에 걸쳐 진행 상황을 추적하세요.", + }, + analytics_no_module: { + title: "모듈을 생성하여 작업을 구성하고 다양한 단계에서 진행 상황을 추적하세요.", + }, + analytics_no_intake: { + title: "접수를 설정하여 들어오는 요청을 관리하고 승인 및 거부 방법을 추적하세요", + }, + }, + settings: { + estimates: { + title: "아직 추정치가 없습니다", + description: "팀이 노력을 측정하는 방법을 정의하고 모든 작업 항목에서 일관되게 추적하세요.", + cta_primary: "추정 시스템 추가", + }, + labels: { + title: "아직 레이블이 없습니다", + description: "작업 항목을 효과적으로 분류하고 관리하기 위한 개인화된 레이블을 만드세요.", + cta_primary: "첫 번째 레이블 생성", + }, + exports: { + title: "아직 내보내기가 없습니다", + description: "현재 내보내기 기록이 없습니다. 데이터를 내보내면 모든 기록이 여기에 표시됩니다.", + }, + tokens: { + title: "아직 개인 토큰이 없습니다", + description: "작업 공간을 외부 시스템 및 애플리케이션과 연결하기 위한 보안 API 토큰을 생성하세요.", + cta_primary: "API 토큰 추가", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/pl/empty-state.ts b/packages/i18n/src/locales/pl/empty-state.ts new file mode 100644 index 000000000..0fe9c9fb4 --- /dev/null +++ b/packages/i18n/src/locales/pl/empty-state.ts @@ -0,0 +1,186 @@ +export default { + common: { + progress: { + title: "Nie ma jeszcze metryk postępu do wyświetlenia.", + description: "Zacznij ustawiać wartości właściwości w elementach roboczych, aby zobaczyć tutaj metryki postępu.", + }, + updates: { + title: "Jeszcze brak aktualizacji.", + description: "Gdy członkowie projektu dodadzą aktualizacje, pojawią się one tutaj", + }, + search: { + title: "Brak pasujących wyników.", + description: "Nie znaleziono wyników. Spróbuj dostosować wyszukiwane hasła.", + }, + not_found: { + title: "Ups! Coś wydaje się nie tak", + description: "Obecnie nie możemy pobrać Twojego konta plane. Może to być błąd sieci.", + cta_primary: "Spróbuj przeładować", + }, + server_error: { + title: "Błąd serwera", + description: "Nie możemy się połączyć i pobrać danych z naszego serwera. Nie martw się, pracujemy nad tym.", + cta_primary: "Spróbuj przeładować", + }, + }, + project: { + work_items: { + title: "Zacznij od swojego pierwszego elementu roboczego.", + description: + "Elementy robocze są podstawowymi elementami Twojego projektu — przypisuj właścicieli, ustalaj priorytety i łatwo śledź postęp.", + cta_primary: "Utwórz swój pierwszy element roboczy", + }, + cycles: { + title: "Grupuj i ograniczaj czasowo swoją pracę w Cyklach.", + description: + "Podziel pracę na bloki czasowe, pracuj wstecz od terminu projektu, aby ustalić daty, i osiągaj wymierny postęp jako zespół.", + cta_primary: "Ustaw swój pierwszy cykl", + }, + cycle_work_items: { + title: "Brak elementów roboczych do wyświetlenia w tym cyklu", + description: + "Utwórz elementy robocze, aby rozpocząć monitorowanie postępów Twojego zespołu w tym cyklu i osiągnąć swoje cele na czas.", + cta_primary: "Utwórz element roboczy", + cta_secondary: "Dodaj istniejący element roboczy", + }, + modules: { + title: "Mapuj cele swojego projektu na Moduły i łatwo śledź.", + description: + "Moduły składają się z połączonych elementów roboczych. Pomagają one monitorować postęp przez fazy projektu, każda z konkretnymi terminami i analityką, aby wskazać, jak blisko jesteś osiągnięcia tych faz.", + cta_primary: "Ustaw swój pierwszy moduł", + }, + module_work_items: { + title: "Brak elementów roboczych do wyświetlenia w tym Module", + description: "Utwórz elementy robocze, aby rozpocząć monitorowanie tego modułu.", + cta_primary: "Utwórz element roboczy", + cta_secondary: "Dodaj istniejący element roboczy", + }, + views: { + title: "Zapisz niestandardowe widoki dla swojego projektu", + description: + "Widoki to zapisane filtry, które pomagają szybko uzyskać dostęp do najczęściej używanych informacji. Współpracuj bez wysiłku, gdy członkowie zespołu udostępniają i dostosowują widoki do swoich konkretnych potrzeb.", + cta_primary: "Utwórz widok", + }, + no_work_items_in_project: { + title: "Brak elementów roboczych w projekcie jeszcze", + description: + "Dodaj elementy robocze do swojego projektu i podziel swoją pracę na śledzone części za pomocą widoków.", + cta_primary: "Dodaj element roboczy", + }, + work_item_filter: { + title: "Nie znaleziono elementów roboczych", + description: "Twój aktualny filtr nie zwrócił żadnych wyników. Spróbuj zmienić filtry.", + cta_primary: "Dodaj element roboczy", + }, + pages: { + title: "Dokumentuj wszystko — od notatek po PRD", + description: + "Strony pozwalają przechwytywać i organizować informacje w jednym miejscu. Pisz notatki ze spotkań, dokumentację projektu i PRD, osadzaj elementy robocze i strukturyzuj je za pomocą gotowych komponentów.", + cta_primary: "Utwórz swoją pierwszą Stronę", + }, + archive_pages: { + title: "Jeszcze brak zarchiwizowanych stron", + description: + "Archiwizuj strony, które nie są na Twoim radarze. Uzyskaj do nich dostęp tutaj, gdy będzie to potrzebne.", + }, + intake_sidebar: { + title: "Rejestruj zgłoszenia przyjmowane", + description: + "Przesyłaj nowe zgłoszenia do przeglądu, ustalania priorytetów i śledzenia w ramach przepływu pracy Twojego projektu.", + cta_primary: "Utwórz zgłoszenie przyjmowane", + }, + intake_main: { + title: "Wybierz element roboczy Intake, aby wyświetlić jego szczegóły", + }, + }, + workspace: { + archive_work_items: { + title: "Jeszcze brak zarchiwizowanych elementów roboczych", + description: + "Ręcznie lub za pomocą automatyzacji możesz archiwizować ukończone lub anulowane elementy robocze. Znajdź je tutaj po zarchiwizowaniu.", + cta_primary: "Ustaw automatyzację", + }, + archive_cycles: { + title: "Jeszcze brak zarchiwizowanych cykli", + description: "Aby uporządkować swój projekt, archiwizuj ukończone cykle. Znajdź je tutaj po zarchiwizowaniu.", + }, + archive_modules: { + title: "Jeszcze brak zarchiwizowanych Modułów", + description: + "Aby uporządkować swój projekt, archiwizuj ukończone lub anulowane moduły. Znajdź je tutaj po zarchiwizowaniu.", + }, + home_widget_quick_links: { + title: "Miej pod ręką ważne odniesienia, zasoby lub dokumenty do swojej pracy", + }, + inbox_sidebar_all: { + title: "Aktualizacje dla Twoich subskrybowanych elementów roboczych pojawią się tutaj", + }, + inbox_sidebar_mentions: { + title: "Wzmianki dotyczące Twoich elementów roboczych pojawią się tutaj", + }, + your_work_by_priority: { + title: "Jeszcze nie przypisano elementu roboczego", + }, + your_work_by_state: { + title: "Jeszcze nie przypisano elementu roboczego", + }, + views: { + title: "Jeszcze brak Widoków", + description: + "Dodaj elementy robocze do swojego projektu i używaj widoków do filtrowania, sortowania i monitorowania postępów bez wysiłku.", + cta_primary: "Dodaj element roboczy", + }, + drafts: { + title: "Półnapisane elementy robocze", + description: + "Aby to wypróbować, zacznij dodawać element roboczy i zostaw go w połowie lub utwórz swój pierwszy szkic poniżej. 😉", + cta_primary: "Utwórz szkic elementu roboczego", + }, + projects_archived: { + title: "Brak zarchiwizowanych projektów", + description: "Wygląda na to, że wszystkie Twoje projekty są nadal aktywne—świetna robota!", + }, + analytics_projects: { + title: "Utwórz projekty, aby wizualizować metryki projektu tutaj.", + }, + analytics_work_items: { + title: + "Utwórz projekty z elementami roboczymi i osobami przypisanymi, aby rozpocząć śledzenie wydajności, postępów i wpływu zespołu tutaj.", + }, + analytics_no_cycle: { + title: "Utwórz cykle, aby organizować pracę w fazy czasowe i śledzić postępy przez sprinty.", + }, + analytics_no_module: { + title: "Utwórz moduły, aby organizować swoją pracę i śledzić postępy przez różne fazy.", + }, + analytics_no_intake: { + title: + "Skonfiguruj przyjmowanie, aby zarządzać przychodzącymi zgłoszeniami i śledzić, jak są akceptowane i odrzucane", + }, + }, + settings: { + estimates: { + title: "Jeszcze brak szacunków", + description: + "Zdefiniuj, jak Twój zespół mierzy wysiłek i śledź to konsekwentnie we wszystkich elementach roboczych.", + cta_primary: "Dodaj system szacowania", + }, + labels: { + title: "Jeszcze brak etykiet", + description: + "Twórz spersonalizowane etykiety, aby skutecznie kategoryzować i zarządzać swoimi elementami roboczymi.", + cta_primary: "Utwórz swoją pierwszą etykietę", + }, + exports: { + title: "Jeszcze brak eksportów", + description: + "Obecnie nie masz żadnych rekordów eksportu. Po wyeksportowaniu danych wszystkie rekordy pojawią się tutaj.", + }, + tokens: { + title: "Jeszcze brak Tokenu osobistego", + description: + "Generuj bezpieczne tokeny API, aby połączyć swój obszar roboczy z zewnętrznymi systemami i aplikacjami.", + cta_primary: "Dodaj token API", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/pt-BR/empty-state.ts b/packages/i18n/src/locales/pt-BR/empty-state.ts new file mode 100644 index 000000000..6c3ebc94a --- /dev/null +++ b/packages/i18n/src/locales/pt-BR/empty-state.ts @@ -0,0 +1,185 @@ +export default { + common: { + progress: { + title: "Ainda não há métricas de progresso para mostrar.", + description: + "Comece definindo valores de propriedades em itens de trabalho para ver as métricas de progresso aqui.", + }, + updates: { + title: "Ainda não há atualizações.", + description: "Quando os membros do projeto adicionarem atualizações, elas aparecerão aqui", + }, + search: { + title: "Nenhum resultado correspondente.", + description: "Nenhum resultado encontrado. Tente ajustar seus termos de pesquisa.", + }, + not_found: { + title: "Ops! Algo parece errado", + description: "Não conseguimos buscar sua conta Plane no momento. Pode ser um erro de rede.", + cta_primary: "Tentar recarregar", + }, + server_error: { + title: "Erro do servidor", + description: + "Não conseguimos conectar e buscar dados do nosso servidor. Não se preocupe, estamos trabalhando nisso.", + cta_primary: "Tentar recarregar", + }, + }, + project: { + work_items: { + title: "Comece com seu primeiro item de trabalho.", + description: + "Os itens de trabalho são os blocos de construção do seu projeto — atribua proprietários, defina prioridades e acompanhe o progresso facilmente.", + cta_primary: "Criar seu primeiro item de trabalho", + }, + cycles: { + title: "Agrupe e defina prazos para seu trabalho em Ciclos.", + description: + "Divida o trabalho em blocos com prazo definido, trabalhe de trás para frente a partir do prazo do projeto para definir datas e faça progresso tangível como equipe.", + cta_primary: "Definir seu primeiro ciclo", + }, + cycle_work_items: { + title: "Nenhum item de trabalho para mostrar neste ciclo", + description: + "Crie itens de trabalho para começar a monitorar o progresso da sua equipe neste ciclo e atingir seus objetivos no prazo.", + cta_primary: "Criar item de trabalho", + cta_secondary: "Adicionar item de trabalho existente", + }, + modules: { + title: "Mapeie as metas do seu projeto para Módulos e acompanhe facilmente.", + description: + "Os módulos são compostos por itens de trabalho interconectados. Eles auxiliam no monitoramento do progresso através das fases do projeto, cada uma com prazos e análises específicas para indicar o quão perto você está de alcançar essas fases.", + cta_primary: "Definir seu primeiro módulo", + }, + module_work_items: { + title: "Nenhum item de trabalho para mostrar neste Módulo", + description: "Crie itens de trabalho para começar a monitorar este módulo.", + cta_primary: "Criar item de trabalho", + cta_secondary: "Adicionar item de trabalho existente", + }, + views: { + title: "Salve visualizações personalizadas para seu projeto", + description: + "As visualizações são filtros salvos que ajudam você a acessar rapidamente as informações que mais usa. Colabore sem esforço enquanto os colegas de equipe compartilham e adaptam as visualizações às suas necessidades específicas.", + cta_primary: "Criar visualização", + }, + no_work_items_in_project: { + title: "Ainda não há itens de trabalho no projeto", + description: + "Adicione itens de trabalho ao seu projeto e divida seu trabalho em partes rastreáveis com visualizações.", + cta_primary: "Adicionar item de trabalho", + }, + work_item_filter: { + title: "Nenhum item de trabalho encontrado", + description: "Seu filtro atual não retornou nenhum resultado. Tente alterar os filtros.", + cta_primary: "Adicionar item de trabalho", + }, + pages: { + title: "Documente tudo — de notas a PRDs", + description: + "As páginas permitem que você capture e organize informações em um só lugar. Escreva notas de reuniões, documentação de projetos e PRDs, incorpore itens de trabalho e estruture-os com componentes prontos para uso.", + cta_primary: "Criar sua primeira Página", + }, + archive_pages: { + title: "Ainda não há páginas arquivadas", + description: "Arquive páginas que não estão no seu radar. Acesse-as aqui quando necessário.", + }, + intake_sidebar: { + title: "Registrar solicitações de Entrada", + description: + "Envie novas solicitações para serem revisadas, priorizadas e rastreadas dentro do fluxo de trabalho do seu projeto.", + cta_primary: "Criar solicitação de Entrada", + }, + intake_main: { + title: "Selecione um item de trabalho de Entrada para ver seus detalhes", + }, + }, + workspace: { + archive_work_items: { + title: "Ainda não há itens de trabalho arquivados", + description: + "Manualmente ou por meio de automação, você pode arquivar itens de trabalho concluídos ou cancelados. Encontre-os aqui uma vez arquivados.", + cta_primary: "Configurar automação", + }, + archive_cycles: { + title: "Ainda não há ciclos arquivados", + description: "Para organizar seu projeto, arquive ciclos concluídos. Encontre-os aqui uma vez arquivados.", + }, + archive_modules: { + title: "Ainda não há Módulos arquivados", + description: + "Para organizar seu projeto, arquive módulos concluídos ou cancelados. Encontre-os aqui uma vez arquivados.", + }, + home_widget_quick_links: { + title: "Mantenha referências, recursos ou documentos importantes à mão para o seu trabalho", + }, + inbox_sidebar_all: { + title: "As atualizações dos seus itens de trabalho inscritos aparecerão aqui", + }, + inbox_sidebar_mentions: { + title: "As menções aos seus itens de trabalho aparecerão aqui", + }, + your_work_by_priority: { + title: "Ainda não há item de trabalho atribuído", + }, + your_work_by_state: { + title: "Ainda não há item de trabalho atribuído", + }, + views: { + title: "Ainda não há Visualizações", + description: + "Adicione itens de trabalho ao seu projeto e use visualizações para filtrar, classificar e monitorar o progresso sem esforço.", + cta_primary: "Adicionar item de trabalho", + }, + drafts: { + title: "Itens de trabalho semi-escritos", + description: + "Para experimentar isso, comece a adicionar um item de trabalho e deixe-o no meio do caminho ou crie seu primeiro rascunho abaixo. 😉", + cta_primary: "Criar item de trabalho de rascunho", + }, + projects_archived: { + title: "Nenhum projeto arquivado", + description: "Parece que todos os seus projetos ainda estão ativos — ótimo trabalho!", + }, + analytics_projects: { + title: "Crie projetos para visualizar as métricas do projeto aqui.", + }, + analytics_work_items: { + title: + "Crie projetos com itens de trabalho e responsáveis para começar a rastrear desempenho, progresso e impacto da equipe aqui.", + }, + analytics_no_cycle: { + title: "Crie ciclos para organizar o trabalho em fases com prazo definido e acompanhar o progresso em sprints.", + }, + analytics_no_module: { + title: "Crie módulos para organizar seu trabalho e acompanhar o progresso em diferentes estágios.", + }, + analytics_no_intake: { + title: "Configure a entrada para gerenciar solicitações recebidas e rastrear como elas são aceitas e rejeitadas", + }, + }, + settings: { + estimates: { + title: "Ainda não há estimativas", + description: + "Defina como sua equipe mede o esforço e acompanhe-o consistentemente em todos os itens de trabalho.", + cta_primary: "Adicionar sistema de estimativas", + }, + labels: { + title: "Ainda não há etiquetas", + description: "Crie etiquetas personalizadas para categorizar e gerenciar efetivamente seus itens de trabalho.", + cta_primary: "Criar sua primeira etiqueta", + }, + exports: { + title: "Ainda não há exportações", + description: + "Você não tem nenhum registro de exportação no momento. Depois de exportar dados, todos os registros aparecerão aqui.", + }, + tokens: { + title: "Ainda não há token Pessoal", + description: + "Gere tokens de API seguros para conectar seu espaço de trabalho com sistemas e aplicativos externos.", + cta_primary: "Adicionar token de API", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/ro/empty-state.ts b/packages/i18n/src/locales/ro/empty-state.ts new file mode 100644 index 000000000..71d2c3031 --- /dev/null +++ b/packages/i18n/src/locales/ro/empty-state.ts @@ -0,0 +1,184 @@ +export default { + common: { + progress: { + title: "Nu există încă metrici de progres de afișat.", + description: + "Începeți să setați valori de proprietăți în elementele de lucru pentru a vedea metricile de progres aici.", + }, + updates: { + title: "Încă nu există actualizări.", + description: "Odată ce membrii proiectului adaugă actualizări, acestea vor apărea aici", + }, + search: { + title: "Niciun rezultat corespunzător.", + description: "Nu s-au găsit rezultate. Încercați să ajustați termenii de căutare.", + }, + not_found: { + title: "Hopa! Se pare că ceva nu este în regulă", + description: "Nu putem accesa contul dvs. Plane în prezent. Aceasta ar putea fi o eroare de rețea.", + cta_primary: "Încercați reîncărcarea", + }, + server_error: { + title: "Eroare de server", + description: "Nu ne putem conecta și accesa datele de pe serverul nostru. Nu vă faceți griji, lucrăm la asta.", + cta_primary: "Încercați reîncărcarea", + }, + }, + project: { + work_items: { + title: "Începeți cu primul dvs. element de lucru.", + description: + "Elementele de lucru sunt blocurile de construcție ale proiectului dvs. — alocați proprietari, stabiliți priorități și urmăriți progresul cu ușurință.", + cta_primary: "Creați primul dvs. element de lucru", + }, + cycles: { + title: "Grupați și limitați în timp munca dvs. în Cicluri.", + description: + "Împărțiți munca în bucăți limitate în timp, lucrați înapoi de la termenul limită al proiectului pentru a stabili datele și faceți progrese tangibile ca echipă.", + cta_primary: "Setați primul dvs. ciclu", + }, + cycle_work_items: { + title: "Niciun element de lucru de afișat în acest ciclu", + description: + "Creați elemente de lucru pentru a începe monitorizarea progresului echipei dvs. în acest ciclu și pentru a-vă atinge obiectivele la timp.", + cta_primary: "Creați element de lucru", + cta_secondary: "Adăugați element de lucru existent", + }, + modules: { + title: "Mapați obiectivele proiectului dvs. la Module și urmăriți cu ușurință.", + description: + "Modulele sunt compuse din elemente de lucru interconectate. Acestea ajută la monitorizarea progresului prin fazele proiectului, fiecare cu termene limită și analize specifice pentru a indica cât de aproape sunteți de atingerea acelor faze.", + cta_primary: "Setați primul dvs. modul", + }, + module_work_items: { + title: "Niciun element de lucru de afișat în acest Modul", + description: "Creați elemente de lucru pentru a începe monitorizarea acestui modul.", + cta_primary: "Creați element de lucru", + cta_secondary: "Adăugați element de lucru existent", + }, + views: { + title: "Salvați vizualizări personalizate pentru proiectul dvs.", + description: + "Vizualizările sunt filtre salvate care vă ajută să accesați rapid informațiile pe care le utilizați cel mai mult. Colaborați fără efort pe măsură ce colegii de echipă partajează și personalizează vizualizările conform nevoilor lor specifice.", + cta_primary: "Creați vizualizare", + }, + no_work_items_in_project: { + title: "Încă nu există elemente de lucru în proiect", + description: + "Adăugați elemente de lucru la proiectul dvs. și împărțiți munca în bucăți urmăribile cu vizualizări.", + cta_primary: "Adăugați element de lucru", + }, + work_item_filter: { + title: "Nu s-au găsit elemente de lucru", + description: "Filtrul dvs. curent nu a returnat niciun rezultat. Încercați să modificați filtrele.", + cta_primary: "Adăugați element de lucru", + }, + pages: { + title: "Documentați totul — de la notițe la PRD-uri", + description: + "Paginile vă permit să capturați și să organizați informații într-un singur loc. Scrieți note de întâlnire, documentație de proiect și PRD-uri, încorporați elemente de lucru și structurați-le cu componente gata de utilizat.", + cta_primary: "Creați prima dvs. Pagină", + }, + archive_pages: { + title: "Încă nu există pagini arhivate", + description: "Arhivați paginile care nu sunt în radar-ul dvs. Accesați-le aici când este necesar.", + }, + intake_sidebar: { + title: "Înregistrați solicitări de Admitere", + description: + "Trimiteți solicitări noi pentru a fi revizuite, prioritizate și urmărite în cadrul fluxului de lucru al proiectului dvs.", + cta_primary: "Creați solicitare de Admitere", + }, + intake_main: { + title: "Selectați un element de lucru de Admitere pentru a vedea detaliile", + }, + }, + workspace: { + archive_work_items: { + title: "Încă nu există elemente de lucru arhivate", + description: + "Manual sau prin automatizare, puteți arhiva elemente de lucru finalizate sau anulate. Găsiți-le aici odată arhivate.", + cta_primary: "Configurați automatizarea", + }, + archive_cycles: { + title: "Încă nu există cicluri arhivate", + description: "Pentru a vă aranja proiectul, arhivați ciclurile finalizate. Găsiți-le aici odată arhivate.", + }, + archive_modules: { + title: "Încă nu există Module arhivate", + description: + "Pentru a vă aranja proiectul, arhivați modulele finalizate sau anulate. Găsiți-le aici odată arhivate.", + }, + home_widget_quick_links: { + title: "Păstrați referințe importante, resurse sau documente la îndemână pentru munca dvs.", + }, + inbox_sidebar_all: { + title: "Actualizările pentru elementele dvs. de lucru la care sunteți abonat vor apărea aici", + }, + inbox_sidebar_mentions: { + title: "Mențiunile pentru elementele dvs. de lucru vor apărea aici", + }, + your_work_by_priority: { + title: "Încă nu există elemente de lucru atribuite", + }, + your_work_by_state: { + title: "Încă nu există elemente de lucru atribuite", + }, + views: { + title: "Încă nu există Vizualizări", + description: + "Adăugați elemente de lucru la proiectul dvs. și utilizați vizualizări pentru a filtra, sorta și monitoriza progresul fără efort.", + cta_primary: "Adăugați element de lucru", + }, + drafts: { + title: "Elemente de lucru semi-scrise", + description: + "Pentru a încerca acest lucru, începeți să adăugați un element de lucru și lăsați-l nefinalizat sau creați prima dvs. schiță mai jos. 😉", + cta_primary: "Creați element de lucru schiță", + }, + projects_archived: { + title: "Niciun proiect arhivat", + description: "Se pare că toate proiectele dvs. sunt încă active — bună treabă!", + }, + analytics_projects: { + title: "Creați proiecte pentru a vizualiza metricile proiectului aici.", + }, + analytics_work_items: { + title: + "Creați proiecte cu elemente de lucru și responsabili pentru a începe urmărirea performanței, progresului și impactului echipei aici.", + }, + analytics_no_cycle: { + title: "Creați cicluri pentru a organiza munca în faze limitate în timp și a urmări progresul în sprint-uri.", + }, + analytics_no_module: { + title: "Creați module pentru a vă organiza munca și a urmări progresul în diferite etape.", + }, + analytics_no_intake: { + title: "Configurați admiterea pentru a gestiona solicitările primite și a urmări cum sunt acceptate și respinse", + }, + }, + settings: { + estimates: { + title: "Încă nu există estimări", + description: + "Definiți modul în care echipa dvs. măsoară efortul și urmăriți-l consecvent în toate elementele de lucru.", + cta_primary: "Adăugați sistem de estimări", + }, + labels: { + title: "Încă nu există etichete", + description: "Creați etichete personalizate pentru a categorisi și gestiona eficient elementele dvs. de lucru.", + cta_primary: "Creați prima dvs. etichetă", + }, + exports: { + title: "Încă nu există exporturi", + description: + "Nu aveți nicio înregistrare de export în acest moment. Odată ce exportați date, toate înregistrările vor apărea aici.", + }, + tokens: { + title: "Încă nu există token Personal", + description: + "Generați token-uri API sigure pentru a conecta spațiul dvs. de lucru cu sisteme și aplicații externe.", + cta_primary: "Adăugați token API", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/ru/empty-state.ts b/packages/i18n/src/locales/ru/empty-state.ts new file mode 100644 index 000000000..3bdc6d393 --- /dev/null +++ b/packages/i18n/src/locales/ru/empty-state.ts @@ -0,0 +1,187 @@ +export default { + common: { + progress: { + title: "Пока нет показателей прогресса для отображения.", + description: + "Начните устанавливать значения свойств в рабочих элементах, чтобы видеть показатели прогресса здесь.", + }, + updates: { + title: "Пока нет обновлений.", + description: "Когда участники проекта добавят обновления, они появятся здесь", + }, + search: { + title: "Не найдено совпадающих результатов.", + description: "Результаты не найдены. Попробуйте изменить условия поиска.", + }, + not_found: { + title: "Упс! Что-то пошло не так", + description: "Мы не можем получить вашу учетную запись Plane в данный момент. Возможно, это ошибка сети.", + cta_primary: "Попробовать перезагрузить", + }, + server_error: { + title: "Ошибка сервера", + description: "Мы не можем подключиться и получить данные с нашего сервера. Не волнуйтесь, мы работаем над этим.", + cta_primary: "Попробовать перезагрузить", + }, + }, + project: { + work_items: { + title: "Начните с вашего первого рабочего элемента.", + description: + "Рабочие элементы — это строительные блоки вашего проекта — назначайте ответственных, устанавливайте приоритеты и легко отслеживайте прогресс.", + cta_primary: "Создайте свой первый рабочий элемент", + }, + cycles: { + title: "Группируйте и ограничивайте по времени свою работу в Циклах.", + description: + "Разбивайте работу на временные блоки, работайте в обратном направлении от крайнего срока проекта для установки дат и добивайтесь ощутимого прогресса как команда.", + cta_primary: "Установите свой первый цикл", + }, + cycle_work_items: { + title: "Нет рабочих элементов для отображения в этом цикле", + description: + "Создайте рабочие элементы, чтобы начать отслеживать прогресс вашей команды в этом цикле и достичь целей вовремя.", + cta_primary: "Создать рабочий элемент", + cta_secondary: "Добавить существующий рабочий элемент", + }, + modules: { + title: "Сопоставьте цели проекта с Модулями и легко отслеживайте.", + description: + "Модули состоят из взаимосвязанных рабочих элементов. Они помогают отслеживать прогресс через фазы проекта, каждая из которых имеет конкретные сроки и аналитику, указывающую, насколько близко вы к достижению этих фаз.", + cta_primary: "Установите свой первый модуль", + }, + module_work_items: { + title: "Нет рабочих элементов для отображения в этом Модуле", + description: "Создайте рабочие элементы, чтобы начать отслеживать этот модуль.", + cta_primary: "Создать рабочий элемент", + cta_secondary: "Добавить существующий рабочий элемент", + }, + views: { + title: "Сохраните пользовательские представления для вашего проекта", + description: + "Представления — это сохраненные фильтры, которые помогают вам быстро получить доступ к информации, которую вы используете чаще всего. Сотрудничайте легко, когда товарищи по команде делятся и адаптируют представления к своим конкретным потребностям.", + cta_primary: "Создать представление", + }, + no_work_items_in_project: { + title: "Пока нет рабочих элементов в проекте", + description: + "Добавьте рабочие элементы в свой проект и разделите работу на отслеживаемые части с помощью представлений.", + cta_primary: "Добавить рабочий элемент", + }, + work_item_filter: { + title: "Рабочие элементы не найдены", + description: "Ваш текущий фильтр не вернул результатов. Попробуйте изменить фильтры.", + cta_primary: "Добавить рабочий элемент", + }, + pages: { + title: "Документируйте все — от заметок до PRD", + description: + "Страницы позволяют захватывать и организовывать информацию в одном месте. Пишите заметки о встречах, документацию проекта и PRD, встраивайте рабочие элементы и структурируйте их с помощью готовых компонентов.", + cta_primary: "Создайте свою первую Страницу", + }, + archive_pages: { + title: "Пока нет архивированных страниц", + description: + "Архивируйте страницы, которые не находятся в поле вашего зрения. Получите доступ к ним здесь при необходимости.", + }, + intake_sidebar: { + title: "Регистрируйте запросы на прием", + description: + "Отправляйте новые запросы для рассмотрения, приоритизации и отслеживания в рамках рабочего процесса вашего проекта.", + cta_primary: "Создать запрос на прием", + }, + intake_main: { + title: "Выберите рабочий элемент приема, чтобы просмотреть его детали", + }, + }, + workspace: { + archive_work_items: { + title: "Пока нет архивированных рабочих элементов", + description: + "Вручную или через автоматизацию вы можете архивировать завершенные или отмененные рабочие элементы. Найдите их здесь после архивирования.", + cta_primary: "Настроить автоматизацию", + }, + archive_cycles: { + title: "Пока нет архивированных циклов", + description: + "Чтобы привести в порядок ваш проект, архивируйте завершенные циклы. Найдите их здесь после архивирования.", + }, + archive_modules: { + title: "Пока нет архивированных Модулей", + description: + "Чтобы привести в порядок ваш проект, архивируйте завершенные или отмененные модули. Найдите их здесь после архивирования.", + }, + home_widget_quick_links: { + title: "Держите под рукой важные ссылки, ресурсы или документы для вашей работы", + }, + inbox_sidebar_all: { + title: "Обновления для ваших подписанных рабочих элементов появятся здесь", + }, + inbox_sidebar_mentions: { + title: "Упоминания ваших рабочих элементов появятся здесь", + }, + your_work_by_priority: { + title: "Пока не назначен рабочий элемент", + }, + your_work_by_state: { + title: "Пока не назначен рабочий элемент", + }, + views: { + title: "Пока нет Представлений", + description: + "Добавьте рабочие элементы в свой проект и используйте представления для фильтрации, сортировки и отслеживания прогресса без усилий.", + cta_primary: "Добавить рабочий элемент", + }, + drafts: { + title: "Недописанные рабочие элементы", + description: + "Чтобы попробовать это, начните добавлять рабочий элемент и оставьте его на полпути или создайте свой первый черновик ниже. 😉", + cta_primary: "Создать черновик рабочего элемента", + }, + projects_archived: { + title: "Нет архивированных проектов", + description: "Похоже, все ваши проекты все еще активны — отличная работа!", + }, + analytics_projects: { + title: "Создайте проекты для визуализации метрик проекта здесь.", + }, + analytics_work_items: { + title: + "Создайте проекты с рабочими элементами и ответственными, чтобы начать отслеживать производительность, прогресс и влияние команды здесь.", + }, + analytics_no_cycle: { + title: + "Создайте циклы для организации работы в ограниченные по времени фазы и отслеживания прогресса в спринтах.", + }, + analytics_no_module: { + title: "Создайте модули для организации работы и отслеживания прогресса на разных этапах.", + }, + analytics_no_intake: { + title: "Настройте прием для управления входящими запросами и отслеживания их принятия и отклонения", + }, + }, + settings: { + estimates: { + title: "Пока нет оценок", + description: + "Определите, как ваша команда измеряет усилия, и отслеживайте это последовательно во всех рабочих элементах.", + cta_primary: "Добавить систему оценок", + }, + labels: { + title: "Пока нет меток", + description: + "Создавайте персонализированные метки для эффективной категоризации и управления рабочими элементами.", + cta_primary: "Создайте свою первую метку", + }, + exports: { + title: "Пока нет экспортов", + description: "У вас пока нет записей экспорта. После экспорта данных все записи появятся здесь.", + }, + tokens: { + title: "Пока нет Личного токена", + description: + "Генерируйте безопасные API-токены для подключения вашего рабочего пространства к внешним системам и приложениям.", + cta_primary: "Добавить API-токен", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/sk/empty-state.ts b/packages/i18n/src/locales/sk/empty-state.ts new file mode 100644 index 000000000..8478fde4a --- /dev/null +++ b/packages/i18n/src/locales/sk/empty-state.ts @@ -0,0 +1,184 @@ +export default { + common: { + progress: { + title: "Zatiaľ nie sú k dispozícii žiadne metriky pokroku.", + description: "Začnite nastavovať hodnoty vlastností v pracovných položkách, aby ste tu videli metriky pokroku.", + }, + updates: { + title: "Zatiaľ žiadne aktualizácie.", + description: "Akonáhle členovia projektu pridajú aktualizácie, zobrazia sa tu", + }, + search: { + title: "Žiadne zodpovedajúce výsledky.", + description: "Neboli nájdené žiadne výsledky. Skúste upraviť vyhľadávacie výrazy.", + }, + not_found: { + title: "Ojej! Niečo sa zdá byť v neporiadku", + description: "Momentálne sa nám nedarí načítať váš účet plane. Môže ísť o chybu siete.", + cta_primary: "Skúste znovu načítať", + }, + server_error: { + title: "Chyba servera", + description: "Nemôžeme sa pripojiť a načítať údaje z nášho servera. Nebojte sa, pracujeme na tom.", + cta_primary: "Skúste znovu načítať", + }, + }, + project: { + work_items: { + title: "Začnite s vašou prvou pracovnou položkou.", + description: + "Pracovné položky sú stavebnými kameňmi vášho projektu — priraďujte vlastníkov, nastavujte priority a jednoducho sledujte pokrok.", + cta_primary: "Vytvorte svoju prvú pracovnú položku", + }, + cycles: { + title: "Zoskupujte a časovo obmedzte svoju prácu v cykloch.", + description: + "Rozdeľte prácu do časovo obmedzených blokov, pracujte spätne od termínu projektu na nastavenie dátumov a dosahujte hmatateľný pokrok ako tým.", + cta_primary: "Nastavte svoj prvý cyklus", + }, + cycle_work_items: { + title: "V tomto cykle nie sú žiadne pracovné položky na zobrazenie", + description: + "Vytvorte pracovné položky na začatie sledovania pokroku vášho tímu v tomto cykle a dosiahnutie vašich cieľov včas.", + cta_primary: "Vytvoriť pracovnú položku", + cta_secondary: "Pridať existujúcu pracovnú položku", + }, + modules: { + title: "Namapujte ciele vášho projektu na moduly a jednoducho sledujte.", + description: + "Moduly sa skladajú z prepojených pracovných položiek. Pomáhajú sledovať pokrok prostredníctvom fáz projektu, z ktorých každá má špecifické termíny a analytiku, ktorá ukazuje, ako blízko ste dosiahnutiu týchto fáz.", + cta_primary: "Nastavte svoj prvý modul", + }, + module_work_items: { + title: "V tomto module nie sú žiadne pracovné položky na zobrazenie", + description: "Vytvorte pracovné položky na začatie sledovania tohto modulu.", + cta_primary: "Vytvoriť pracovnú položku", + cta_secondary: "Pridať existujúcu pracovnú položku", + }, + views: { + title: "Uložte vlastné pohľady pre váš projekt", + description: + "Pohľady sú uložené filtre, ktoré vám pomáhajú rýchlo pristupovať k informáciám, ktoré používate najčastejšie. Spolupracujte bez námahy, zatiaľ čo spolupracovníci zdieľajú a prispôsobujú pohľady svojim špecifickým potrebám.", + cta_primary: "Vytvoriť pohľad", + }, + no_work_items_in_project: { + title: "V projekte zatiaľ nie sú žiadne pracovné položky", + description: + "Pridajte pracovné položky do svojho projektu a rozdeľte svoju prácu na sledovateľné časti pomocou pohľadov.", + cta_primary: "Pridať pracovnú položku", + }, + work_item_filter: { + title: "Neboli nájdené žiadne pracovné položky", + description: "Váš aktuálny filter nevrátil žiadne výsledky. Skúste zmeniť filtre.", + cta_primary: "Pridať pracovnú položku", + }, + pages: { + title: "Dokumentujte všetko — od poznámok po PRD", + description: + "Stránky vám umožňujú zachytiť a organizovať informácie na jednom mieste. Píšte poznámky zo stretnutí, projektovú dokumentáciu a PRD, vkladajte pracovné položky a štruktúrujte ich pomocou pripravených komponentov.", + cta_primary: "Vytvorte svoju prvú stránku", + }, + archive_pages: { + title: "Zatiaľ žiadne archivované stránky", + description: "Archivujte stránky, ktoré nie sú na vašom radare. Pristúpte k nim tu, keď budete potrebovať.", + }, + intake_sidebar: { + title: "Zaznamenajte príchodzí požiadavky", + description: + "Odosielajte nové požiadavky na preskúmanie, stanovenie priorít a sledovanie v rámci pracovného postupu vášho projektu.", + cta_primary: "Vytvoriť príchodzí požiadavku", + }, + intake_main: { + title: "Vyberte príchodzí pracovnú položku na zobrazenie jej podrobností", + }, + }, + workspace: { + archive_work_items: { + title: "Zatiaľ žiadne archivované pracovné položky", + description: + "Ručne alebo pomocou automatizácie môžete archivovať dokončené alebo zrušené pracovné položky. Nájdete ich tu, akonáhle budú archivované.", + cta_primary: "Nastaviť automatizáciu", + }, + archive_cycles: { + title: "Zatiaľ žiadne archivované cykly", + description: + "Pre upratanie vášho projektu archivujte dokončené cykly. Nájdete ich tu, akonáhle budú archivované.", + }, + archive_modules: { + title: "Zatiaľ žiadne archivované moduly", + description: + "Pre upratanie vášho projektu archivujte dokončené alebo zrušené moduly. Nájdete ich tu, akonáhle budú archivované.", + }, + home_widget_quick_links: { + title: "Majte po ruke dôležité odkazy, zdroje alebo dokumenty pre vašu prácu", + }, + inbox_sidebar_all: { + title: "Aktualizácie pre vaše odoberané pracovné položky sa zobrazia tu", + }, + inbox_sidebar_mentions: { + title: "Zmienky o vašich pracovných položkách sa zobrazia tu", + }, + your_work_by_priority: { + title: "Zatiaľ nie je priradená žiadna pracovná položka", + }, + your_work_by_state: { + title: "Zatiaľ nie je priradená žiadna pracovná položka", + }, + views: { + title: "Zatiaľ žiadne pohľady", + description: + "Pridajte pracovné položky do svojho projektu a používajte pohľady na jednoduché filtrovanie, triedenie a sledovanie pokroku.", + cta_primary: "Pridať pracovnú položku", + }, + drafts: { + title: "Napoly napísané pracovné položky", + description: + "Ak to chcete vyskúšať, začnite pridávať pracovnú položku a nechajte ju nedokončenú alebo vytvorte svoj prvý koncept nižšie. 😉", + cta_primary: "Vytvoriť koncept pracovnej položky", + }, + projects_archived: { + title: "Žiadne archivované projekty", + description: "Vyzerá to, že všetky vaše projekty sú stále aktívne—skvelá práca!", + }, + analytics_projects: { + title: "Vytvorte projekty na vizualizáciu metrík projektu tu.", + }, + analytics_work_items: { + title: + "Vytvorte projekty s pracovnými položkami a priradenými osobami na začatie sledovania výkonu, pokroku a dopadu tímu tu.", + }, + analytics_no_cycle: { + title: "Vytvorte cykly na organizáciu práce do časovo obmedzených fáz a sledovanie pokroku naprieč šprintmi.", + }, + analytics_no_module: { + title: "Vytvorte moduly na organizáciu svojej práce a sledovanie pokroku naprieč rôznymi fázami.", + }, + analytics_no_intake: { + title: "Nastavte príjem na správu prichádzajúcich požiadaviek a sledovanie, ako sú prijímané a odmietané", + }, + }, + settings: { + estimates: { + title: "Zatiaľ žiadne odhady", + description: + "Definujte, ako váš tím meria úsilie, a sledujte to konzistentne naprieč všetkými pracovnými položkami.", + cta_primary: "Pridať systém odhadov", + }, + labels: { + title: "Zatiaľ žiadne štítky", + description: "Vytvorte personalizované štítky na efektívnu kategorizáciu a správu vašich pracovných položiek.", + cta_primary: "Vytvorte svoj prvý štítok", + }, + exports: { + title: "Zatiaľ žiadne exporty", + description: + "Momentálne nemáte žiadne záznamy exportu. Akonáhle exportujete údaje, všetky záznamy sa zobrazia tu.", + }, + tokens: { + title: "Zatiaľ žiadny osobný token", + description: + "Generujte bezpečné API tokeny na pripojenie vášho pracovného priestoru s externými systémami a aplikáciami.", + cta_primary: "Pridať API token", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/tr-TR/empty-state.ts b/packages/i18n/src/locales/tr-TR/empty-state.ts new file mode 100644 index 000000000..a1cb6296d --- /dev/null +++ b/packages/i18n/src/locales/tr-TR/empty-state.ts @@ -0,0 +1,183 @@ +export default { + common: { + progress: { + title: "Henüz gösterilecek ilerleme metriği yok.", + description: "İlerleme metriklerini burada görmek için iş öğelerinde özellik değerleri belirlemeye başlayın.", + }, + updates: { + title: "Henüz güncelleme yok.", + description: "Proje üyeleri güncelleme eklediğinde burada görünecek", + }, + search: { + title: "Eşleşen sonuç yok.", + description: "Sonuç bulunamadı. Arama terimlerinizi ayarlamayı deneyin.", + }, + not_found: { + title: "Hata! Bir şeyler ters gitti", + description: "Şu anda Plane hesabınızı alamıyoruz. Bu bir ağ hatası olabilir.", + cta_primary: "Yeniden yüklemeyi dene", + }, + server_error: { + title: "Sunucu hatası", + description: "Sunucumuza bağlanamıyor ve veri alamıyoruz. Endişelenmeyin, üzerinde çalışıyoruz.", + cta_primary: "Yeniden yüklemeyi dene", + }, + }, + project: { + work_items: { + title: "İlk iş öğenizle başlayın.", + description: + "İş öğeleri projenizin yapı taşlarıdır — sahipler atayın, öncelikleri belirleyin ve ilerlemeyi kolayca takip edin.", + cta_primary: "İlk iş öğenizi oluşturun", + }, + cycles: { + title: "Çalışmanızı Döngülerde gruplayın ve zaman sınırlayın.", + description: + "Çalışmayı zaman sınırlı parçalara bölün, tarihleri belirlemek için proje son tarihinden geriye doğru çalışın ve bir ekip olarak somut ilerleme kaydedin.", + cta_primary: "İlk döngünüzü ayarlayın", + }, + cycle_work_items: { + title: "Bu döngüde gösterilecek iş öğesi yok", + description: + "Ekibinizin bu döngüdeki ilerlemesini izlemeye başlamak ve hedeflerinize zamanında ulaşmak için iş öğeleri oluşturun.", + cta_primary: "İş öğesi oluştur", + cta_secondary: "Mevcut iş öğesini ekle", + }, + modules: { + title: "Proje hedeflerinizi Modüllere eşleyin ve kolayca takip edin.", + description: + "Modüller birbirine bağlı iş öğelerinden oluşur. Proje aşamalarındaki ilerlemeyi izlemeye yardımcı olurlar, her biri bu aşamalara ne kadar yakın olduğunuzu göstermek için belirli son tarihler ve analizlerle.", + cta_primary: "İlk modülünüzü ayarlayın", + }, + module_work_items: { + title: "Bu Modülde gösterilecek iş öğesi yok", + description: "Bu modülü izlemeye başlamak için iş öğeleri oluşturun.", + cta_primary: "İş öğesi oluştur", + cta_secondary: "Mevcut iş öğesini ekle", + }, + views: { + title: "Projeniz için özel görünümler kaydedin", + description: + "Görünümler, en çok kullandığınız bilgilere hızlı erişmenize yardımcı olan kaydedilmiş filtrelerdir. Ekip arkadaşları görünümleri paylaşıp kendi özel ihtiyaçlarına göre uyarladıkça zahmetsizce işbirliği yapın.", + cta_primary: "Görünüm oluştur", + }, + no_work_items_in_project: { + title: "Projede henüz iş öğesi yok", + description: "Projenize iş öğeleri ekleyin ve çalışmanızı görünümlerle takip edilebilir parçalara ayırın.", + cta_primary: "İş öğesi ekle", + }, + work_item_filter: { + title: "İş öğesi bulunamadı", + description: "Mevcut filtreniz hiçbir sonuç döndürmedi. Filtreleri değiştirmeyi deneyin.", + cta_primary: "İş öğesi ekle", + }, + pages: { + title: "Her şeyi belgeleyin — notlardan PRD'lere", + description: + "Sayfalar bilgileri tek bir yerde yakalamanıza ve düzenlemenize olanak tanır. Toplantı notları, proje belgeleri ve PRD'ler yazın, iş öğelerini yerleştirin ve kullanıma hazır bileşenlerle yapılandırın.", + cta_primary: "İlk Sayfanızı oluşturun", + }, + archive_pages: { + title: "Henüz arşivlenmiş sayfa yok", + description: "Radarınızda olmayan sayfaları arşivleyin. Gerektiğinde buradan erişin.", + }, + intake_sidebar: { + title: "Giriş isteklerini kaydedin", + description: + "Projenizin iş akışı içinde incelenmek, önceliklendirilmek ve takip edilmek üzere yeni istekler gönderin.", + cta_primary: "Giriş isteği oluştur", + }, + intake_main: { + title: "Ayrıntılarını görmek için bir Giriş iş öğesi seçin", + }, + }, + workspace: { + archive_work_items: { + title: "Henüz arşivlenmiş iş öğesi yok", + description: + "Manuel veya otomasyon yoluyla tamamlanmış veya iptal edilmiş iş öğelerini arşivleyebilirsiniz. Arşivlendikten sonra burada bulun.", + cta_primary: "Otomasyonu ayarla", + }, + archive_cycles: { + title: "Henüz arşivlenmiş döngü yok", + description: "Projenizi düzenlemek için tamamlanmış döngüleri arşivleyin. Arşivlendikten sonra burada bulun.", + }, + archive_modules: { + title: "Henüz arşivlenmiş Modül yok", + description: + "Projenizi düzenlemek için tamamlanmış veya iptal edilmiş modülleri arşivleyin. Arşivlendikten sonra burada bulun.", + }, + home_widget_quick_links: { + title: "Çalışmanız için önemli referansları, kaynakları veya belgeleri elinizin altında tutun", + }, + inbox_sidebar_all: { + title: "Abone olduğunuz iş öğeleri için güncellemeler burada görünecek", + }, + inbox_sidebar_mentions: { + title: "İş öğeleriniz için bahsetmeler burada görünecek", + }, + your_work_by_priority: { + title: "Henüz atanmış iş öğesi yok", + }, + your_work_by_state: { + title: "Henüz atanmış iş öğesi yok", + }, + views: { + title: "Henüz Görünüm yok", + description: + "Projenize iş öğeleri ekleyin ve zahmetsizce filtrelemek, sıralamak ve ilerlemeyi izlemek için görünümleri kullanın.", + cta_primary: "İş öğesi ekle", + }, + drafts: { + title: "Yarım yazılmış iş öğeleri", + description: + "Bunu denemek için bir iş öğesi eklemeye başlayın ve yarıda bırakın veya aşağıda ilk taslağınızı oluşturun. 😉", + cta_primary: "Taslak iş öğesi oluştur", + }, + projects_archived: { + title: "Arşivlenmiş proje yok", + description: "Görünüşe göre tüm projeleriniz hala aktif—harika iş!", + }, + analytics_projects: { + title: "Proje metriklerini burada görselleştirmek için projeler oluşturun.", + }, + analytics_work_items: { + title: + "Performansı, ilerlemeyi ve ekip etkisini burada izlemeye başlamak için iş öğeleri ve atananlar içeren projeler oluşturun.", + }, + analytics_no_cycle: { + title: + "Çalışmayı zaman sınırlı aşamalara organize etmek ve sprintler boyunca ilerlemeyi takip etmek için döngüler oluşturun.", + }, + analytics_no_module: { + title: "Çalışmanızı organize etmek ve farklı aşamalarda ilerlemeyi takip etmek için modüller oluşturun.", + }, + analytics_no_intake: { + title: + "Gelen istekleri yönetmek ve bunların nasıl kabul edildiğini ve reddedildiğini izlemek için giriş ayarlayın", + }, + }, + settings: { + estimates: { + title: "Henüz tahmin yok", + description: "Ekibinizin çabayı nasıl ölçtüğünü tanımlayın ve tüm iş öğelerinde tutarlı bir şekilde takip edin.", + cta_primary: "Tahmin sistemi ekle", + }, + labels: { + title: "Henüz etiket yok", + description: + "İş öğelerinizi etkili bir şekilde kategorize etmek ve yönetmek için kişiselleştirilmiş etiketler oluşturun.", + cta_primary: "İlk etiketinizi oluşturun", + }, + exports: { + title: "Henüz dışa aktarma yok", + description: "Şu anda hiç dışa aktarma kaydınız yok. Verileri dışa aktardığınızda tüm kayıtlar burada görünecek.", + }, + tokens: { + title: "Henüz Kişisel token yok", + description: + "Çalışma alanınızı harici sistemler ve uygulamalarla bağlamak için güvenli API token'ları oluşturun.", + cta_primary: "API token'ı ekle", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/ua/empty-state.ts b/packages/i18n/src/locales/ua/empty-state.ts new file mode 100644 index 000000000..12cc622e2 --- /dev/null +++ b/packages/i18n/src/locales/ua/empty-state.ts @@ -0,0 +1,186 @@ +export default { + common: { + progress: { + title: "Ще немає метрик прогресу для відображення.", + description: + "Почніть встановлювати значення властивостей у робочих елементах, щоб побачити тут метрики прогресу.", + }, + updates: { + title: "Ще немає оновлень.", + description: "Як тільки учасники проєкту додадуть оновлення, вони з'являться тут", + }, + search: { + title: "Немає відповідних результатів.", + description: "Результатів не знайдено. Спробуйте змінити пошукові терміни.", + }, + not_found: { + title: "Ой! Щось здається не так", + description: "Ми не можемо отримати ваш обліковий запис plane зараз. Це може бути помилка мережі.", + cta_primary: "Спробуйте перезавантажити", + }, + server_error: { + title: "Помилка сервера", + description: "Ми не можемо підключитися та отримати дані з нашого сервера. Не хвилюйтеся, ми працюємо над цим.", + cta_primary: "Спробуйте перезавантажити", + }, + }, + project: { + work_items: { + title: "Почніть з вашого першого робочого елемента.", + description: + "Робочі елементи є будівельними блоками вашого проєкту — призначайте власників, встановлюйте пріоритети та легко відстежуйте прогрес.", + cta_primary: "Створіть свій перший робочий елемент", + }, + cycles: { + title: "Групуйте та обмежуйте за часом вашу роботу в Циклах.", + description: + "Розділіть роботу на часові блоки, працюйте назад від кінцевого терміну проєкту для встановлення дат і робіть відчутний прогрес як команда.", + cta_primary: "Встановіть свій перший цикл", + }, + cycle_work_items: { + title: "Немає робочих елементів для відображення в цьому циклі", + description: + "Створіть робочі елементи, щоб почати моніторинг прогресу вашої команди в цьому циклі та досягти своїх цілей вчасно.", + cta_primary: "Створити робочий елемент", + cta_secondary: "Додати існуючий робочий елемент", + }, + modules: { + title: "Відобразіть цілі вашого проєкту на Модулі та легко відстежуйте.", + description: + "Модулі складаються з взаємопов'язаних робочих елементів. Вони допомагають моніторити прогрес через фази проєкту, кожна з конкретними термінами та аналітикою, щоб показати, наскільки ви близькі до досягнення цих фаз.", + cta_primary: "Встановіть свій перший модуль", + }, + module_work_items: { + title: "Немає робочих елементів для відображення в цьому Модулі", + description: "Створіть робочі елементи, щоб почати моніторинг цього модуля.", + cta_primary: "Створити робочий елемент", + cta_secondary: "Додати існуючий робочий елемент", + }, + views: { + title: "Зберігайте власні подання для вашого проєкту", + description: + "Подання є збереженими фільтрами, які допомагають вам швидко отримувати доступ до інформації, яку ви використовуєте найчастіше. Співпрацюйте без зусиль, оскільки члени команди діляться та адаптують подання до своїх конкретних потреб.", + cta_primary: "Створити подання", + }, + no_work_items_in_project: { + title: "У проєкті ще немає робочих елементів", + description: + "Додайте робочі елементи до свого проєкту та розділіть свою роботу на відстежувані частини з поданнями.", + cta_primary: "Додати робочий елемент", + }, + work_item_filter: { + title: "Робочих елементів не знайдено", + description: "Ваш поточний фільтр не повернув результатів. Спробуйте змінити фільтри.", + cta_primary: "Додати робочий елемент", + }, + pages: { + title: "Документуйте все — від нотаток до PRD", + description: + "Сторінки дозволяють вам захоплювати та організовувати інформацію в одному місці. Пишіть нотатки зустрічей, проєктну документацію та PRD, вбудовуйте робочі елементи та структуруйте їх за допомогою готових компонентів.", + cta_primary: "Створіть свою першу Сторінку", + }, + archive_pages: { + title: "Ще немає архівованих сторінок", + description: "Архівуйте сторінки, які не на вашому радарі. Отримуйте до них доступ тут, коли потрібно.", + }, + intake_sidebar: { + title: "Реєструйте запити на вхід", + description: + "Надсилайте нові запити для перегляду, встановлення пріоритетів та відстеження в рамках робочого процесу вашого проєкту.", + cta_primary: "Створити запит на вхід", + }, + intake_main: { + title: "Виберіть робочий елемент Intake, щоб переглянути його деталі", + }, + }, + workspace: { + archive_work_items: { + title: "Ще немає архівованих робочих елементів", + description: + "Вручну або через автоматизацію ви можете архівувати завершені або скасовані робочі елементи. Знайдіть їх тут після архівування.", + cta_primary: "Налаштувати автоматизацію", + }, + archive_cycles: { + title: "Ще немає архівованих циклів", + description: "Щоб упорядкувати свій проєкт, архівуйте завершені цикли. Знайдіть їх тут після архівування.", + }, + archive_modules: { + title: "Ще немає архівованих Модулів", + description: + "Щоб упорядкувати свій проєкт, архівуйте завершені або скасовані модулі. Знайдіть їх тут після архівування.", + }, + home_widget_quick_links: { + title: "Тримайте під рукою важливі посилання, ресурси або документи для вашої роботи", + }, + inbox_sidebar_all: { + title: "Оновлення для ваших підписаних робочих елементів з'являться тут", + }, + inbox_sidebar_mentions: { + title: "Згадки для ваших робочих елементів з'являться тут", + }, + your_work_by_priority: { + title: "Ще не призначено робочого елемента", + }, + your_work_by_state: { + title: "Ще не призначено робочого елемента", + }, + views: { + title: "Ще немає Поданнь", + description: + "Додайте робочі елементи до свого проєкту та використовуйте подання для фільтрування, сортування та моніторингу прогресу без зусиль.", + cta_primary: "Додати робочий елемент", + }, + drafts: { + title: "Напівнаписані робочі елементи", + description: + "Щоб спробувати це, почніть додавати робочий елемент і залиште його на половині або створіть свій перший чернетка нижче. 😉", + cta_primary: "Створити чернетковий робочий елемент", + }, + projects_archived: { + title: "Немає архівованих проєктів", + description: "Схоже, всі ваші проєкти все ще активні—чудова робота!", + }, + analytics_projects: { + title: "Створіть проєкти для візуалізації метрик проєкту тут.", + }, + analytics_work_items: { + title: + "Створіть проєкти з робочими елементами та призначеними особами, щоб почати відстежувати ефективність, прогрес та вплив команди тут.", + }, + analytics_no_cycle: { + title: "Створіть цикли для організації роботи в часові фази та відстеження прогресу через спринти.", + }, + analytics_no_module: { + title: "Створіть модулі для організації вашої роботи та відстеження прогресу через різні фази.", + }, + analytics_no_intake: { + title: + "Налаштуйте вхід для управління вхідними запитами та відстеження того, як вони приймаються та відхиляються", + }, + }, + settings: { + estimates: { + title: "Ще немає оцінок", + description: + "Визначте, як ваша команда вимірює зусилля, та відстежуйте це послідовно для всіх робочих елементів.", + cta_primary: "Додати систему оцінок", + }, + labels: { + title: "Ще немає міток", + description: + "Створіть персоналізовані мітки для ефективної категоризації та управління вашими робочими елементами.", + cta_primary: "Створіть свою першу мітку", + }, + exports: { + title: "Ще немає експортів", + description: + "Ви зараз не маєте жодних записів експорту. Як тільки ви експортуєте дані, усі записи з'являться тут.", + }, + tokens: { + title: "Ще немає Персонального токена", + description: + "Генеруйте безпечні API токени для підключення вашого робочого простору до зовнішніх систем та додатків.", + cta_primary: "Додати API токен", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/vi-VN/empty-state.ts b/packages/i18n/src/locales/vi-VN/empty-state.ts new file mode 100644 index 000000000..6899c509e --- /dev/null +++ b/packages/i18n/src/locales/vi-VN/empty-state.ts @@ -0,0 +1,184 @@ +export default { + common: { + progress: { + title: "Chưa có số liệu tiến độ để hiển thị.", + description: "Bắt đầu đặt giá trị thuộc tính trong các mục công việc để xem số liệu tiến độ ở đây.", + }, + updates: { + title: "Chưa có cập nhật.", + description: "Khi thành viên dự án thêm cập nhật, nó sẽ xuất hiện ở đây", + }, + search: { + title: "Không có kết quả phù hợp.", + description: "Không tìm thấy kết quả. Hãy thử điều chỉnh các từ khóa tìm kiếm.", + }, + not_found: { + title: "Rất tiếc! Có vẻ như có gì đó không ổn", + description: "Chúng tôi không thể tải tài khoản Plane của bạn hiện tại. Đây có thể là lỗi mạng.", + cta_primary: "Thử tải lại", + }, + server_error: { + title: "Lỗi máy chủ", + description: + "Chúng tôi không thể kết nối và lấy dữ liệu từ máy chủ của chúng tôi. Đừng lo lắng, chúng tôi đang khắc phục.", + cta_primary: "Thử tải lại", + }, + }, + project: { + work_items: { + title: "Bắt đầu với mục công việc đầu tiên của bạn.", + description: + "Các mục công việc là những khối xây dựng của dự án của bạn — chỉ định người sở hữu, đặt mức độ ưu tiên và theo dõi tiến độ dễ dàng.", + cta_primary: "Tạo mục công việc đầu tiên của bạn", + }, + cycles: { + title: "Nhóm và giới hạn thời gian công việc của bạn trong Chu kỳ.", + description: + "Chia nhỏ công việc thành các phần có giới hạn thời gian, làm ngược từ thời hạn dự án để đặt ngày và tạo tiến triển cụ thể như một đội.", + cta_primary: "Đặt chu kỳ đầu tiên của bạn", + }, + cycle_work_items: { + title: "Không có mục công việc để hiển thị trong chu kỳ này", + description: + "Tạo các mục công việc để bắt đầu giám sát tiến độ của đội bạn trong chu kỳ này và đạt được mục tiêu đúng hạn.", + cta_primary: "Tạo mục công việc", + cta_secondary: "Thêm mục công việc hiện có", + }, + modules: { + title: "Ánh xạ mục tiêu dự án của bạn vào Mô-đun và theo dõi dễ dàng.", + description: + "Các mô-đun được tạo thành từ các mục công việc kết nối với nhau. Chúng hỗ trợ theo dõi tiến độ qua các giai đoạn dự án, mỗi giai đoạn có thời hạn và phân tích cụ thể để chỉ ra bạn gần đạt được các giai đoạn đó như thế nào.", + cta_primary: "Đặt mô-đun đầu tiên của bạn", + }, + module_work_items: { + title: "Không có mục công việc để hiển thị trong Mô-đun này", + description: "Tạo các mục công việc để bắt đầu giám sát mô-đun này.", + cta_primary: "Tạo mục công việc", + cta_secondary: "Thêm mục công việc hiện có", + }, + views: { + title: "Lưu chế độ xem tùy chỉnh cho dự án của bạn", + description: + "Chế độ xem là các bộ lọc đã lưu giúp bạn truy cập nhanh chóng thông tin bạn sử dụng nhiều nhất. Cộng tác dễ dàng khi các đồng đội chia sẻ và điều chỉnh chế độ xem theo nhu cầu cụ thể của họ.", + cta_primary: "Tạo chế độ xem", + }, + no_work_items_in_project: { + title: "Chưa có mục công việc trong dự án", + description: + "Thêm các mục công việc vào dự án của bạn và chia nhỏ công việc thành các phần có thể theo dõi với chế độ xem.", + cta_primary: "Thêm mục công việc", + }, + work_item_filter: { + title: "Không tìm thấy mục công việc", + description: "Bộ lọc hiện tại của bạn không trả về kết quả nào. Hãy thử thay đổi bộ lọc.", + cta_primary: "Thêm mục công việc", + }, + pages: { + title: "Ghi chép mọi thứ — từ ghi chú đến PRD", + description: + "Các trang cho phép bạn ghi lại và tổ chức thông tin ở một nơi. Viết ghi chú cuộc họp, tài liệu dự án và PRD, nhúng các mục công việc và cấu trúc chúng với các thành phần sẵn sàng sử dụng.", + cta_primary: "Tạo Trang đầu tiên của bạn", + }, + archive_pages: { + title: "Chưa có trang được lưu trữ", + description: "Lưu trữ các trang không nằm trong tầm quan sát của bạn. Truy cập chúng ở đây khi cần.", + }, + intake_sidebar: { + title: "Ghi lại yêu cầu Tiếp nhận", + description: "Gửi các yêu cầu mới để được xem xét, ưu tiên và theo dõi trong quy trình làm việc của dự án.", + cta_primary: "Tạo yêu cầu Tiếp nhận", + }, + intake_main: { + title: "Chọn một mục công việc Tiếp nhận để xem chi tiết của nó", + }, + }, + workspace: { + archive_work_items: { + title: "Chưa có mục công việc được lưu trữ", + description: + "Thủ công hoặc thông qua tự động hóa, bạn có thể lưu trữ các mục công việc đã hoàn thành hoặc bị hủy. Tìm chúng ở đây sau khi lưu trữ.", + cta_primary: "Thiết lập tự động hóa", + }, + archive_cycles: { + title: "Chưa có chu kỳ được lưu trữ", + description: "Để sắp xếp dự án của bạn, hãy lưu trữ các chu kỳ đã hoàn thành. Tìm chúng ở đây sau khi lưu trữ.", + }, + archive_modules: { + title: "Chưa có Mô-đun được lưu trữ", + description: + "Để sắp xếp dự án của bạn, hãy lưu trữ các mô-đun đã hoàn thành hoặc bị hủy. Tìm chúng ở đây sau khi lưu trữ.", + }, + home_widget_quick_links: { + title: "Giữ các tài liệu tham khảo, tài nguyên hoặc tài liệu quan trọng tiện lợi cho công việc của bạn", + }, + inbox_sidebar_all: { + title: "Cập nhật cho các mục công việc bạn đăng ký sẽ xuất hiện ở đây", + }, + inbox_sidebar_mentions: { + title: "Đề cập cho các mục công việc của bạn sẽ xuất hiện ở đây", + }, + your_work_by_priority: { + title: "Chưa có mục công việc được giao", + }, + your_work_by_state: { + title: "Chưa có mục công việc được giao", + }, + views: { + title: "Chưa có Chế độ xem", + description: + "Thêm các mục công việc vào dự án của bạn và sử dụng chế độ xem để lọc, sắp xếp và giám sát tiến độ dễ dàng.", + cta_primary: "Thêm mục công việc", + }, + drafts: { + title: "Các mục công việc viết dở", + description: + "Để thử điều này, hãy bắt đầu thêm một mục công việc và để nó ở giữa chừng hoặc tạo bản nháp đầu tiên của bạn bên dưới. 😉", + cta_primary: "Tạo mục công việc nháp", + }, + projects_archived: { + title: "Không có dự án được lưu trữ", + description: "Có vẻ như tất cả các dự án của bạn vẫn đang hoạt động—làm tốt lắm!", + }, + analytics_projects: { + title: "Tạo dự án để trực quan hóa số liệu dự án ở đây.", + }, + analytics_work_items: { + title: + "Tạo dự án với các mục công việc và người được giao để bắt đầu theo dõi hiệu suất, tiến độ và tác động của đội ở đây.", + }, + analytics_no_cycle: { + title: + "Tạo chu kỳ để tổ chức công việc thành các giai đoạn có giới hạn thời gian và theo dõi tiến độ qua các sprint.", + }, + analytics_no_module: { + title: "Tạo mô-đun để tổ chức công việc của bạn và theo dõi tiến độ qua các giai đoạn khác nhau.", + }, + analytics_no_intake: { + title: "Thiết lập tiếp nhận để quản lý các yêu cầu đến và theo dõi cách chúng được chấp nhận và từ chối", + }, + }, + settings: { + estimates: { + title: "Chưa có ước tính", + description: + "Xác định cách đội của bạn đo lường nỗ lực và theo dõi nó một cách nhất quán trên tất cả các mục công việc.", + cta_primary: "Thêm hệ thống ước tính", + }, + labels: { + title: "Chưa có nhãn", + description: "Tạo nhãn cá nhân hóa để phân loại và quản lý các mục công việc của bạn một cách hiệu quả.", + cta_primary: "Tạo nhãn đầu tiên của bạn", + }, + exports: { + title: "Chưa có xuất khẩu", + description: + "Bạn chưa có bản ghi xuất khẩu nào ngay bây giờ. Sau khi bạn xuất dữ liệu, tất cả các bản ghi sẽ xuất hiện ở đây.", + }, + tokens: { + title: "Chưa có token Cá nhân", + description: + "Tạo token API an toàn để kết nối không gian làm việc của bạn với các hệ thống và ứng dụng bên ngoài.", + cta_primary: "Thêm token API", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/zh-CN/empty-state.ts b/packages/i18n/src/locales/zh-CN/empty-state.ts new file mode 100644 index 000000000..7d0cfb531 --- /dev/null +++ b/packages/i18n/src/locales/zh-CN/empty-state.ts @@ -0,0 +1,170 @@ +export default { + common: { + progress: { + title: "暂无进度指标可显示。", + description: "开始在工作项中设置属性值以在此查看进度指标。", + }, + updates: { + title: "暂无更新。", + description: "项目成员添加更新后将显示在此处", + }, + search: { + title: "未找到匹配结果。", + description: "未找到结果。请尝试调整搜索条件。", + }, + not_found: { + title: "糟糕!似乎出了点问题", + description: "我们目前无法获取您的 Plane 账户。这可能是网络错误。", + cta_primary: "尝试重新加载", + }, + server_error: { + title: "服务器错误", + description: "我们无法连接并从服务器获取数据。请放心,我们正在处理。", + cta_primary: "尝试重新加载", + }, + }, + project: { + work_items: { + title: "从您的第一个工作项开始。", + description: "工作项是项目的构建块 — 分配负责人、设置优先级并轻松跟踪进度。", + cta_primary: "创建您的第一个工作项", + }, + cycles: { + title: "在周期中分组和限时您的工作。", + description: "将工作分解为限时块,从项目截止日期倒推设置日期,并作为团队取得实质性进展。", + cta_primary: "设置您的第一个周期", + }, + cycle_work_items: { + title: "此周期中没有要显示的工作项", + description: "创建工作项以开始监控团队在此周期中的进度并按时实现目标。", + cta_primary: "创建工作项", + cta_secondary: "添加现有工作项", + }, + modules: { + title: "将项目目标映射到模块并轻松跟踪。", + description: + "模块由相互关联的工作项组成。它们有助于监控项目阶段的进度,每个阶段都有特定的截止日期和分析,以指示您离实现这些阶段有多近。", + cta_primary: "设置您的第一个模块", + }, + module_work_items: { + title: "此模块中没有要显示的工作项", + description: "创建工作项以开始监控此模块。", + cta_primary: "创建工作项", + cta_secondary: "添加现有工作项", + }, + views: { + title: "为项目保存自定义视图", + description: + "视图是保存的过滤器,可帮助您快速访问最常用的信息。团队成员可以轻松协作,共享视图并根据特定需求进行调整。", + cta_primary: "创建视图", + }, + no_work_items_in_project: { + title: "项目中暂无工作项", + description: "将工作项添加到项目中,并使用视图将工作切分为可跟踪的部分。", + cta_primary: "添加工作项", + }, + work_item_filter: { + title: "未找到工作项", + description: "您当前的过滤器未返回任何结果。请尝试更改过滤器。", + cta_primary: "添加工作项", + }, + pages: { + title: "记录一切 — 从笔记到 PRD", + description: + "页面让您在一个地方捕获和组织信息。编写会议笔记、项目文档和 PRD,嵌入工作项,并使用现成的组件进行结构化。", + cta_primary: "创建您的第一个页面", + }, + archive_pages: { + title: "暂无已归档页面", + description: "归档不在您关注范围内的页面。需要时在此处访问它们。", + }, + intake_sidebar: { + title: "记录接收请求", + description: "提交新请求以在项目工作流程中进行审查、优先排序和跟踪。", + cta_primary: "创建接收请求", + }, + intake_main: { + title: "选择一个接收工作项以查看其详细信息", + }, + }, + workspace: { + archive_work_items: { + title: "暂无已归档工作项", + description: "通过手动或自动化,您可以归档已完成或已取消的工作项。归档后在此处查找它们。", + cta_primary: "设置自动化", + }, + archive_cycles: { + title: "暂无已归档周期", + description: "为了整理项目,请归档已完成的周期。归档后在此处查找它们。", + }, + archive_modules: { + title: "暂无已归档模块", + description: "为了整理项目,请归档已完成或已取消的模块。归档后在此处查找它们。", + }, + home_widget_quick_links: { + title: "为您的工作保留重要的参考、资源或文档", + }, + inbox_sidebar_all: { + title: "您订阅的工作项的更新将显示在此处", + }, + inbox_sidebar_mentions: { + title: "您的工作项的提及将显示在此处", + }, + your_work_by_priority: { + title: "尚未分配工作项", + }, + your_work_by_state: { + title: "尚未分配工作项", + }, + views: { + title: "暂无视图", + description: "将工作项添加到项目中并使用视图轻松过滤、排序和监控进度。", + cta_primary: "添加工作项", + }, + drafts: { + title: "半成品工作项", + description: "要试用此功能,请开始添加工作项并在中途离开,或在下方创建您的第一个草稿。😉", + cta_primary: "创建草稿工作项", + }, + projects_archived: { + title: "没有已归档项目", + description: "看起来您的所有项目仍然活跃 — 做得好!", + }, + analytics_projects: { + title: "创建项目以在此处可视化项目指标。", + }, + analytics_work_items: { + title: "创建包含工作项和受理人的项目,以开始在此处跟踪绩效、进度和团队影响。", + }, + analytics_no_cycle: { + title: "创建周期以将工作组织成有时限的阶段并跟踪冲刺进度。", + }, + analytics_no_module: { + title: "创建模块以组织工作并跟踪不同阶段的进度。", + }, + analytics_no_intake: { + title: "设置接收以管理传入请求并跟踪它们的接受和拒绝情况", + }, + }, + settings: { + estimates: { + title: "暂无估算", + description: "定义团队如何衡量工作量,并在所有工作项中一致地跟踪它。", + cta_primary: "添加估算系统", + }, + labels: { + title: "暂无标签", + description: "创建个性化标签以有效分类和管理工作项。", + cta_primary: "创建您的第一个标签", + }, + exports: { + title: "暂无导出", + description: "您目前没有任何导出记录。导出数据后,所有记录将显示在此处。", + }, + tokens: { + title: "暂无个人令牌", + description: "生成安全的 API 令牌以将工作空间与外部系统和应用程序连接。", + cta_primary: "添加 API 令牌", + }, + }, +} as const; diff --git a/packages/i18n/src/locales/zh-TW/empty-state.ts b/packages/i18n/src/locales/zh-TW/empty-state.ts new file mode 100644 index 000000000..b7c3f4bdc --- /dev/null +++ b/packages/i18n/src/locales/zh-TW/empty-state.ts @@ -0,0 +1,170 @@ +export default { + common: { + progress: { + title: "暫無進度指標可顯示。", + description: "開始在工作項中設定屬性值以在此查看進度指標。", + }, + updates: { + title: "暫無更新。", + description: "專案成員新增更新後將顯示在此處", + }, + search: { + title: "未找到符合結果。", + description: "未找到結果。請嘗試調整搜尋條件。", + }, + not_found: { + title: "糟糕!似乎出了點問題", + description: "我們目前無法取得您的 Plane 帳戶。這可能是網路錯誤。", + cta_primary: "嘗試重新載入", + }, + server_error: { + title: "伺服器錯誤", + description: "我們無法連線並從伺服器取得資料。請放心,我們正在處理。", + cta_primary: "嘗試重新載入", + }, + }, + project: { + work_items: { + title: "從您的第一個工作項開始。", + description: "工作項是專案的建構模組 — 指派負責人、設定優先順序並輕鬆追蹤進度。", + cta_primary: "建立您的第一個工作項", + }, + cycles: { + title: "在週期中分組和限時您的工作。", + description: "將工作分解為限時區塊,從專案截止日期倒推設定日期,並作為團隊取得實質性進展。", + cta_primary: "設定您的第一個週期", + }, + cycle_work_items: { + title: "此週期中沒有要顯示的工作項", + description: "建立工作項以開始監控團隊在此週期中的進度並按時實現目標。", + cta_primary: "建立工作項", + cta_secondary: "新增現有工作項", + }, + modules: { + title: "將專案目標對應到模組並輕鬆追蹤。", + description: + "模組由相互關聯的工作項組成。它們有助於監控專案階段的進度,每個階段都有特定的截止日期和分析,以指示您離實現這些階段有多近。", + cta_primary: "設定您的第一個模組", + }, + module_work_items: { + title: "此模組中沒有要顯示的工作項", + description: "建立工作項以開始監控此模組。", + cta_primary: "建立工作項", + cta_secondary: "新增現有工作項", + }, + views: { + title: "為專案儲存自訂檢視", + description: + "檢視是已儲存的篩選器,可協助您快速存取最常用的資訊。團隊成員可以輕鬆協作,共用檢視並根據特定需求進行調整。", + cta_primary: "建立檢視", + }, + no_work_items_in_project: { + title: "專案中暫無工作項", + description: "將工作項新增至專案中,並使用檢視將工作切分為可追蹤的部分。", + cta_primary: "新增工作項", + }, + work_item_filter: { + title: "未找到工作項", + description: "您目前的篩選器未傳回任何結果。請嘗試變更篩選器。", + cta_primary: "新增工作項", + }, + pages: { + title: "記錄一切 — 從筆記到 PRD", + description: + "頁面讓您在一個地方擷取和組織資訊。撰寫會議筆記、專案文件和 PRD,嵌入工作項,並使用現成的元件進行結構化。", + cta_primary: "建立您的第一個頁面", + }, + archive_pages: { + title: "暫無已封存頁面", + description: "封存不在您關注範圍內的頁面。需要時在此處存取它們。", + }, + intake_sidebar: { + title: "記錄接收請求", + description: "提交新請求以在專案工作流程中進行審查、優先順序排序和追蹤。", + cta_primary: "建立接收請求", + }, + intake_main: { + title: "選擇一個接收工作項以查看其詳細資訊", + }, + }, + workspace: { + archive_work_items: { + title: "暫無已封存工作項", + description: "透過手動或自動化,您可以封存已完成或已取消的工作項。封存後在此處尋找它們。", + cta_primary: "設定自動化", + }, + archive_cycles: { + title: "暫無已封存週期", + description: "為了整理專案,請封存已完成的週期。封存後在此處尋找它們。", + }, + archive_modules: { + title: "暫無已封存模組", + description: "為了整理專案,請封存已完成或已取消的模組。封存後在此處尋找它們。", + }, + home_widget_quick_links: { + title: "為您的工作保留重要的參考、資源或文件", + }, + inbox_sidebar_all: { + title: "您訂閱的工作項的更新將顯示在此處", + }, + inbox_sidebar_mentions: { + title: "您的工作項的提及將顯示在此處", + }, + your_work_by_priority: { + title: "尚未分配工作項", + }, + your_work_by_state: { + title: "尚未分配工作項", + }, + views: { + title: "暫無檢視", + description: "將工作項新增至專案中並使用檢視輕鬆篩選、排序和監控進度。", + cta_primary: "新增工作項", + }, + drafts: { + title: "半成品工作項", + description: "要試用此功能,請開始新增工作項並在中途離開,或在下方建立您的第一個草稿。😉", + cta_primary: "建立草稿工作項", + }, + projects_archived: { + title: "沒有已封存專案", + description: "看起來您的所有專案仍然活躍 — 做得好!", + }, + analytics_projects: { + title: "建立專案以在此處視覺化專案指標。", + }, + analytics_work_items: { + title: "建立包含工作項和受託人的專案,以開始在此處追蹤績效、進度和團隊影響。", + }, + analytics_no_cycle: { + title: "建立週期以將工作組織成有時限的階段並追蹤衝刺進度。", + }, + analytics_no_module: { + title: "建立模組以組織工作並追蹤不同階段的進度。", + }, + analytics_no_intake: { + title: "設定接收以管理傳入請求並追蹤它們的接受和拒絕情況", + }, + }, + settings: { + estimates: { + title: "暫無估算", + description: "定義團隊如何衡量工作量,並在所有工作項中一致地追蹤它。", + cta_primary: "新增估算系統", + }, + labels: { + title: "暫無標籤", + description: "建立個人化標籤以有效分類和管理工作項。", + cta_primary: "建立您的第一個標籤", + }, + exports: { + title: "暫無匯出", + description: "您目前沒有任何匯出記錄。匯出資料後,所有記錄將顯示在此處。", + }, + tokens: { + title: "暫無個人權杖", + description: "產生安全的 API 權杖以將工作區與外部系統和應用程式連線。", + cta_primary: "新增 API 權杖", + }, + }, +} as const; diff --git a/packages/propel/src/empty-state/assets-showcase.stories.tsx b/packages/propel/src/empty-state/assets-showcase.stories.tsx new file mode 100644 index 000000000..da35189fc --- /dev/null +++ b/packages/propel/src/empty-state/assets-showcase.stories.tsx @@ -0,0 +1,204 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { HorizontalStackAssetsMap } from "./assets/horizontal-stack/constant"; +import { IllustrationMap } from "./assets/illustration/constant"; +import { VerticalStackAssetsMap } from "./assets/vertical-stack/constant"; + +// Meta for asset showcase +const meta: Meta = { + title: "Components/EmptyState/Assets Showcase", + parameters: { + layout: "fullscreen", + docs: { + description: { + component: "Visual catalog of all available empty state assets organized by type.", + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const HorizontalStackAssets: Story = { + parameters: { + docs: { + description: { + story: + "Horizontal stack assets designed for compact empty states. These are optimized for smaller, inline empty state scenarios.", + }, + }, + }, + render: () => ( +
+
+

Horizontal Stack Assets

+

Used primarily in EmptyStateCompact component

+
+
+ {HorizontalStackAssetsMap.map((item) => ( +
+
{item.asset}
+

{item.title}

+ + {item.title.toLowerCase().replace(/\s+/g, "-")} + +
+ ))} +
+
+ ), +}; + +export const VerticalStackAssets: Story = { + parameters: { + docs: { + description: { + story: + "Vertical stack assets designed for detailed empty states. These are larger and more prominent, suitable for feature-specific empty states.", + }, + }, + }, + render: () => ( +
+
+

Vertical Stack Assets

+

Used primarily in EmptyStateDetailed component

+
+
+ {VerticalStackAssetsMap.map((item) => ( +
+
{item.asset}
+

+ {item.title.replace(/VerticalStackIllustration$/, "")} +

+ + {item.title + .replace(/VerticalStackIllustration$/, "") + .replace(/([A-Z])/g, "-$1") + .toLowerCase() + .slice(1)} + +
+ ))} +
+
+ ), +}; + +export const IllustrationAssets: Story = { + parameters: { + docs: { + description: { + story: "Illustration assets available for both compact and detailed empty states.", + }, + }, + }, + render: () => ( +
+
+

Illustration Assets

+

Available in both EmptyStateCompact and EmptyStateDetailed

+
+
+ {IllustrationMap.map((item) => ( +
+
{item.asset}
+

{item.title}

+ + {item.title.toLowerCase()} + +
+ ))} +
+
+ ), +}; + +export const AllAssets: Story = { + parameters: { + docs: { + description: { + story: "Complete catalog of all available empty state assets.", + }, + }, + }, + render: () => ( +
+ {/* Horizontal Stack */} +
+
+

Horizontal Stack Assets

+

+ For EmptyStateCompact - {HorizontalStackAssetsMap.length} assets +

+
+
+ {HorizontalStackAssetsMap.map((item) => ( +
+
{item.asset}
+ {item.title.toLowerCase().replace(/\s+/g, "-")} +
+ ))} +
+
+ + {/* Vertical Stack */} +
+
+

Vertical Stack Assets

+

+ For EmptyStateDetailed - {VerticalStackAssetsMap.length} assets +

+
+
+ {VerticalStackAssetsMap.map((item) => ( +
+
{item.asset}
+ + {item.title + .replace(/VerticalStackIllustration$/, "") + .replace(/([A-Z])/g, "-$1") + .toLowerCase() + .slice(1)} + +
+ ))} +
+
+ + {/* Illustrations */} +
+
+

Illustration Assets

+

For both components - {IllustrationMap.length} assets

+
+
+ {IllustrationMap.map((item) => ( +
+
{item.asset}
+ {item.title.toLowerCase()} +
+ ))} +
+
+
+ ), +}; diff --git a/packages/propel/src/empty-state/assets/asset-registry.tsx b/packages/propel/src/empty-state/assets/asset-registry.tsx new file mode 100644 index 000000000..42ac821e6 --- /dev/null +++ b/packages/propel/src/empty-state/assets/asset-registry.tsx @@ -0,0 +1,130 @@ +import React from "react"; +import type { + CompactAssetType, + DetailedAssetType, + HorizontalStackAssetType, + IllustrationAssetType, + VerticalStackAssetType, +} from "./asset-types"; +import { + CustomerHorizontalStackIllustration, + EpicHorizontalStackIllustration, + EstimateHorizontalStackIllustration, + ExportHorizontalStackIllustration, + IntakeHorizontalStackIllustration, + LabelHorizontalStackIllustration, + LinkHorizontalStackIllustration, + MembersHorizontalStackIllustration, + NoteHorizontalStackIllustration, + PriorityHorizontalStackIllustration, + ProjectHorizontalStackIllustration, + SettingsHorizontalStackIllustration, + StateHorizontalStackIllustration, + TemplateHorizontalStackIllustration, + TokenHorizontalStackIllustration, + UnknownHorizontalStackIllustration, + UpdateHorizontalStackIllustration, + WebhookHorizontalStackIllustration, + WorkItemHorizontalStackIllustration, + WorklogHorizontalStackIllustration, +} from "./horizontal-stack"; +import { InboxIllustration, SearchIllustration } from "./illustration"; +import { + ArchivedCycleVerticalStackIllustration, + ArchivedModuleVerticalStackIllustration, + ArchivedWorkItemVerticalStackIllustration, + CustomerVerticalStackIllustration, + CycleVerticalStackIllustration, + DashboardVerticalStackIllustration, + DraftVerticalStackIllustration, + EpicVerticalStackIllustration, + Error404VerticalStackIllustration, + InvalidLinkVerticalStackIllustration, + ModuleVerticalStackIllustration, + NoAccessVerticalStackIllustration, + PageVerticalStackIllustration, + ProjectVerticalStackIllustration, + ServerErrorVerticalStackIllustration, + TeamspaceVerticalStackIllustration, + ViewVerticalStackIllustration, + WorkItemVerticalStackIllustration, +} from "./vertical-stack"; + +// Horizontal Stack Asset Registry +export const HORIZONTAL_STACK_ASSETS: Record> = { + customer: CustomerHorizontalStackIllustration, + epic: EpicHorizontalStackIllustration, + estimate: EstimateHorizontalStackIllustration, + export: ExportHorizontalStackIllustration, + intake: IntakeHorizontalStackIllustration, + label: LabelHorizontalStackIllustration, + link: LinkHorizontalStackIllustration, + members: MembersHorizontalStackIllustration, + note: NoteHorizontalStackIllustration, + priority: PriorityHorizontalStackIllustration, + project: ProjectHorizontalStackIllustration, + settings: SettingsHorizontalStackIllustration, + state: StateHorizontalStackIllustration, + template: TemplateHorizontalStackIllustration, + token: TokenHorizontalStackIllustration, + unknown: UnknownHorizontalStackIllustration, + update: UpdateHorizontalStackIllustration, + webhook: WebhookHorizontalStackIllustration, + "work-item": WorkItemHorizontalStackIllustration, + worklog: WorklogHorizontalStackIllustration, +}; + +// Vertical Stack Asset Registry +export const VERTICAL_STACK_ASSETS: Record> = { + "archived-cycle": ArchivedCycleVerticalStackIllustration, + "archived-module": ArchivedModuleVerticalStackIllustration, + "archived-work-item": ArchivedWorkItemVerticalStackIllustration, + customer: CustomerVerticalStackIllustration, + cycle: CycleVerticalStackIllustration, + dashboard: DashboardVerticalStackIllustration, + draft: DraftVerticalStackIllustration, + epic: EpicVerticalStackIllustration, + "error-404": Error404VerticalStackIllustration, + "invalid-link": InvalidLinkVerticalStackIllustration, + module: ModuleVerticalStackIllustration, + "no-access": NoAccessVerticalStackIllustration, + page: PageVerticalStackIllustration, + project: ProjectVerticalStackIllustration, + "server-error": ServerErrorVerticalStackIllustration, + teamspace: TeamspaceVerticalStackIllustration, + view: ViewVerticalStackIllustration, + "work-item": WorkItemVerticalStackIllustration, +}; + +// Illustration Asset Registry +export const ILLUSTRATION_ASSETS: Record> = { + inbox: InboxIllustration, + search: SearchIllustration, +}; + +// Helper functions to get assets +export const getCompactAsset = (assetKey: CompactAssetType, className?: string): React.ReactNode => { + const AssetComponent = + (HORIZONTAL_STACK_ASSETS[assetKey as HorizontalStackAssetType] as React.ComponentType<{ className?: string }>) || + ILLUSTRATION_ASSETS[assetKey as IllustrationAssetType]; + + if (!AssetComponent) { + console.warn(`Asset "${assetKey}" not found in compact asset registry`); + return null; + } + + return ; +}; + +export const getDetailedAsset = (assetKey: DetailedAssetType, className?: string): React.ReactNode => { + const AssetComponent = + (VERTICAL_STACK_ASSETS[assetKey as VerticalStackAssetType] as React.ComponentType<{ className?: string }>) || + ILLUSTRATION_ASSETS[assetKey as IllustrationAssetType]; + + if (!AssetComponent) { + console.warn(`Asset "${assetKey}" not found in detailed asset registry`); + return null; + } + + return ; +}; diff --git a/packages/propel/src/empty-state/assets/asset-types.ts b/packages/propel/src/empty-state/assets/asset-types.ts new file mode 100644 index 000000000..af8e48e08 --- /dev/null +++ b/packages/propel/src/empty-state/assets/asset-types.ts @@ -0,0 +1,52 @@ +// Horizontal Stack Asset Types +export type HorizontalStackAssetType = + | "customer" + | "epic" + | "estimate" + | "export" + | "intake" + | "label" + | "link" + | "members" + | "note" + | "priority" + | "project" + | "settings" + | "state" + | "template" + | "token" + | "unknown" + | "update" + | "webhook" + | "work-item" + | "worklog"; + +// Vertical Stack Asset Types +export type VerticalStackAssetType = + | "archived-cycle" + | "archived-module" + | "archived-work-item" + | "customer" + | "cycle" + | "dashboard" + | "draft" + | "epic" + | "error-404" + | "invalid-link" + | "module" + | "no-access" + | "page" + | "project" + | "server-error" + | "teamspace" + | "view" + | "work-item"; + +// Illustration Asset Types +export type IllustrationAssetType = "inbox" | "search"; + +// Combined Asset Types for Compact (uses horizontal + illustration) +export type CompactAssetType = HorizontalStackAssetType | IllustrationAssetType; + +// Combined Asset Types for Detailed (uses vertical + illustration) +export type DetailedAssetType = VerticalStackAssetType | IllustrationAssetType; diff --git a/packages/propel/src/empty-state/assets/horizontal-stack/constant.tsx b/packages/propel/src/empty-state/assets/horizontal-stack/constant.tsx index ebaa89b45..2328b58e5 100644 --- a/packages/propel/src/empty-state/assets/horizontal-stack/constant.tsx +++ b/packages/propel/src/empty-state/assets/horizontal-stack/constant.tsx @@ -9,6 +9,7 @@ import { MembersHorizontalStackIllustration, NoteHorizontalStackIllustration, PriorityHorizontalStackIllustration, + ProjectHorizontalStackIllustration, SettingsHorizontalStackIllustration, StateHorizontalStackIllustration, TemplateHorizontalStackIllustration, @@ -61,6 +62,10 @@ export const HorizontalStackAssetsMap = [ asset: , title: "Priority", }, + { + asset: , + title: "Project", + }, { asset: , title: "Settings", diff --git a/packages/propel/src/empty-state/assets/horizontal-stack/index.ts b/packages/propel/src/empty-state/assets/horizontal-stack/index.ts index 049eb5438..35bda744d 100644 --- a/packages/propel/src/empty-state/assets/horizontal-stack/index.ts +++ b/packages/propel/src/empty-state/assets/horizontal-stack/index.ts @@ -8,6 +8,7 @@ export * from "./link"; export * from "./members"; export * from "./note"; export * from "./priority"; +export * from "./project"; export * from "./settings"; export * from "./state"; export * from "./template"; diff --git a/packages/propel/src/empty-state/assets/horizontal-stack/project.tsx b/packages/propel/src/empty-state/assets/horizontal-stack/project.tsx new file mode 100644 index 000000000..2417d6830 --- /dev/null +++ b/packages/propel/src/empty-state/assets/horizontal-stack/project.tsx @@ -0,0 +1,69 @@ +import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper"; + +export const ProjectHorizontalStackIllustration = ({ className }: TIllustrationAssetProps) => ( + + + + + + + + + + + + + +); diff --git a/packages/propel/src/empty-state/assets/illustration/constant.tsx b/packages/propel/src/empty-state/assets/illustration/constant.tsx new file mode 100644 index 000000000..31da2e49e --- /dev/null +++ b/packages/propel/src/empty-state/assets/illustration/constant.tsx @@ -0,0 +1,12 @@ +import { InboxIllustration, SearchIllustration } from "./"; + +export const IllustrationMap = [ + { + asset: , + title: "Inbox", + }, + { + asset: , + title: "Search", + }, +]; diff --git a/packages/propel/src/empty-state/assets/illustration/inbox.tsx b/packages/propel/src/empty-state/assets/illustration/inbox.tsx new file mode 100644 index 000000000..93aa614f9 --- /dev/null +++ b/packages/propel/src/empty-state/assets/illustration/inbox.tsx @@ -0,0 +1,134 @@ +import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper"; + +export const InboxIllustration = ({ className, ...rest }: TIllustrationAssetProps) => ( + + + + + + + + + + + + + + + + + + + +); diff --git a/packages/propel/src/empty-state/assets/illustration/index.ts b/packages/propel/src/empty-state/assets/illustration/index.ts new file mode 100644 index 000000000..0a9791fe4 --- /dev/null +++ b/packages/propel/src/empty-state/assets/illustration/index.ts @@ -0,0 +1,2 @@ +export * from "./inbox"; +export * from "./search"; diff --git a/packages/propel/src/empty-state/assets/illustration/search.tsx b/packages/propel/src/empty-state/assets/illustration/search.tsx new file mode 100644 index 000000000..a9057f98f --- /dev/null +++ b/packages/propel/src/empty-state/assets/illustration/search.tsx @@ -0,0 +1,58 @@ +import { type TIllustrationAssetProps, ILLUSTRATION_COLOR_TOKEN_MAP } from "../helper"; + +export const SearchIllustration = ({ className, ...rest }: TIllustrationAssetProps) => ( + + + + + + + + + +); diff --git a/packages/propel/src/empty-state/assets/index.ts b/packages/propel/src/empty-state/assets/index.ts index 3791a83f5..c5431d804 100644 --- a/packages/propel/src/empty-state/assets/index.ts +++ b/packages/propel/src/empty-state/assets/index.ts @@ -1,3 +1,6 @@ -export * from "./horizontal-stack"; -export * from "./vertical-stack"; +export * from "./asset-registry"; +export * from "./asset-types"; export * from "./helper"; +export * from "./horizontal-stack"; +export * from "./illustration"; +export * from "./vertical-stack"; diff --git a/packages/propel/src/empty-state/compact-empty-state.stories.tsx b/packages/propel/src/empty-state/compact-empty-state.stories.tsx new file mode 100644 index 000000000..d49d5f10b --- /dev/null +++ b/packages/propel/src/empty-state/compact-empty-state.stories.tsx @@ -0,0 +1,142 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { EmptyStateCompact } from "./compact-empty-state"; +import type { BaseEmptyStateCommonProps } from "./types"; + +const meta: Meta = { + title: "Components/EmptyState/Compact", + component: EmptyStateCompact, + parameters: { + layout: "centered", + docs: { + description: { + component: + "A compact empty state component with centered title, asset, and action buttons. Best used for simple, space-constrained empty states. Supports horizontal stack and illustration assets via `assetKey`.", + }, + }, + }, + argTypes: { + title: { + control: "text", + description: "The main title text for the empty state", + }, + assetKey: { + control: "select", + options: [ + "customer", + "epic", + "estimate", + "export", + "intake", + "label", + "link", + "members", + "note", + "priority", + "project", + "settings", + "state", + "template", + "token", + "unknown", + "update", + "webhook", + "work-item", + "worklog", + "inbox", + ], + description: "Predefined asset key (horizontal-stack or illustration)", + }, + className: { + control: "text", + description: "Additional CSS classes to apply to the content wrapper", + }, + rootClassName: { + control: "text", + description: "Additional CSS classes to apply to the root container", + }, + assetClassName: { + control: "text", + description: "Additional CSS classes to apply to the asset", + }, + asset: { + control: false, + description: "Custom React node to display as the visual asset (use this for full control instead of assetKey)", + }, + actions: { + control: false, + description: "Array of action buttons to display", + }, + }, +}; + +export default meta; +type Story = StoryObj; + +// Using assetKey (recommended approach) +export const WithAssetKey: Story = { + args: { + assetKey: "work-item", + assetClassName: "size-20", + title: "There're no progress metrics to show yet.", + }, +}; + +export const WithAssetKeyAndAction: Story = { + args: { + assetKey: "project", + assetClassName: "size-20", + title: "No projects found", + actions: [ + { + label: "Create Project", + onClick: () => console.log("create-clicked"), + variant: "primary", + }, + ], + }, +}; + +export const WithAssetKeyAndMultipleActions: Story = { + args: { + assetKey: "members", + assetClassName: "size-20", + title: "Get started with your workspace", + actions: [ + { + label: "Create Project", + onClick: () => console.log("create-clicked"), + variant: "primary", + }, + { + label: "Import", + onClick: () => console.log("import-clicked"), + variant: "outline-primary", + }, + ], + }, +}; + +// Using custom asset (legacy approach) +export const WithCustomAsset: Story = { + args: { + asset: ( + + + + ), + title: "No items found", + actions: [ + { + label: "Create Item", + onClick: () => console.log("create-clicked"), + variant: "primary", + }, + ], + }, +}; + +export const TitleOnly: Story = { + args: { + title: "No results found", + }, +}; diff --git a/packages/propel/src/empty-state/compact-empty-state.tsx b/packages/propel/src/empty-state/compact-empty-state.tsx new file mode 100644 index 000000000..33f135d97 --- /dev/null +++ b/packages/propel/src/empty-state/compact-empty-state.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { Button } from "../button/button"; +import { cn } from "../utils/classname"; +import { getCompactAsset } from "./assets/asset-registry"; +import type { CompactAssetType } from "./assets/asset-types"; +import type { BaseEmptyStateCommonProps } from "./types"; + +export const EmptyStateCompact: React.FC = ({ + asset, + assetKey, + title, + description, + actions, + className, + rootClassName, + assetClassName, + align = "center", +}) => { + // Determine which asset to use: assetKey takes precedence, fallback to custom asset + const resolvedAsset = assetKey ? getCompactAsset(assetKey as CompactAssetType, assetClassName) : asset; + + const rootAlignClasses = align === "center" ? "items-center" : "items-start"; + const containerAlignClasses = align === "center" ? "items-center text-center" : "items-start text-left"; + + return ( +
+
+ {resolvedAsset &&
{resolvedAsset}
} + +
+ {title && description ? ( +
+ {title &&

{title}

} + {description &&

{description}

} +
+ ) : ( + title &&

{title}

+ )} + + {actions && actions.length > 0 && ( +
+ {actions.map((action, index) => { + const { label, variant, ...rest } = action; + return ( + + ); + })} +
+ )} +
+
+
+ ); +}; + +EmptyStateCompact.displayName = "EmptyStateCompact"; diff --git a/packages/propel/src/empty-state/detailed-empty-state.stories.tsx b/packages/propel/src/empty-state/detailed-empty-state.stories.tsx new file mode 100644 index 000000000..a06d2cbca --- /dev/null +++ b/packages/propel/src/empty-state/detailed-empty-state.stories.tsx @@ -0,0 +1,295 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { EmptyStateDetailed } from "./detailed-empty-state"; +import type { BaseEmptyStateCommonProps } from "./types"; + +const meta: Meta = { + title: "Components/EmptyState/Detailed", + component: EmptyStateDetailed, + parameters: { + layout: "centered", + docs: { + description: { + component: + "A detailed empty state component with title, description, asset, and action buttons. Best used for feature-specific empty states that need more context. Supports vertical stack and illustration assets via `assetKey`.", + }, + }, + }, + argTypes: { + title: { + control: "text", + description: "The main title text for the empty state", + }, + description: { + control: "text", + description: "Optional description text that appears below the title", + }, + assetKey: { + control: "select", + options: [ + "archived-cycle", + "archived-module", + "archived-work-item", + "customer", + "cycle", + "dashboard", + "draft", + "epic", + "error-404", + "invalid-link", + "module", + "no-access", + "page", + "project", + "server-error", + "teamspace", + "view", + "work-item", + "inbox", + ], + description: "Predefined asset key (vertical-stack or illustration)", + }, + className: { + control: "text", + description: "Additional CSS classes to apply to the content wrapper", + }, + rootClassName: { + control: "text", + description: "Additional CSS classes to apply to the root container", + }, + assetClassName: { + control: "text", + description: "Additional CSS classes to apply to the asset", + }, + asset: { + control: false, + description: "Custom React node to display as the visual asset (use this for full control instead of assetKey)", + }, + actions: { + control: false, + description: "Array of action buttons to display", + }, + }, +}; + +export default meta; +type Story = StoryObj; + +// Primary story - showcases the most common usage +export const Default: Story = { + args: { + assetKey: "epic", + assetClassName: "w-40 h-45", + title: "Create an epic and split work into smaller goals", + description: "For larger bodies of work that span several cycles and can live across modules, create an epic.", + actions: [ + { + label: "Create an Epic", + onClick: () => console.log("primary-action-clicked"), + variant: "primary", + }, + ], + }, +}; + +export const WithSingleAction: Story = { + args: { + assetKey: "project", + assetClassName: "w-40 h-45", + title: "No projects found", + description: "Get started by creating your first project to organize your work.", + actions: [ + { + label: "Create Project", + onClick: () => console.log("create-clicked"), + variant: "primary", + }, + ], + }, +}; + +export const WithMultipleActions: Story = { + args: { + assetKey: "module", + assetClassName: "w-40 h-45", + title: "No modules found", + description: "Get started by creating your first module or import existing ones.", + actions: [ + { + label: "Create Module", + onClick: () => console.log("create-clicked"), + variant: "primary", + }, + { + label: "Import Modules", + onClick: () => console.log("import-clicked"), + variant: "outline-primary", + }, + ], + }, +}; + +export const WithoutActions: Story = { + args: { + assetKey: "dashboard", + assetClassName: "w-40 h-45", + title: "No activity yet", + description: "Your activity feed will show up here once you start using the platform.", + }, +}; + +export const ErrorState: Story = { + args: { + assetKey: "error-404", + assetClassName: "w-40 h-45", + title: "Page not found", + description: "The page you're looking for doesn't exist or has been moved.", + actions: [ + { + label: "Go to Home", + onClick: () => console.log("home-clicked"), + variant: "primary", + }, + ], + }, +}; + +export const ServerErrorState: Story = { + name: "Error - Server", + args: { + assetKey: "server-error", + assetClassName: "w-40 h-45", + title: "Something went wrong", + description: "We're experiencing technical difficulties. Please try again later.", + actions: [ + { + label: "Retry", + onClick: () => console.log("retry-clicked"), + variant: "primary", + }, + { + label: "Contact Support", + onClick: () => console.log("support-clicked"), + variant: "outline-primary", + }, + ], + }, +}; + +export const NoAccessState: Story = { + name: "Access Denied", + args: { + assetKey: "no-access", + assetClassName: "w-40 h-45", + title: "You don't have access", + description: "Contact your workspace admin to request access to this resource.", + }, +}; + +export const ArchivedState: Story = { + name: "Archived Content", + args: { + assetKey: "archived-work-item", + assetClassName: "w-40 h-45", + title: "No archived items", + description: "Archived items will appear here when you archive them.", + }, +}; + +export const CycleState: Story = { + name: "Cycles", + args: { + assetKey: "cycle", + assetClassName: "w-40 h-45", + title: "No cycles found", + description: "Create cycles to organize your work into time-boxed iterations.", + actions: [ + { + label: "Create Cycle", + onClick: () => console.log("create-cycle-clicked"), + variant: "primary", + }, + ], + }, +}; + +export const ModuleState: Story = { + name: "Modules", + args: { + assetKey: "module", + assetClassName: "w-40 h-45", + title: "No modules found", + description: "Modules help you organize related work items into logical groups.", + actions: [ + { + label: "Create Module", + onClick: () => console.log("create-module-clicked"), + variant: "primary", + }, + ], + }, +}; + +export const ViewState: Story = { + name: "Views", + args: { + assetKey: "view", + assetClassName: "w-40 h-45", + title: "No saved views", + description: "Create custom views to filter and organize your work items.", + actions: [ + { + label: "Create View", + onClick: () => console.log("create-view-clicked"), + variant: "primary", + }, + ], + }, +}; + +export const PageState: Story = { + name: "Pages", + args: { + assetKey: "page", + assetClassName: "w-40 h-45", + title: "No pages found", + description: "Create pages to document your project, share knowledge, and collaborate.", + actions: [ + { + label: "Create Page", + onClick: () => console.log("create-page-clicked"), + variant: "primary", + }, + ], + }, +}; + +// Using custom asset (for special cases) +export const WithCustomAsset: Story = { + name: "Custom Asset", + args: { + asset: ( + + + + + ), + title: "Custom asset example", + description: "This example uses a custom SVG asset instead of predefined assetKey.", + actions: [ + { + label: "Get Started", + onClick: () => console.log("action-clicked"), + variant: "primary", + }, + ], + }, +}; + +// Minimal example +export const Minimal: Story = { + name: "Minimal - Text Only", + args: { + title: "No data available", + description: "Data will appear here once available.", + }, +}; diff --git a/packages/propel/src/empty-state/detailed-empty-state.tsx b/packages/propel/src/empty-state/detailed-empty-state.tsx new file mode 100644 index 000000000..b6fd4af0c --- /dev/null +++ b/packages/propel/src/empty-state/detailed-empty-state.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { Button } from "../button/button"; +import { cn } from "../utils/classname"; +import { getDetailedAsset } from "./assets/asset-registry"; +import type { DetailedAssetType } from "./assets/asset-types"; +import type { BaseEmptyStateCommonProps } from "./types"; + +export const EmptyStateDetailed: React.FC = ({ + asset, + assetKey, + title, + description, + actions, + className, + rootClassName, + assetClassName, +}) => { + // Determine which asset to use: assetKey takes precedence, fallback to custom asset + const resolvedAsset = assetKey ? getDetailedAsset(assetKey as DetailedAssetType, assetClassName) : asset; + + return ( +
+
+ {resolvedAsset &&
{resolvedAsset}
} + +
+ {(title || description) && ( +
+ {title &&

{title}

} + {description &&

{description}

} +
+ )} + + {actions && actions.length > 0 && ( +
+ {actions.map((action, index) => { + const { label, variant, ...rest } = action; + return ( + + ); + })} +
+ )} +
+
+
+ ); +}; + +EmptyStateDetailed.displayName = "EmptyStateDetailed"; diff --git a/packages/propel/src/empty-state/empty-state.stories.tsx b/packages/propel/src/empty-state/empty-state.stories.tsx deleted file mode 100644 index 095bbb711..000000000 --- a/packages/propel/src/empty-state/empty-state.stories.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { WorkItemHorizontalStackIllustration } from "./assets/horizontal-stack"; -import { HorizontalStackAssetsMap } from "./assets/horizontal-stack/constant"; -import { WorkItemVerticalStackIllustration } from "./assets/vertical-stack"; -import { VerticalStackAssetsMap } from "./assets/vertical-stack/constant"; -import { EmptyState, type EmptyStateProps } from "./empty-state"; - -const meta: Meta = { - title: "Components/EmptyState", - component: EmptyState, - parameters: { - layout: "centered", - docs: { - description: { - component: - "A flexible empty state component that can display an asset, title, description, and action buttons.", - }, - }, - }, - argTypes: { - title: { - control: "text", - description: "The main title text for the empty state", - }, - description: { - control: "text", - description: "Optional description text that appears below the title", - }, - className: { - control: "text", - description: "Additional CSS classes to apply to the root element", - }, - type: { - control: "select", - options: ["detailed", "simple"], - description: "The layout type of the empty state", - }, - asset: { - control: false, - description: "React node to display as the visual asset (icon, illustration, etc.)", - }, - actions: { - control: false, - description: "Array of action buttons to display", - }, - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - asset: , - title: "Create an epic and split work into smaller goals", - description: "For larger bodies of work that span several cycles and can live across modules, create an epic.", - actions: [ - { - label: "Create an Epic", - onClick: () => console.log("primary-action-clicked"), - variant: "primary", - }, - ], - }, -}; - -export const Simple: Story = { - args: { - asset: , - title: "There're no progress metrics to show yet.", - description: "For larger bodies of work that span several cycles and can live across modules, create an epic.", - type: "simple", - }, -}; - -export const HorizontalStackAssets: Story = { - render: () => ( -
- {HorizontalStackAssetsMap.map((item) => ( -
- {item.asset} -

{item.title}

-
- ))} -
- ), -}; - -export const VerticalStackAssets: Story = { - render: () => ( -
- {VerticalStackAssetsMap.map((item) => ( -
- {item.asset} -

{item.title}

-
- ))} -
- ), -}; diff --git a/packages/propel/src/empty-state/empty-state.tsx b/packages/propel/src/empty-state/empty-state.tsx index d1dc8823b..a1304d08d 100644 --- a/packages/propel/src/empty-state/empty-state.tsx +++ b/packages/propel/src/empty-state/empty-state.tsx @@ -1,62 +1,71 @@ import React from "react"; -import { Button } from "../button/button"; -import { TButtonVariant } from "../button/helper"; +import { CompactAssetType, DetailedAssetType } from "./assets/asset-types"; +import { EmptyStateCompact } from "./compact-empty-state"; +import { EmptyStateDetailed } from "./detailed-empty-state"; +import type { BaseEmptyStateCommonProps } from "./types"; -export interface ActionButton { - label: string; - onClick: () => void; - variant?: TButtonVariant; - disabled?: boolean; -} +/** + * @deprecated Use EmptyStateCompact or EmptyStateDetailed directly with assetKey for better type safety + * + * This wrapper component maintains backward compatibility for existing code. + * For new code, prefer: + * - EmptyStateCompact with assetKey for simple states + * - EmptyStateDetailed with assetKey for detailed states + */ -type TEmptyStateType = "detailed" | "simple"; +type EmptyStateType = "detailed" | "simple"; export interface EmptyStateProps { + /** @deprecated Use assetKey instead */ asset?: React.ReactNode; - title: string; + title?: string; description?: string; - actions?: ActionButton[]; + actions?: BaseEmptyStateCommonProps["actions"]; className?: string; - type?: TEmptyStateType; + rootClassName?: string; + assetClassName?: string; + type?: EmptyStateType; + /** Type-safe asset key (use instead of asset) */ + assetKey?: CompactAssetType | DetailedAssetType; } -const EmptyStateContent: React.FC<{ - title: string; - description?: string; - actions?: ActionButton[]; -}> = ({ title, description, actions }) => ( -
-
-

{title}

- {description &&

{description}

} -
- - {actions && actions.length > 0 && ( -
- {actions.map((action, index) => ( - - ))} -
- )} -
-); - export const EmptyState: React.FC = ({ + type = "detailed", asset, + assetKey, title, description, actions, - className = "", - type = "detailed", + className, + rootClassName, + assetClassName, }) => { - const alignmentClass = type === "simple" ? "items-center text-center" : "text-left"; + if (type === "simple") { + return ( + + ); + } return ( -
- {asset &&
{asset}
} - -
+ ); }; + +EmptyState.displayName = "EmptyState"; diff --git a/packages/propel/src/empty-state/index.ts b/packages/propel/src/empty-state/index.ts index ce215b7c3..b38181d5e 100644 --- a/packages/propel/src/empty-state/index.ts +++ b/packages/propel/src/empty-state/index.ts @@ -1 +1,5 @@ +export * from "./assets"; +export * from "./compact-empty-state"; +export * from "./detailed-empty-state"; export * from "./empty-state"; +export * from "./types"; diff --git a/packages/propel/src/empty-state/types.ts b/packages/propel/src/empty-state/types.ts new file mode 100644 index 000000000..8c2886799 --- /dev/null +++ b/packages/propel/src/empty-state/types.ts @@ -0,0 +1,24 @@ +import type { TButtonVariant } from "../button/helper"; +import type { TAlign } from "../utils/placement"; +import type { CompactAssetType, DetailedAssetType } from "./assets/asset-types"; + +export interface ActionButton extends Omit, "children"> { + label: string; + variant?: TButtonVariant; + [key: `data-${string}`]: string | undefined; +} + +export interface BaseEmptyStateCommonProps { + title?: string; + actions?: ActionButton[]; + /** CSS classes for the content wrapper */ + className?: string; + /** CSS classes for the root container */ + rootClassName?: string; + /** CSS classes for the asset wrapper */ + assetClassName?: string; + description?: string; + assetKey?: CompactAssetType | DetailedAssetType; + asset?: React.ReactNode; + align?: TAlign; +} From 33b6405695e68c2d9f7cd98bbf371d9f0c5aaa1d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 24 Oct 2025 19:48:28 +0530 Subject: [PATCH 013/212] [WEB-5160] chore: propel banner and archived work item improvements (#7999) --- .../(detail)/[archivedIssueId]/page.tsx | 45 +++-- packages/propel/package.json | 4 + packages/propel/src/banner/banner.stories.tsx | 181 ++++++++++++++++++ packages/propel/src/banner/banner.tsx | 131 +++++++++++++ packages/propel/src/banner/helper.tsx | 46 +++++ packages/propel/src/banner/index.ts | 3 + packages/propel/tsdown.config.ts | 1 + 7 files changed, 399 insertions(+), 12 deletions(-) create mode 100644 packages/propel/src/banner/banner.stories.tsx create mode 100644 packages/propel/src/banner/banner.tsx create mode 100644 packages/propel/src/banner/helper.tsx create mode 100644 packages/propel/src/banner/index.ts diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx index 550bfa7b8..45430c2bf 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx @@ -1,9 +1,12 @@ "use client"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; import useSWR from "swr"; // ui +import { Banner } from "@plane/propel/banner"; +import { Button } from "@plane/propel/button"; +import { ArchiveIcon } from "@plane/propel/icons"; import { Loader } from "@plane/ui"; // components import { PageHead } from "@/components/core/page-title"; @@ -16,6 +19,7 @@ import { useProject } from "@/hooks/store/use-project"; const ArchivedIssueDetailsPage = observer(() => { // router const { workspaceSlug, projectId, archivedIssueId } = useParams(); + const router = useRouter(); // states // hooks const { @@ -62,18 +66,35 @@ const ArchivedIssueDetailsPage = observer(() => {
) : ( -
-
- {workspaceSlug && projectId && archivedIssueId && ( - - )} + <> + } + action={ + + } + className="border-b border-custom-border-200" + /> +
+
+ {workspaceSlug && projectId && archivedIssueId && ( + + )} +
-
+ )} ); diff --git a/packages/propel/package.json b/packages/propel/package.json index df630fcfc..46abc585a 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -28,6 +28,10 @@ "import": "./dist/avatar/index.mjs", "require": "./dist/avatar/index.js" }, + "./banner": { + "import": "./dist/banner/index.mjs", + "require": "./dist/banner/index.js" + }, "./button": { "import": "./dist/button/index.mjs", "require": "./dist/button/index.js" diff --git a/packages/propel/src/banner/banner.stories.tsx b/packages/propel/src/banner/banner.stories.tsx new file mode 100644 index 000000000..70187155a --- /dev/null +++ b/packages/propel/src/banner/banner.stories.tsx @@ -0,0 +1,181 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Banner } from "./banner"; + +const meta = { + title: "Components/Banner", + component: Banner, + parameters: { + layout: "fullscreen", + }, + tags: ["autodocs"], + argTypes: { + variant: { + control: "select", + options: ["success", "error", "warning", "info"], + description: "Visual variant of the banner", + }, + title: { + control: "text", + description: "Banner message text", + }, + icon: { + control: false, + description: "Icon element to display before the title", + }, + action: { + control: false, + description: "Action element(s) to display on the right side", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Sample icons for different variants +const SuccessIcon = () => ( + + + + +); + +const ErrorIcon = () => ( + + + + + +); + +const WarningIcon = () => ( + + + + + +); + +const InfoIcon = () => ( + + + + + +); + +const CloseButton = ({ onClick }: { onClick?: () => void }) => ( + +); + +// ============================================================================ +// Interactive Stories +// ============================================================================ + +export const Interactive: Story = { + args: { + variant: "info", + title: "This is an interactive banner. Use the controls to customize it.", + icon: , + dismissible: true, + }, +}; + +// ============================================================================ +// Main Variants +// ============================================================================ + +export const Success: Story = { + args: { + variant: "success", + title: "Operation completed successfully", + icon: , + action: , + }, +}; + +export const Error: Story = { + args: { + variant: "error", + title: "An error occurred while processing your request", + icon: , + action: , + }, +}; + +export const Warning: Story = { + args: { + variant: "warning", + title: "Your session will expire in 5 minutes", + icon: , + action: , + }, +}; + +export const Info: Story = { + args: { + variant: "info", + title: "New features are available. Check out what's new!", + icon: , + action: , + }, +}; diff --git a/packages/propel/src/banner/banner.tsx b/packages/propel/src/banner/banner.tsx new file mode 100644 index 000000000..f3c22ee22 --- /dev/null +++ b/packages/propel/src/banner/banner.tsx @@ -0,0 +1,131 @@ +import React from "react"; +import { cn } from "../utils"; +import { + TBannerVariant, + getBannerStyling, + getBannerTitleStyling, + getBannerActionStyling, + getBannerDismissStyling, + getBannerDismissIconStyling, +} from "./helper"; + +export interface BannerProps extends Omit, "title"> { + /** Visual variant of the banner */ + variant?: TBannerVariant; + /** Icon to display before the title */ + icon?: React.ReactNode; + /** Banner title/message */ + title?: React.ReactNode; + /** Action elements to display on the right side */ + action?: React.ReactNode; + /** Whether the banner can be dismissed */ + dismissible?: boolean; + /** Callback when banner is dismissed */ + onDismiss?: () => void; + /** Whether to show the banner */ + visible?: boolean; + /** Animation duration for show/hide */ + animationDuration?: number; +} + +export const Banner = React.forwardRef( + ( + { + icon, + title, + action, + variant = "info", + dismissible = false, + onDismiss, + visible = true, + animationDuration = 200, + className, + children, + ...props + }, + ref + ) => { + // Handle dismissal + const handleDismiss = () => { + if (onDismiss) { + onDismiss(); + } + }; + + // Don't render if not visible + if (!visible) { + return null; + } + + // Get styling using helper functions + const containerStyling = getBannerStyling(variant); + const iconStyling = "flex items-center justify-center flex-shrink-0 size-5"; + const titleStyling = getBannerTitleStyling(); + const actionStyling = getBannerActionStyling(); + const dismissStyling = getBannerDismissStyling(); + const dismissIconStyling = getBannerDismissIconStyling(); + + // Render custom icon component if provided + const renderIcon = () => { + if (icon) { + return
{icon}
; + } + return null; + }; + + // Render dismiss button if dismissible + const renderDismissButton = () => { + if (!dismissible) return null; + + return ( + + ); + }; + + return ( +
+ {/* Left side: Icon and Title */} +
+ {renderIcon()} + {title &&
{title}
} + {children} +
+ + {/* Right side: Actions */} + {(action || dismissible) && ( +
+ {action} + {renderDismissButton()} +
+ )} +
+ ); + } +); + +Banner.displayName = "Banner"; + +// Export variant types for external use +export type BannerVariant = TBannerVariant; diff --git a/packages/propel/src/banner/helper.tsx b/packages/propel/src/banner/helper.tsx new file mode 100644 index 000000000..cf128a209 --- /dev/null +++ b/packages/propel/src/banner/helper.tsx @@ -0,0 +1,46 @@ +export type TBannerVariant = "success" | "error" | "warning" | "info"; + +export interface IBannerStyling { + [key: string]: string; +} + +export const bannerSizeStyling = { + container: "py-3 px-6 h-12", + icon: "w-5 h-5", + title: "text-sm", + action: "gap-2", +}; + +// TODO: update this with new color once its implemented +// Banner variant styling +export const bannerStyling: IBannerStyling = { + success: "bg-green-500/10", + error: "bg-red-500/10", + warning: "bg-yellow-500/10", + info: "bg-blue-500/10", +}; + +// Base banner styles +export const bannerBaseStyles = "flex items-center justify-between w-full transition-all duration-200"; + +// Get banner container styling +export const getBannerStyling = (variant: TBannerVariant): string => { + const variantStyles = bannerStyling[variant]; + const sizeStyles = bannerSizeStyling.container; + + return `${bannerBaseStyles} ${variantStyles} ${sizeStyles}`; +}; + +// Get title styling +export const getBannerTitleStyling = (): string => + `font-medium text-custom-text-200 flex-1 min-w-0 ${bannerSizeStyling.title}`; + +// Get action container styling +export const getBannerActionStyling = (): string => `flex items-center flex-shrink-0 ${bannerSizeStyling.action}`; + +// Get dismiss button styling +export const getBannerDismissStyling = (): string => + "rounded p-1 hover:bg-custom-background-90 transition-colors flex-shrink-0"; + +// Get dismiss icon styling +export const getBannerDismissIconStyling = (): string => "text-custom-text-200"; diff --git a/packages/propel/src/banner/index.ts b/packages/propel/src/banner/index.ts new file mode 100644 index 000000000..12d0a82b4 --- /dev/null +++ b/packages/propel/src/banner/index.ts @@ -0,0 +1,3 @@ +export { Banner } from "./banner"; +export type { BannerProps, BannerVariant } from "./banner"; +export type { TBannerVariant } from "./helper"; diff --git a/packages/propel/tsdown.config.ts b/packages/propel/tsdown.config.ts index 677fea529..d698a2b90 100644 --- a/packages/propel/tsdown.config.ts +++ b/packages/propel/tsdown.config.ts @@ -5,6 +5,7 @@ export default defineConfig({ "src/accordion/index.ts", "src/animated-counter/index.ts", "src/avatar/index.ts", + "src/banner/index.ts", "src/button/index.ts", "src/calendar/index.ts", "src/card/index.ts", From d71dfe8f86e95a8994ebad447bb5c863b3d77f17 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 24 Oct 2025 19:53:36 +0530 Subject: [PATCH 014/212] [WEB-5188 | WEB-5190] chore: layout and properties icon revamp (#7980) --- .gitignore | 3 +- .../issue-layouts/properties/due-date.tsx | 4 +- .../issue-layouts/properties/labels.tsx | 4 +- .../issue-layouts/properties/member.tsx | 8 +- .../components/issues/navbar/layout-icon.tsx | 19 +- .../issues/peek-overview/issue-properties.tsx | 11 +- .../cycles/(detail)/mobile-header.tsx | 9 +- .../cycles/(list)/mobile-header.tsx | 15 +- .../modules/(detail)/mobile-header.tsx | 9 +- .../components/issues/issue-layouts/utils.tsx | 39 ++-- apps/web/ce/components/relations/index.tsx | 6 +- .../use-work-item-filters-config.tsx | 38 ++-- .../automation/auto-close-automation.tsx | 4 +- .../actions/issue-actions/actions-list.tsx | 10 +- .../components/common/activity/helper.tsx | 32 +-- .../core/modals/user-image-upload-modal.tsx | 4 +- .../modals/workspace-image-upload-modal.tsx | 4 +- .../analytics-sidebar/sidebar-details.tsx | 6 +- .../cycles/list/cycle-list-item-action.tsx | 6 +- .../core/components/dropdowns/date-range.tsx | 5 +- .../core/components/dropdowns/estimate.tsx | 11 +- .../components/dropdowns/member/avatar.tsx | 8 +- .../inbox/content/issue-properties.tsx | 22 +- .../modals/create-modal/issue-properties.tsx | 7 +- .../components/integration/github/root.tsx | 5 +- .../core/components/integration/jira/root.tsx | 7 +- .../issue-detail-widgets/action-buttons.tsx | 6 +- .../sub-issues/issues-list/properties.tsx | 6 +- .../activity/actions/assignee.tsx | 4 +- .../activity/actions/estimate.tsx | 4 +- .../issue-activity/activity/actions/label.tsx | 4 +- .../activity/actions/parent.tsx | 4 +- .../activity/actions/priority.tsx | 4 +- .../issue-activity/activity/actions/state.tsx | 4 +- .../label/select/label-select.tsx | 5 +- .../issues/issue-detail/sidebar.tsx | 33 ++- .../issues/issue-layouts/layout-icon.tsx | 31 ++- .../properties/all-properties.tsx | 8 +- .../issue-layouts/properties/labels.tsx | 4 +- .../spreadsheet/columns/due-date-column.tsx | 4 +- .../spreadsheet/columns/start-date-column.tsx | 4 +- .../components/default-properties.tsx | 5 +- .../issues/peek-overview/properties.tsx | 33 ++- .../core/components/issues/select/base.tsx | 5 +- .../draft-issue-properties.tsx | 6 +- .../modules/analytics-sidebar/root.tsx | 8 +- .../components/modules/module-layout-icon.tsx | 12 +- .../core/components/modules/select/status.tsx | 4 +- .../modules/sidebar-select/select-status.tsx | 4 +- .../components/profile/overview/stats.tsx | 5 +- .../web/core/components/readonly/estimate.tsx | 5 +- packages/constants/src/issue/common.ts | 14 +- .../src/icons/{ => actions}/add-icon.tsx | 36 +--- packages/propel/src/icons/actions/index.ts | 1 + packages/propel/src/icons/constants.tsx | 62 ++++++ .../src/icons/cycle/cycle-group-icon.tsx | 2 +- packages/propel/src/icons/default-icon.tsx | 10 + packages/propel/src/icons/epic-icon.tsx | 28 --- packages/propel/src/icons/helpers.ts | 29 +++ packages/propel/src/icons/icon-wrapper.tsx | 42 ++++ packages/propel/src/icons/icon.tsx | 13 ++ packages/propel/src/icons/icons.stories.tsx | 196 ++++++++++++++++++ packages/propel/src/icons/index.ts | 32 ++- .../propel/src/icons/layouts/board-icon.tsx | 13 ++ .../src/icons/layouts/calendar-icon.tsx | 13 ++ .../propel/src/icons/layouts/card-icon.tsx | 13 ++ .../propel/src/icons/layouts/grid-icon.tsx | 13 ++ packages/propel/src/icons/layouts/index.ts | 7 + .../propel/src/icons/layouts/list-icon.tsx | 13 ++ .../propel/src/icons/layouts/sheet-icon.tsx | 13 ++ .../src/icons/layouts/timeline-icon.tsx | 13 ++ .../src/icons/{ => project}/cycle-icon.tsx | 36 +--- .../propel/src/icons/project/epic-icon.tsx | 13 ++ packages/propel/src/icons/project/index.ts | 7 + .../src/icons/{ => project}/intake-icon.tsx | 36 +--- .../src/icons/{ => project}/module-icon.tsx | 23 +- .../src/icons/{ => project}/page-icon.tsx | 23 +- .../src/icons/{ => project}/view-icon.tsx | 36 +--- .../src/icons/project/work-items-icon.tsx | 13 ++ .../src/icons/properties/boolean-icon.tsx | 13 ++ .../src/icons/properties/dropdown-icon.tsx | 17 ++ .../src/icons/properties/due-date-icon.tsx | 13 ++ .../src/icons/properties/duplicate-icon.tsx | 17 ++ .../src/icons/properties/estimate-icon.tsx | 13 ++ .../propel/src/icons/properties/hash-icon.tsx | 13 ++ packages/propel/src/icons/properties/index.ts | 20 ++ .../src/icons/properties/label-icon.tsx | 21 ++ .../src/icons/properties/members-icon.tsx | 13 ++ .../icons/properties/overdue-date-icon.tsx | 13 ++ .../src/icons/properties/parent-icon.tsx | 21 ++ .../src/icons/properties/priority-icon.tsx | 13 ++ .../src/icons/properties/relates-to-icon.tsx | 13 ++ .../src/icons/properties/relation-icon.tsx | 13 ++ .../src/icons/properties/scope-icon.tsx | 17 ++ .../src/icons/properties/start-date-icon.tsx | 13 ++ .../src/icons/properties/state-icon.tsx | 17 ++ .../src/icons/properties/user-circle-icon.tsx | 17 ++ .../propel/src/icons/properties/user-icon.tsx | 13 ++ .../src/icons/properties/user-square-icon.tsx | 17 ++ .../src/icons/properties/workflows-icon.tsx | 21 ++ packages/propel/src/icons/registry.ts | 115 ++++++++++ packages/propel/src/icons/sub-brand/index.ts | 3 + .../src/icons/{ => sub-brand}/pi-chat.tsx | 16 +- .../src/icons/{ => sub-brand}/plane-icon.tsx | 34 +-- .../src/icons/{ => sub-brand}/wiki-icon.tsx | 29 +-- packages/propel/src/icons/work-items-icon.tsx | 26 --- .../icons/{ => workspace}/analytics-icon.tsx | 23 +- .../icons/{ => workspace}/archive-icon.tsx | 9 +- .../icons/{ => workspace}/dashboard-icon.tsx | 23 +- .../src/icons/{ => workspace}/draft-icon.tsx | 23 +- .../src/icons/{ => workspace}/home-icon.tsx | 16 +- .../src/icons/{ => workspace}/inbox-icon.tsx | 23 +- packages/propel/src/icons/workspace/index.ts | 8 + .../icons/{ => workspace}/project-icon.tsx | 23 +- .../icons/{ => workspace}/your-work-icon.tsx | 23 +- 115 files changed, 1362 insertions(+), 618 deletions(-) rename packages/propel/src/icons/{ => actions}/add-icon.tsx (62%) create mode 100644 packages/propel/src/icons/actions/index.ts create mode 100644 packages/propel/src/icons/constants.tsx create mode 100644 packages/propel/src/icons/default-icon.tsx delete mode 100644 packages/propel/src/icons/epic-icon.tsx create mode 100644 packages/propel/src/icons/helpers.ts create mode 100644 packages/propel/src/icons/icon-wrapper.tsx create mode 100644 packages/propel/src/icons/icon.tsx create mode 100644 packages/propel/src/icons/icons.stories.tsx create mode 100644 packages/propel/src/icons/layouts/board-icon.tsx create mode 100644 packages/propel/src/icons/layouts/calendar-icon.tsx create mode 100644 packages/propel/src/icons/layouts/card-icon.tsx create mode 100644 packages/propel/src/icons/layouts/grid-icon.tsx create mode 100644 packages/propel/src/icons/layouts/index.ts create mode 100644 packages/propel/src/icons/layouts/list-icon.tsx create mode 100644 packages/propel/src/icons/layouts/sheet-icon.tsx create mode 100644 packages/propel/src/icons/layouts/timeline-icon.tsx rename packages/propel/src/icons/{ => project}/cycle-icon.tsx (52%) create mode 100644 packages/propel/src/icons/project/epic-icon.tsx create mode 100644 packages/propel/src/icons/project/index.ts rename packages/propel/src/icons/{ => project}/intake-icon.tsx (72%) rename packages/propel/src/icons/{ => project}/module-icon.tsx (91%) rename packages/propel/src/icons/{ => project}/page-icon.tsx (89%) rename packages/propel/src/icons/{ => project}/view-icon.tsx (74%) create mode 100644 packages/propel/src/icons/project/work-items-icon.tsx create mode 100644 packages/propel/src/icons/properties/boolean-icon.tsx create mode 100644 packages/propel/src/icons/properties/dropdown-icon.tsx create mode 100644 packages/propel/src/icons/properties/due-date-icon.tsx create mode 100644 packages/propel/src/icons/properties/duplicate-icon.tsx create mode 100644 packages/propel/src/icons/properties/estimate-icon.tsx create mode 100644 packages/propel/src/icons/properties/hash-icon.tsx create mode 100644 packages/propel/src/icons/properties/index.ts create mode 100644 packages/propel/src/icons/properties/label-icon.tsx create mode 100644 packages/propel/src/icons/properties/members-icon.tsx create mode 100644 packages/propel/src/icons/properties/overdue-date-icon.tsx create mode 100644 packages/propel/src/icons/properties/parent-icon.tsx create mode 100644 packages/propel/src/icons/properties/priority-icon.tsx create mode 100644 packages/propel/src/icons/properties/relates-to-icon.tsx create mode 100644 packages/propel/src/icons/properties/relation-icon.tsx create mode 100644 packages/propel/src/icons/properties/scope-icon.tsx create mode 100644 packages/propel/src/icons/properties/start-date-icon.tsx create mode 100644 packages/propel/src/icons/properties/state-icon.tsx create mode 100644 packages/propel/src/icons/properties/user-circle-icon.tsx create mode 100644 packages/propel/src/icons/properties/user-icon.tsx create mode 100644 packages/propel/src/icons/properties/user-square-icon.tsx create mode 100644 packages/propel/src/icons/properties/workflows-icon.tsx create mode 100644 packages/propel/src/icons/registry.ts create mode 100644 packages/propel/src/icons/sub-brand/index.ts rename packages/propel/src/icons/{ => sub-brand}/pi-chat.tsx (82%) rename packages/propel/src/icons/{ => sub-brand}/plane-icon.tsx (54%) rename packages/propel/src/icons/{ => sub-brand}/wiki-icon.tsx (54%) delete mode 100644 packages/propel/src/icons/work-items-icon.tsx rename packages/propel/src/icons/{ => workspace}/analytics-icon.tsx (87%) rename packages/propel/src/icons/{ => workspace}/archive-icon.tsx (89%) rename packages/propel/src/icons/{ => workspace}/dashboard-icon.tsx (86%) rename packages/propel/src/icons/{ => workspace}/draft-icon.tsx (82%) rename packages/propel/src/icons/{ => workspace}/home-icon.tsx (91%) rename packages/propel/src/icons/{ => workspace}/inbox-icon.tsx (88%) create mode 100644 packages/propel/src/icons/workspace/index.ts rename packages/propel/src/icons/{ => workspace}/project-icon.tsx (91%) rename packages/propel/src/icons/{ => workspace}/your-work-icon.tsx (81%) diff --git a/.gitignore b/.gitignore index 053db6d29..4a756d030 100644 --- a/.gitignore +++ b/.gitignore @@ -102,5 +102,6 @@ dev-editor storybook-static CLAUDE.md +AGENTS.md -temp/ \ No newline at end of file +temp/ diff --git a/apps/space/core/components/issues/issue-layouts/properties/due-date.tsx b/apps/space/core/components/issues/issue-layouts/properties/due-date.tsx index 2f1669837..71afb2489 100644 --- a/apps/space/core/components/issues/issue-layouts/properties/due-date.tsx +++ b/apps/space/core/components/issues/issue-layouts/properties/due-date.tsx @@ -1,7 +1,7 @@ "use client"; import { observer } from "mobx-react"; -import { CalendarCheck2 } from "lucide-react"; +import { DueDatePropertyIcon } from "@plane/propel/icons"; import { Tooltip } from "@plane/propel/tooltip"; import { cn } from "@plane/utils"; // helpers @@ -33,7 +33,7 @@ export const IssueBlockDate = observer((props: Props) => { "border-[0.5px] border-custom-border-300": shouldShowBorder, })} > - + {formattedDate ? formattedDate : "No Date"}
diff --git a/apps/space/core/components/issues/issue-layouts/properties/labels.tsx b/apps/space/core/components/issues/issue-layouts/properties/labels.tsx index 12ed76d3a..9e4b80fff 100644 --- a/apps/space/core/components/issues/issue-layouts/properties/labels.tsx +++ b/apps/space/core/components/issues/issue-layouts/properties/labels.tsx @@ -1,7 +1,7 @@ "use client"; import { observer } from "mobx-react"; -import { Tags } from "lucide-react"; +import { LabelPropertyIcon } from "@plane/propel/icons"; // plane imports import { Tooltip } from "@plane/propel/tooltip"; // hooks @@ -25,7 +25,7 @@ export const IssueBlockLabels = observer(({ labelIds, shouldShowLabel = false }:
- + {shouldShowLabel && No Labels}
diff --git a/apps/space/core/components/issues/issue-layouts/properties/member.tsx b/apps/space/core/components/issues/issue-layouts/properties/member.tsx index a5baae8a3..6b1bce1f9 100644 --- a/apps/space/core/components/issues/issue-layouts/properties/member.tsx +++ b/apps/space/core/components/issues/issue-layouts/properties/member.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; // icons import type { LucideIcon } from "lucide-react"; -import { Users } from "lucide-react"; +import { MembersPropertyIcon } from "@plane/propel/icons"; // plane ui import { Avatar, AvatarGroup } from "@plane/ui"; // plane utils @@ -49,7 +49,11 @@ export const ButtonAvatars: React.FC = observer((props: AvatarProps } } - return Icon ? : ; + return Icon ? ( + + ) : ( + + ); }); export const IssueBlockMembers = observer(({ memberIds, shouldShowBorder = true }: Props) => { diff --git a/apps/space/core/components/issues/navbar/layout-icon.tsx b/apps/space/core/components/issues/navbar/layout-icon.tsx index e9aed2b26..2d5a5db5a 100644 --- a/apps/space/core/components/issues/navbar/layout-icon.tsx +++ b/apps/space/core/components/issues/navbar/layout-icon.tsx @@ -1,13 +1,22 @@ -import type { LucideProps } from "lucide-react"; -import { List, Kanban } from "lucide-react"; import type { TIssueLayout } from "@plane/constants"; +import { ListLayoutIcon, BoardLayoutIcon } from "@plane/propel/icons"; +import type { ISvgIcons } from "@plane/propel/icons"; + +export const IssueLayoutIcon = ({ + layout, + size, + ...props +}: { layout: TIssueLayout; size?: number } & Omit) => { + const iconProps = { + ...props, + ...(size && { width: size, height: size }), + }; -export const IssueLayoutIcon = ({ layout, ...props }: { layout: TIssueLayout } & LucideProps) => { switch (layout) { case "list": - return ; + return ; case "kanban": - return ; + return ; default: return null; } diff --git a/apps/space/core/components/issues/peek-overview/issue-properties.tsx b/apps/space/core/components/issues/peek-overview/issue-properties.tsx index 6ea937d19..94507922a 100644 --- a/apps/space/core/components/issues/peek-overview/issue-properties.tsx +++ b/apps/space/core/components/issues/peek-overview/issue-properties.tsx @@ -2,10 +2,9 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import { CalendarCheck2, Signal } from "lucide-react"; // plane imports import { useTranslation } from "@plane/i18n"; -import { DoubleCircleIcon, StateGroupIcon } from "@plane/propel/icons"; +import { StatePropertyIcon, StateGroupIcon, PriorityPropertyIcon, DueDatePropertyIcon } from "@plane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { cn, getIssuePriorityFilters } from "@plane/utils"; // components @@ -66,7 +65,7 @@ export const PeekOverviewIssueProperties: React.FC = observer(({ issueDet
- + State
@@ -77,7 +76,7 @@ export const PeekOverviewIssueProperties: React.FC = observer(({ issueDet
- + Priority
@@ -106,7 +105,7 @@ export const PeekOverviewIssueProperties: React.FC = observer(({ issueDet
- + Due date
@@ -116,7 +115,7 @@ export const PeekOverviewIssueProperties: React.FC = observer(({ issueDet "text-red-500": shouldHighlightIssueDueDate(issueDetails.target_date, state?.group), })} > - + {renderFormattedDate(issueDetails.target_date)}
) : ( diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx index f56a32079..747e65c16 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx @@ -4,10 +4,11 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // icons -import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; +import { ChevronDown } from "lucide-react"; // plane imports import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { CalendarLayoutIcon, BoardLayoutIcon, ListLayoutIcon } from "@plane/propel/icons"; import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types"; import { EIssuesStoreType } from "@plane/types"; import { CustomMenu } from "@plane/ui"; @@ -21,9 +22,9 @@ import { useIssues } from "@/hooks/store/use-issues"; import { useProject } from "@/hooks/store/use-project"; const SUPPORTED_LAYOUTS = [ - { key: "list", titleTranslationKey: "issue.layouts.list", icon: List }, - { key: "kanban", titleTranslationKey: "issue.layouts.kanban", icon: Kanban }, - { key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: Calendar }, + { key: "list", titleTranslationKey: "issue.layouts.list", icon: ListLayoutIcon }, + { key: "kanban", titleTranslationKey: "issue.layouts.kanban", icon: BoardLayoutIcon }, + { key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: CalendarLayoutIcon }, ]; export const CycleIssuesMobileHeader = observer(() => { diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/mobile-header.tsx index 97838d349..4c8e4e9c4 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/mobile-header.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/mobile-header.tsx @@ -1,9 +1,10 @@ "use client"; +import type React from "react"; import { observer } from "mobx-react"; // ui -import { GanttChartSquare, LayoutGrid, List } from "lucide-react"; -import type { LucideIcon } from "lucide-react"; +import type { ISvgIcons } from "@plane/propel/icons"; +import { TimelineLayoutIcon, GridLayoutIcon, ListLayoutIcon } from "@plane/propel/icons"; // plane package imports import type { TCycleLayoutOptions } from "@plane/types"; import { CustomMenu } from "@plane/ui"; @@ -13,22 +14,22 @@ import { useProject } from "@/hooks/store/use-project"; const CYCLE_VIEW_LAYOUTS: { key: TCycleLayoutOptions; - icon: LucideIcon; + icon: React.FC; title: string; }[] = [ { key: "list", - icon: List, + icon: ListLayoutIcon, title: "List layout", }, { key: "board", - icon: LayoutGrid, + icon: GridLayoutIcon, title: "Gallery layout", }, { key: "gantt", - icon: GanttChartSquare, + icon: TimelineLayoutIcon, title: "Timeline layout", }, ]; @@ -45,7 +46,7 @@ export const CyclesListMobileHeader = observer(() => { // placement="bottom-start" customButton={ - + Layout } diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx index 77b0e2b24..ae450f5de 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx @@ -4,10 +4,11 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // icons -import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; +import { ChevronDown } from "lucide-react"; // plane imports import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { CalendarLayoutIcon, BoardLayoutIcon, ListLayoutIcon } from "@plane/propel/icons"; import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types"; import { EIssuesStoreType } from "@plane/types"; import { CustomMenu } from "@plane/ui"; @@ -21,9 +22,9 @@ import { useModule } from "@/hooks/store/use-module"; import { useProject } from "@/hooks/store/use-project"; const SUPPORTED_LAYOUTS = [ - { key: "list", i18n_title: "issue.layouts.list", icon: List }, - { key: "kanban", i18n_title: "issue.layouts.kanban", icon: Kanban }, - { key: "calendar", i18n_title: "issue.layouts.calendar", icon: Calendar }, + { key: "list", i18n_title: "issue.layouts.list", icon: ListLayoutIcon }, + { key: "kanban", i18n_title: "issue.layouts.kanban", icon: BoardLayoutIcon }, + { key: "calendar", i18n_title: "issue.layouts.calendar", icon: CalendarLayoutIcon }, ]; export const ModuleIssuesMobileHeader = observer(() => { diff --git a/apps/web/ce/components/issues/issue-layouts/utils.tsx b/apps/web/ce/components/issues/issue-layouts/utils.tsx index 61a32c4fd..2d2a1deb8 100644 --- a/apps/web/ce/components/issues/issue-layouts/utils.tsx +++ b/apps/web/ce/components/issues/issue-layouts/utils.tsx @@ -1,19 +1,18 @@ import type { FC } from "react"; -import { - CalendarCheck2, - CalendarClock, - CalendarDays, - LayersIcon, - Link2, - Paperclip, - Signal, - Tag, - Triangle, - Users, -} from "lucide-react"; +import { CalendarDays, LayersIcon, Link2, Paperclip } from "lucide-react"; // types import type { ISvgIcons } from "@plane/propel/icons"; -import { CycleIcon, DoubleCircleIcon, ModuleIcon } from "@plane/propel/icons"; +import { + CycleIcon, + StatePropertyIcon, + ModuleIcon, + MembersPropertyIcon, + DueDatePropertyIcon, + EstimatePropertyIcon, + LabelPropertyIcon, + PriorityPropertyIcon, + StartDatePropertyIcon, +} from "@plane/propel/icons"; import type { IGroupByColumn, IIssueDisplayProperties, TGetColumns, TSpreadsheetColumn } from "@plane/types"; // components import { @@ -66,16 +65,16 @@ export const getScopeMemberIds = ({ isWorkspaceLevel, projectId }: TGetColumns): export const getTeamProjectColumns = (): IGroupByColumn[] | undefined => undefined; export const SpreadSheetPropertyIconMap: Record> = { - Users: Users, + MembersPropertyIcon: MembersPropertyIcon, CalenderDays: CalendarDays, - CalenderCheck2: CalendarCheck2, - Triangle: Triangle, - Tag: Tag, + DueDatePropertyIcon: DueDatePropertyIcon, + EstimatePropertyIcon: EstimatePropertyIcon, + LabelPropertyIcon: LabelPropertyIcon, ModuleIcon: ModuleIcon, ContrastIcon: CycleIcon, - Signal: Signal, - CalendarClock: CalendarClock, - DoubleCircleIcon: DoubleCircleIcon, + PriorityPropertyIcon: PriorityPropertyIcon, + StartDatePropertyIcon: StartDatePropertyIcon, + StatePropertyIcon: StatePropertyIcon, Link2: Link2, Paperclip: Paperclip, LayersIcon: LayersIcon, diff --git a/apps/web/ce/components/relations/index.tsx b/apps/web/ce/components/relations/index.tsx index 2a7ebf0eb..f9d72944c 100644 --- a/apps/web/ce/components/relations/index.tsx +++ b/apps/web/ce/components/relations/index.tsx @@ -1,5 +1,5 @@ -import { CircleDot, CopyPlus, XCircle } from "lucide-react"; -import { RelatedIcon } from "@plane/propel/icons"; +import { CircleDot, XCircle } from "lucide-react"; +import { RelatedIcon, DuplicatePropertyIcon } from "@plane/propel/icons"; import type { TRelationObject } from "@/components/issues/issue-detail-widgets/relations"; import type { TIssueRelationTypes } from "../../types"; @@ -17,7 +17,7 @@ export const ISSUE_RELATION_OPTIONS: Record , + icon: (size) => , placeholder: "None", }, blocked_by: { diff --git a/apps/web/ce/hooks/work-item-filters/use-work-item-filters-config.tsx b/apps/web/ce/hooks/work-item-filters/use-work-item-filters-config.tsx index edc6f54fa..90ad9394d 100644 --- a/apps/web/ce/hooks/work-item-filters/use-work-item-filters-config.tsx +++ b/apps/web/ce/hooks/work-item-filters/use-work-item-filters-config.tsx @@ -1,23 +1,19 @@ import { useCallback, useMemo } from "react"; -import { - AtSign, - Briefcase, - Calendar, - CalendarCheck2, - CalendarClock, - CircleUserRound, - SignalHigh, - Tag, - Users, -} from "lucide-react"; +import { AtSign, Briefcase, Calendar } from "lucide-react"; // plane imports import { CycleGroupIcon, CycleIcon, ModuleIcon, - DoubleCircleIcon, + StatePropertyIcon, PriorityIcon, StateGroupIcon, + MembersPropertyIcon, + LabelPropertyIcon, + StartDatePropertyIcon, + DueDatePropertyIcon, + UserCirclePropertyIcon, + PriorityPropertyIcon, } from "@plane/propel/icons"; import type { ICycle, @@ -149,7 +145,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps): () => getStateGroupFilterConfig("state_group")({ isEnabled: isFilterEnabled("state_group"), - filterIcon: DoubleCircleIcon, + filterIcon: StatePropertyIcon, getOptionIcon: (stateGroupKey) => , ...operatorConfigs, }), @@ -161,7 +157,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps): () => getStateFilterConfig("state_id")({ isEnabled: isFilterEnabled("state_id") && workItemStates !== undefined, - filterIcon: DoubleCircleIcon, + filterIcon: StatePropertyIcon, getOptionIcon: (state) => , states: workItemStates ?? [], ...operatorConfigs, @@ -174,7 +170,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps): () => getLabelFilterConfig("label_id")({ isEnabled: isFilterEnabled("label_id") && workItemLabels !== undefined, - filterIcon: Tag, + filterIcon: LabelPropertyIcon, labels: workItemLabels ?? [], getOptionIcon: (color) => ( @@ -215,7 +211,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps): () => getAssigneeFilterConfig("assignee_id")({ isEnabled: isFilterEnabled("assignee_id") && members !== undefined, - filterIcon: Users, + filterIcon: MembersPropertyIcon, members: members ?? [], getOptionIcon: (memberDetails) => ( getCreatedByFilterConfig("created_by_id")({ isEnabled: isFilterEnabled("created_by_id") && members !== undefined, - filterIcon: CircleUserRound, + filterIcon: UserCirclePropertyIcon, members: members ?? [], getOptionIcon: (memberDetails) => ( getSubscriberFilterConfig("subscriber_id")({ isEnabled: isFilterEnabled("subscriber_id") && members !== undefined, - filterIcon: Users, + filterIcon: MembersPropertyIcon, members: members ?? [], getOptionIcon: (memberDetails) => ( getPriorityFilterConfig("priority")({ isEnabled: isFilterEnabled("priority"), - filterIcon: SignalHigh, + filterIcon: PriorityPropertyIcon, getOptionIcon: (priority) => , ...operatorConfigs, }), @@ -307,7 +303,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps): () => getStartDateFilterConfig("start_date")({ isEnabled: true, - filterIcon: CalendarClock, + filterIcon: StartDatePropertyIcon, ...operatorConfigs, }), [operatorConfigs] @@ -318,7 +314,7 @@ export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps): () => getTargetDateFilterConfig("target_date")({ isEnabled: true, - filterIcon: CalendarCheck2, + filterIcon: DueDatePropertyIcon, ...operatorConfigs, }), [operatorConfigs] diff --git a/apps/web/core/components/automation/auto-close-automation.tsx b/apps/web/core/components/automation/auto-close-automation.tsx index f493f16ae..19e294a22 100644 --- a/apps/web/core/components/automation/auto-close-automation.tsx +++ b/apps/web/core/components/automation/auto-close-automation.tsx @@ -15,7 +15,7 @@ import { PROJECT_SETTINGS_TRACKER_EVENTS, } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { StateGroupIcon, DoubleCircleIcon } from "@plane/propel/icons"; +import { StateGroupIcon, StatePropertyIcon } from "@plane/propel/icons"; import type { IProject } from "@plane/types"; // ui import { CustomSelect, CustomSearchSelect, ToggleSwitch, Loader } from "@plane/ui"; @@ -188,7 +188,7 @@ export const AutoCloseAutomation: React.FC = observer((props) => { size={EIconSize.LG} /> ) : ( - + )} {selectedOption?.name ? selectedOption.name diff --git a/apps/web/core/components/command-palette/actions/issue-actions/actions-list.tsx b/apps/web/core/components/command-palette/actions/issue-actions/actions-list.tsx index 0af68d137..eefe3dabc 100644 --- a/apps/web/core/components/command-palette/actions/issue-actions/actions-list.tsx +++ b/apps/web/core/components/command-palette/actions/issue-actions/actions-list.tsx @@ -3,8 +3,8 @@ import { Command } from "cmdk"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2, Users } from "lucide-react"; -import { DoubleCircleIcon } from "@plane/propel/icons"; +import { LinkIcon, Trash2, UserMinus2, UserPlus2 } from "lucide-react"; +import { StatePropertyIcon, PriorityPropertyIcon, MembersPropertyIcon } from "@plane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { TIssue } from "@plane/types"; import { EIssueServiceType } from "@plane/types"; @@ -90,7 +90,7 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { className="focus:outline-none" >
- + Change state...
@@ -103,7 +103,7 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { className="focus:outline-none" >
- + Change priority...
@@ -116,7 +116,7 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { className="focus:outline-none" >
- + Assign to...
diff --git a/apps/web/core/components/common/activity/helper.tsx b/apps/web/core/components/common/activity/helper.tsx index 48cc39b87..8c656f33e 100644 --- a/apps/web/core/components/common/activity/helper.tsx +++ b/apps/web/core/components/common/activity/helper.tsx @@ -1,17 +1,13 @@ import type { ReactNode } from "react"; import { - Signal, RotateCcw, Network, Link as LinkIcon, Calendar, - Tag, Inbox, AlignLeft, - Users, Paperclip, Type, - Triangle, FileText, Globe, Hash, @@ -25,7 +21,19 @@ import { } from "lucide-react"; // components -import { ArchiveIcon, CycleIcon, DoubleCircleIcon, IntakeIcon, ModuleIcon } from "@plane/propel/icons"; +import { + ArchiveIcon, + CycleIcon, + StatePropertyIcon, + IntakeIcon, + ModuleIcon, + PriorityPropertyIcon, + StartDatePropertyIcon, + DueDatePropertyIcon, + LabelPropertyIcon, + MembersPropertyIcon, + EstimatePropertyIcon, +} from "@plane/propel/icons"; import { store } from "@/lib/store-context"; import type { TProjectActivity } from "@/plane-web/types"; @@ -33,20 +41,20 @@ type ActivityIconMap = { [key: string]: ReactNode; }; export const iconsMap: ActivityIconMap = { - priority: , + priority: , archived_at: , restored: , link: , - start_date: , - target_date: , - label: , + start_date: , + target_date: , + label: , inbox: , description: , - assignee: , + assignee: , attachment: , name: , - state: , - estimate: , + state: , + estimate: , cycle: , module: , page: , diff --git a/apps/web/core/components/core/modals/user-image-upload-modal.tsx b/apps/web/core/components/core/modals/user-image-upload-modal.tsx index cade52096..e12ca566e 100644 --- a/apps/web/core/components/core/modals/user-image-upload-modal.tsx +++ b/apps/web/core/components/core/modals/user-image-upload-modal.tsx @@ -3,11 +3,11 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; import { useDropzone } from "react-dropzone"; -import { UserCircle2 } from "lucide-react"; import { Transition, Dialog } from "@headlessui/react"; // plane imports import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants"; import { Button } from "@plane/propel/button"; +import { UserCirclePropertyIcon } from "@plane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { EFileAssetType } from "@plane/types"; import { getAssetIdFromUrl, getFileURL, checkURLValidity } from "@plane/utils"; @@ -146,7 +146,7 @@ export const UserImageUploadModal: React.FC = observer((props) => { ) : (
- + {isDragActive ? "Drop image here to upload" : "Drag & drop image here"} diff --git a/apps/web/core/components/core/modals/workspace-image-upload-modal.tsx b/apps/web/core/components/core/modals/workspace-image-upload-modal.tsx index 496319f75..decf80470 100644 --- a/apps/web/core/components/core/modals/workspace-image-upload-modal.tsx +++ b/apps/web/core/components/core/modals/workspace-image-upload-modal.tsx @@ -3,11 +3,11 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { useDropzone } from "react-dropzone"; -import { UserCircle2 } from "lucide-react"; import { Transition, Dialog } from "@headlessui/react"; // plane imports import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants"; import { Button } from "@plane/propel/button"; +import { UserCirclePropertyIcon } from "@plane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { EFileAssetType } from "@plane/types"; import { getAssetIdFromUrl, getFileURL, checkURLValidity } from "@plane/utils"; @@ -158,7 +158,7 @@ export const WorkspaceImageUploadModal: React.FC = observer((props) => { ) : (
- + {isDragActive ? "Drop image here to upload" : "Drag & drop image here"} diff --git a/apps/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx b/apps/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx index e3e83216a..7c6e6b4ed 100644 --- a/apps/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx +++ b/apps/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx @@ -3,11 +3,11 @@ import type { FC } from "react"; import React from "react"; import { isEmpty } from "lodash-es"; import { observer } from "mobx-react"; -import { SquareUser, Users } from "lucide-react"; +import { SquareUser } from "lucide-react"; // plane types import { EEstimateSystem } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { WorkItemsIcon } from "@plane/propel/icons"; +import { MembersPropertyIcon, WorkItemsIcon } from "@plane/propel/icons"; import type { ICycle } from "@plane/types"; // plane ui import { Avatar, AvatarGroup, TextArea } from "@plane/ui"; @@ -87,7 +87,7 @@ export const CycleSidebarDetails: FC = observer((props) => {
- + {t("members")}
diff --git a/apps/web/core/components/cycles/list/cycle-list-item-action.tsx b/apps/web/core/components/cycles/list/cycle-list-item-action.tsx index 9a3e256b0..efcc22583 100644 --- a/apps/web/core/components/cycles/list/cycle-list-item-action.tsx +++ b/apps/web/core/components/cycles/list/cycle-list-item-action.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { observer } from "mobx-react"; import { useParams, usePathname, useSearchParams } from "next/navigation"; import { useForm } from "react-hook-form"; -import { Eye, Users, ArrowRight, CalendarDays } from "lucide-react"; +import { Eye, ArrowRight, CalendarDays } from "lucide-react"; // plane imports import { CYCLE_TRACKER_EVENTS, @@ -16,7 +16,7 @@ import { } from "@plane/constants"; import { useLocalStorage } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; -import { TransferIcon, WorkItemsIcon } from "@plane/propel/icons"; +import { TransferIcon, WorkItemsIcon, MembersPropertyIcon } from "@plane/propel/icons"; import { setPromiseToast } from "@plane/propel/toast"; import { Tooltip } from "@plane/propel/tooltip"; import type { ICycle, TCycleGroups } from "@plane/types"; @@ -314,7 +314,7 @@ export const CycleListItemAction: FC = observer((props) => { })} ) : ( - + )}
diff --git a/apps/web/core/components/dropdowns/date-range.tsx b/apps/web/core/components/dropdowns/date-range.tsx index 83f0c904e..69e74c05c 100644 --- a/apps/web/core/components/dropdowns/date-range.tsx +++ b/apps/web/core/components/dropdowns/date-range.tsx @@ -5,13 +5,14 @@ import type { Placement } from "@popperjs/core"; import { observer } from "mobx-react"; import { createPortal } from "react-dom"; import { usePopper } from "react-popper"; -import { ArrowRight, CalendarCheck2, CalendarDays, X } from "lucide-react"; +import { ArrowRight, CalendarDays, X } from "lucide-react"; import { Combobox } from "@headlessui/react"; // plane imports import { useTranslation } from "@plane/i18n"; // ui import type { DateRange, Matcher } from "@plane/propel/calendar"; import { Calendar } from "@plane/propel/calendar"; +import { DueDatePropertyIcon } from "@plane/propel/icons"; import { ComboDropDown } from "@plane/ui"; import { cn, renderFormattedDate } from "@plane/utils"; // helpers @@ -236,7 +237,7 @@ export const DateRangeDropdown: React.FC = observer((props) => { buttonToDateClassName )} > - {!hideIcon.to && } + {!hideIcon.to && } {dateRange.to ? renderFormattedDate(dateRange.to) : renderPlaceholder ? placeholder.to : ""} {isClearable && !disabled && hasDisplayedDates && ( diff --git a/apps/web/core/components/dropdowns/estimate.tsx b/apps/web/core/components/dropdowns/estimate.tsx index 3a8ec9174..2888183eb 100644 --- a/apps/web/core/components/dropdowns/estimate.tsx +++ b/apps/web/core/components/dropdowns/estimate.tsx @@ -3,10 +3,11 @@ import { useRef, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { usePopper } from "react-popper"; -import { Check, ChevronDown, Search, Triangle } from "lucide-react"; +import { Check, ChevronDown, Search } from "lucide-react"; import { Combobox } from "@headlessui/react"; // plane imports import { useTranslation } from "@plane/i18n"; +import { EstimatePropertyIcon } from "@plane/propel/icons"; import { EEstimateSystem } from "@plane/types"; import { ComboDropDown } from "@plane/ui"; import { convertMinutesToHoursMinutesString, cn } from "@plane/utils"; @@ -104,7 +105,7 @@ export const EstimateDropdown: React.FC = observer((props) => { query: `${currentEstimatePoint?.value}`, content: (
- + {currentActiveEstimate?.type === EEstimateSystem.TIME ? convertMinutesToHoursMinutesString(Number(currentEstimatePoint.value)) @@ -121,7 +122,7 @@ export const EstimateDropdown: React.FC = observer((props) => { query: t("project_settings.estimates.no_estimate"), content: (
- + {t("project_settings.estimates.no_estimate")}
), @@ -189,7 +190,7 @@ export const EstimateDropdown: React.FC = observer((props) => { variant={buttonVariant} renderToolTipByDefault={renderByDefault} > - {!hideIcon && } + {!hideIcon && } {(selectedEstimate || placeholder) && BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( {selectedEstimate @@ -249,7 +250,7 @@ export const EstimateDropdown: React.FC = observer((props) => { > {/* NOTE: This condition renders when estimates are not enabled for the project */}
- + {t("project_settings.estimates.no_estimate")}
diff --git a/apps/web/core/components/dropdowns/member/avatar.tsx b/apps/web/core/components/dropdowns/member/avatar.tsx index 5dc01268a..826369abb 100644 --- a/apps/web/core/components/dropdowns/member/avatar.tsx +++ b/apps/web/core/components/dropdowns/member/avatar.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react"; import type { LucideIcon } from "lucide-react"; -import { Users } from "lucide-react"; +import { MembersPropertyIcon } from "@plane/propel/icons"; // plane ui import { Avatar, AvatarGroup } from "@plane/ui"; import { cn, getFileURL } from "@plane/utils"; @@ -49,5 +49,9 @@ export const ButtonAvatars: React.FC = observer((props) => { } } - return Icon ? : ; + return Icon ? ( + + ) : ( + + ); }); diff --git a/apps/web/core/components/inbox/content/issue-properties.tsx b/apps/web/core/components/inbox/content/issue-properties.tsx index 41a1677d5..c1df42157 100644 --- a/apps/web/core/components/inbox/content/issue-properties.tsx +++ b/apps/web/core/components/inbox/content/issue-properties.tsx @@ -2,8 +2,14 @@ import React from "react"; import { observer } from "mobx-react"; -import { CalendarCheck2, CopyPlus, Signal, Tag, Users } from "lucide-react"; -import { DoubleCircleIcon } from "@plane/propel/icons"; +import { + StatePropertyIcon, + MembersPropertyIcon, + PriorityPropertyIcon, + DueDatePropertyIcon, + LabelPropertyIcon, + DuplicatePropertyIcon, +} from "@plane/propel/icons"; import { Tooltip } from "@plane/propel/tooltip"; import type { TInboxDuplicateIssueDetails, TIssue } from "@plane/types"; import { ControlLink } from "@plane/ui"; @@ -56,7 +62,7 @@ export const InboxIssueContentProperties: React.FC = observer((props) => {/* State */}
- + State
{issue?.state_id && ( @@ -79,7 +85,7 @@ export const InboxIssueContentProperties: React.FC = observer((props) => {/* Assignee */}
- + Assignees
= observer((props) => {/* Priority */}
- + Priority
= observer((props) => {/* Due Date */}
- + Due date
= observer((props) => {/* Labels */}
- + Labels
@@ -177,7 +183,7 @@ export const InboxIssueContentProperties: React.FC = observer((props) => {duplicateIssueDetails && (
- + Duplicate of
diff --git a/apps/web/core/components/inbox/modals/create-modal/issue-properties.tsx b/apps/web/core/components/inbox/modals/create-modal/issue-properties.tsx index 92b823d3a..3d7c5abba 100644 --- a/apps/web/core/components/inbox/modals/create-modal/issue-properties.tsx +++ b/apps/web/core/components/inbox/modals/create-modal/issue-properties.tsx @@ -1,9 +1,8 @@ import type { FC } from "react"; import { useState } from "react"; import { observer } from "mobx-react"; -import { LayoutPanelTop } from "lucide-react"; -// plane imports import { ETabIndices } from "@plane/constants"; +import { ParentPropertyIcon } from "@plane/propel/icons"; import type { ISearchIssueResponse, TIssue } from "@plane/types"; import { CustomMenu } from "@plane/ui"; import { renderFormattedPayloadDate, getDate, getTabIndex } from "@plane/utils"; @@ -177,7 +176,7 @@ export const InboxIssueProperties: FC = observer((props) type="button" className="flex cursor-pointer items-center justify-between gap-1 h-full rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 text-xs hover:bg-custom-background-80" > - + {selectedParentIssue ? `${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}` @@ -211,7 +210,7 @@ export const InboxIssueProperties: FC = observer((props) className="flex cursor-pointer items-center justify-between gap-1 h-full rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 text-xs hover:bg-custom-background-80" onClick={() => setParentIssueModalOpen(true)} > - + Add parent )} diff --git a/apps/web/core/components/integration/github/root.tsx b/apps/web/core/components/integration/github/root.tsx index 1a8fbb472..cccef5da4 100644 --- a/apps/web/core/components/integration/github/root.tsx +++ b/apps/web/core/components/integration/github/root.tsx @@ -6,7 +6,8 @@ import Link from "next/link"; import { useParams, useSearchParams } from "next/navigation"; import { useForm } from "react-hook-form"; import useSWR, { mutate } from "swr"; -import { ArrowLeft, Check, List, Settings, UploadCloud, Users } from "lucide-react"; +import { ArrowLeft, Check, List, Settings, UploadCloud } from "lucide-react"; +import { MembersPropertyIcon } from "@plane/propel/icons"; // types import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { IGithubRepoCollaborator, IGithubServiceImportFormData } from "@plane/types"; @@ -68,7 +69,7 @@ const integrationWorkflowData = [ { title: "Users", key: "import-users", - icon: Users, + icon: MembersPropertyIcon, }, { title: "Confirm", diff --git a/apps/web/core/components/integration/jira/root.tsx b/apps/web/core/components/integration/jira/root.tsx index ed9fa09ba..f2d5c9955 100644 --- a/apps/web/core/components/integration/jira/root.tsx +++ b/apps/web/core/components/integration/jira/root.tsx @@ -7,9 +7,10 @@ import { useParams } from "next/navigation"; import { FormProvider, useForm } from "react-hook-form"; import { mutate } from "swr"; // icons -import { ArrowLeft, Check, List, Settings, Users } from "lucide-react"; -// types +import { ArrowLeft, Check, List, Settings } from "lucide-react"; import { Button } from "@plane/propel/button"; +import { MembersPropertyIcon } from "@plane/propel/icons"; +// types import type { IJiraImporterForm } from "@plane/types"; // ui // fetch keys @@ -42,7 +43,7 @@ const integrationWorkflowData: Array<{ { title: "Users", key: "import-users", - icon: Users, + icon: MembersPropertyIcon, }, { title: "Confirm", diff --git a/apps/web/core/components/issues/issue-detail-widgets/action-buttons.tsx b/apps/web/core/components/issues/issue-detail-widgets/action-buttons.tsx index ae8387efc..ec4cba9aa 100644 --- a/apps/web/core/components/issues/issue-detail-widgets/action-buttons.tsx +++ b/apps/web/core/components/issues/issue-detail-widgets/action-buttons.tsx @@ -2,9 +2,9 @@ import type { FC } from "react"; import React from "react"; -import { Link, Paperclip, Waypoints } from "lucide-react"; +import { Link, Paperclip } from "lucide-react"; import { useTranslation } from "@plane/i18n"; -import { ViewsIcon } from "@plane/propel/icons"; +import { ViewsIcon, RelationPropertyIcon } from "@plane/propel/icons"; // plane imports import type { TIssueServiceType, TWorkItemWidgets } from "@plane/types"; // plane web imports @@ -52,7 +52,7 @@ export const IssueDetailWidgetActionButtons: FC = (props) => { customButton={ } + icon={} disabled={disabled} /> } diff --git a/apps/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx b/apps/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx index 6295c8538..35097a298 100644 --- a/apps/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx +++ b/apps/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx @@ -2,8 +2,8 @@ import type { SyntheticEvent } from "react"; import { useMemo } from "react"; import { observer } from "mobx-react"; -import { CalendarCheck2, CalendarClock } from "lucide-react"; import { useTranslation } from "@plane/i18n"; +import { StartDatePropertyIcon, DueDatePropertyIcon } from "@plane/propel/icons"; import type { IIssueDisplayProperties, TIssue } from "@plane/types"; import { getDate, renderFormattedPayloadDate, shouldHighlightIssueDueDate } from "@plane/utils"; // components @@ -165,7 +165,7 @@ export const SubIssuesListItemProperties: React.FC = observer((props) => onChange={handleStartDate} maxDate={maxDate} placeholder={t("common.order_by.start_date")} - icon={} + icon={} buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} optionsClassName="z-30" disabled={!canEdit} @@ -186,7 +186,7 @@ export const SubIssuesListItemProperties: React.FC = observer((props) => onChange={handleTargetDate} minDate={minDate} placeholder={t("common.order_by.due_date")} - icon={} + icon={} buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} buttonClassName={shouldHighlight ? "text-red-500" : ""} clearIconClassName="text-custom-text-100" diff --git a/apps/web/core/components/issues/issue-detail/issue-activity/activity/actions/assignee.tsx b/apps/web/core/components/issues/issue-detail/issue-activity/activity/actions/assignee.tsx index 97294839f..5c4466f8a 100644 --- a/apps/web/core/components/issues/issue-detail/issue-activity/activity/actions/assignee.tsx +++ b/apps/web/core/components/issues/issue-detail/issue-activity/activity/actions/assignee.tsx @@ -1,7 +1,7 @@ import type { FC } from "react"; import { observer } from "mobx-react"; // icons -import { Users } from "lucide-react"; +import { MembersPropertyIcon } from "@plane/propel/icons"; // hooks; import { useIssueDetail } from "@/hooks/store/use-issue-detail"; // components @@ -21,7 +21,7 @@ export const IssueAssigneeActivity: FC = observer((props if (!activity) return <>; return ( } + icon={} activityId={activityId} ends={ends} > diff --git a/apps/web/core/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx b/apps/web/core/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx index 8c43a2171..ada9b5237 100644 --- a/apps/web/core/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx +++ b/apps/web/core/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx @@ -1,6 +1,6 @@ import type { FC } from "react"; import { observer } from "mobx-react"; -import { Triangle } from "lucide-react"; +import { EstimatePropertyIcon } from "@plane/propel/icons"; // hooks import { useIssueDetail } from "@/hooks/store/use-issue-detail"; // components @@ -21,7 +21,7 @@ export const IssueEstimateActivity: FC = observer((props return (
diff --git a/apps/web/core/components/issues/issue-detail/sidebar.tsx b/apps/web/core/components/issues/issue-detail/sidebar.tsx index f93fb67e1..4c6fc3837 100644 --- a/apps/web/core/components/issues/issue-detail/sidebar.tsx +++ b/apps/web/core/components/issues/issue-detail/sidebar.tsx @@ -2,11 +2,22 @@ import React from "react"; import { observer } from "mobx-react"; -import { CalendarCheck2, CalendarClock, LayoutPanelTop, Signal, Tag, Triangle, UserCircle2, Users } from "lucide-react"; // i18n import { useTranslation } from "@plane/i18n"; // ui -import { CycleIcon, DoubleCircleIcon, ModuleIcon } from "@plane/propel/icons"; +import { + CycleIcon, + StatePropertyIcon, + ModuleIcon, + MembersPropertyIcon, + PriorityPropertyIcon, + StartDatePropertyIcon, + DueDatePropertyIcon, + LabelPropertyIcon, + UserCirclePropertyIcon, + EstimatePropertyIcon, + ParentPropertyIcon, +} from "@plane/propel/icons"; import { cn, getDate, renderFormattedPayloadDate, shouldHighlightIssueDueDate } from "@plane/utils"; // components import { DateDropdown } from "@/components/dropdowns/date"; @@ -74,7 +85,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
- + {t("common.state")}
= observer((props) => {
- + {t("common.assignees")}
= observer((props) => {
- + {t("common.priority")}
= observer((props) => { {createdByDetails && (
- + {t("common.created_by")}
@@ -146,7 +157,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
- + {t("common.order_by.start_date")}
= observer((props) => {
- + {t("common.order_by.due_date")}
= observer((props) => { {projectId && areEstimateEnabledByProjectId(projectId) && (
- + {t("common.estimate")}
= observer((props) => {
- + {t("common.parent")}
= observer((props) => {
- + {t("common.labels")}
diff --git a/apps/web/core/components/issues/issue-layouts/layout-icon.tsx b/apps/web/core/components/issues/issue-layouts/layout-icon.tsx index e4f7f0004..c55669c68 100644 --- a/apps/web/core/components/issues/issue-layouts/layout-icon.tsx +++ b/apps/web/core/components/issues/issue-layouts/layout-icon.tsx @@ -1,19 +1,34 @@ -import type { LucideProps } from "lucide-react"; -import { List, Kanban, Calendar, Sheet, GanttChartSquare } from "lucide-react"; +import { + ListLayoutIcon, + BoardLayoutIcon, + CalendarLayoutIcon, + SheetLayoutIcon, + TimelineLayoutIcon, +} from "@plane/propel/icons"; +import type { ISvgIcons } from "@plane/propel/icons"; import { EIssueLayoutTypes } from "@plane/types"; -export const IssueLayoutIcon = ({ layout, ...props }: { layout: EIssueLayoutTypes } & LucideProps) => { +export const IssueLayoutIcon = ({ + layout, + size, + ...props +}: { layout: EIssueLayoutTypes; size?: number } & Omit) => { + const iconProps = { + ...props, + ...(size && { width: size, height: size }), + }; + switch (layout) { case EIssueLayoutTypes.LIST: - return ; + return ; case EIssueLayoutTypes.KANBAN: - return ; + return ; case EIssueLayoutTypes.CALENDAR: - return ; + return ; case EIssueLayoutTypes.SPREADSHEET: - return ; + return ; case EIssueLayoutTypes.GANTT: - return ; + return ; default: return null; } diff --git a/apps/web/core/components/issues/issue-layouts/properties/all-properties.tsx b/apps/web/core/components/issues/issue-layouts/properties/all-properties.tsx index 0a5f0a392..a61d6ccf4 100644 --- a/apps/web/core/components/issues/issue-layouts/properties/all-properties.tsx +++ b/apps/web/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -6,12 +6,12 @@ import { xor } from "lodash-es"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // icons -import { CalendarCheck2, CalendarClock, Link, Paperclip } from "lucide-react"; +import { Link, Paperclip } from "lucide-react"; // types import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants"; // i18n import { useTranslation } from "@plane/i18n"; -import { ViewsIcon } from "@plane/propel/icons"; +import { StartDatePropertyIcon, ViewsIcon, DueDatePropertyIcon } from "@plane/propel/icons"; import { Tooltip } from "@plane/propel/tooltip"; import type { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; // ui @@ -324,7 +324,7 @@ export const IssueProperties: React.FC = observer((props) => { onChange={handleStartDate} maxDate={maxDate} placeholder={t("common.order_by.start_date")} - icon={} + icon={} buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} optionsClassName="z-10" disabled={isReadOnly} @@ -346,7 +346,7 @@ export const IssueProperties: React.FC = observer((props) => { onChange={handleTargetDate} minDate={minDate} placeholder={t("common.order_by.due_date")} - icon={} + icon={} buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""} clearIconClassName="!text-custom-text-100" diff --git a/apps/web/core/components/issues/issue-layouts/properties/labels.tsx b/apps/web/core/components/issues/issue-layouts/properties/labels.tsx index eb02435e0..ecb856a0c 100644 --- a/apps/web/core/components/issues/issue-layouts/properties/labels.tsx +++ b/apps/web/core/components/issues/issue-layouts/properties/labels.tsx @@ -3,11 +3,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { Placement } from "@popperjs/core"; import { observer } from "mobx-react"; -import { Tags } from "lucide-react"; // plane helpers import { useOutsideClickDetector } from "@plane/hooks"; // i18n import { useTranslation } from "@plane/i18n"; +import { LabelPropertyIcon } from "@plane/propel/icons"; // types import { Tooltip } from "@plane/propel/tooltip"; import type { IIssueLabel } from "@plane/types"; @@ -101,7 +101,7 @@ export const IssuePropertyLabels: React.FC = observer((pro fullWidth && "w-full" )} > - + {placeholderText}
diff --git a/apps/web/core/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx b/apps/web/core/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx index 1d793c320..087e349b6 100644 --- a/apps/web/core/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx +++ b/apps/web/core/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx @@ -1,6 +1,6 @@ import React from "react"; import { observer } from "mobx-react"; -import { CalendarCheck2 } from "lucide-react"; +import { DueDatePropertyIcon } from "@plane/propel/icons"; // types import type { TIssue } from "@plane/types"; import { cn, getDate, renderFormattedPayloadDate, shouldHighlightIssueDueDate } from "@plane/utils"; @@ -42,7 +42,7 @@ export const SpreadsheetDueDateColumn: React.FC = observer((props: Props) }} disabled={disabled} placeholder="Due date" - icon={} + icon={} buttonVariant="transparent-with-text" buttonContainerClassName="w-full" buttonClassName={cn( diff --git a/apps/web/core/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx b/apps/web/core/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx index f1c35a4b5..bcdc01a1d 100644 --- a/apps/web/core/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx +++ b/apps/web/core/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx @@ -1,6 +1,6 @@ import React from "react"; import { observer } from "mobx-react"; -import { CalendarClock } from "lucide-react"; +import { StartDatePropertyIcon } from "@plane/propel/icons"; // types import type { TIssue } from "@plane/types"; // components @@ -36,7 +36,7 @@ export const SpreadsheetStartDateColumn: React.FC = observer((props: Prop }} disabled={disabled} placeholder="Start date" - icon={} + icon={} buttonVariant="transparent-with-text" buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10 px-page-x" buttonContainerClassName="w-full" diff --git a/apps/web/core/components/issues/issue-modal/components/default-properties.tsx b/apps/web/core/components/issues/issue-modal/components/default-properties.tsx index f7bba82d3..e9def41c6 100644 --- a/apps/web/core/components/issues/issue-modal/components/default-properties.tsx +++ b/apps/web/core/components/issues/issue-modal/components/default-properties.tsx @@ -4,10 +4,9 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; import type { Control } from "react-hook-form"; import { Controller } from "react-hook-form"; -import { LayoutPanelTop } from "lucide-react"; -// plane imports import { ETabIndices, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { ParentPropertyIcon } from "@plane/propel/icons"; // types import type { ISearchIssueResponse, TIssue } from "@plane/types"; // ui @@ -313,7 +312,7 @@ export const IssueDefaultProperties: React.FC = ob className="flex cursor-pointer items-center justify-between gap-1 h-full rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 text-xs hover:bg-custom-background-80" onClick={() => setParentIssueListModalOpen(true)} > - + {t("add_parent")} )} diff --git a/apps/web/core/components/issues/peek-overview/properties.tsx b/apps/web/core/components/issues/peek-overview/properties.tsx index bd6a727ea..34f5265dc 100644 --- a/apps/web/core/components/issues/peek-overview/properties.tsx +++ b/apps/web/core/components/issues/peek-overview/properties.tsx @@ -2,11 +2,22 @@ import type { FC } from "react"; import { observer } from "mobx-react"; -import { Signal, Tag, Triangle, LayoutPanelTop, CalendarClock, CalendarCheck2, Users, UserCircle2 } from "lucide-react"; // i18n import { useTranslation } from "@plane/i18n"; // ui icons -import { CycleIcon, DoubleCircleIcon, ModuleIcon } from "@plane/propel/icons"; +import { + CycleIcon, + StatePropertyIcon, + ModuleIcon, + MembersPropertyIcon, + PriorityPropertyIcon, + StartDatePropertyIcon, + DueDatePropertyIcon, + LabelPropertyIcon, + UserCirclePropertyIcon, + EstimatePropertyIcon, + ParentPropertyIcon, +} from "@plane/propel/icons"; import { cn, getDate, renderFormattedPayloadDate, shouldHighlightIssueDueDate } from "@plane/utils"; // components import { DateDropdown } from "@/components/dropdowns/date"; @@ -69,7 +80,7 @@ export const PeekOverviewProperties: FC = observer((pro {/* state */}
- + {t("common.state")}
= observer((pro {/* assignee */}
- + {t("common.assignees")}
= observer((pro {/* priority */}
- + {t("common.priority")}
= observer((pro {createdByDetails && (
- + {t("common.created_by")}
@@ -148,7 +159,7 @@ export const PeekOverviewProperties: FC = observer((pro {/* start date */}
- + {t("common.order_by.start_date")}
= observer((pro {/* due date */}
- + {t("common.order_by.due_date")}
= observer((pro {isEstimateEnabled && (
- + {t("common.estimate")}
= observer((pro {/* parent */}
- +

{t("common.parent")}

= observer((pro {/* label */}
- + {t("common.labels")}
diff --git a/apps/web/core/components/issues/select/base.tsx b/apps/web/core/components/issues/select/base.tsx index eb0bb02c7..a81eebcd9 100644 --- a/apps/web/core/components/issues/select/base.tsx +++ b/apps/web/core/components/issues/select/base.tsx @@ -2,12 +2,13 @@ import React, { useEffect, useRef, useState } from "react"; import type { Placement } from "@popperjs/core"; import { observer } from "mobx-react"; import { usePopper } from "react-popper"; -import { Check, Component, Loader, Search, Tag } from "lucide-react"; +import { Check, Component, Loader, Search } from "lucide-react"; import { Combobox } from "@headlessui/react"; import { getRandomLabelColor } from "@plane/constants"; // plane imports import { useOutsideClickDetector } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; +import { LabelPropertyIcon } from "@plane/propel/icons"; import type { IIssueLabel } from "@plane/types"; import { cn } from "@plane/utils"; // components @@ -181,7 +182,7 @@ export const WorkItemLabelSelectBase: React.FC = buttonClassName )} > - + {t("labels")}
)} diff --git a/apps/web/core/components/issues/workspace-draft/draft-issue-properties.tsx b/apps/web/core/components/issues/workspace-draft/draft-issue-properties.tsx index 04ca1b82d..e931bd25c 100644 --- a/apps/web/core/components/issues/workspace-draft/draft-issue-properties.tsx +++ b/apps/web/core/components/issues/workspace-draft/draft-issue-properties.tsx @@ -4,7 +4,7 @@ import { useCallback, useMemo } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // icons -import { CalendarCheck2, CalendarClock } from "lucide-react"; +import { DueDatePropertyIcon, StartDatePropertyIcon } from "@plane/propel/icons"; // types import type { TIssuePriorities, TWorkspaceDraftIssue } from "@plane/types"; import { getDate, renderFormattedPayloadDate, shouldHighlightIssueDueDate } from "@plane/utils"; @@ -177,7 +177,7 @@ export const DraftIssueProperties: React.FC = observer((props) onChange={handleStartDate} maxDate={maxDate} placeholder="Start date" - icon={} + icon={} buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} optionsClassName="z-10" renderByDefault={isMobile} @@ -192,7 +192,7 @@ export const DraftIssueProperties: React.FC = observer((props) onChange={handleTargetDate} minDate={minDate} placeholder="Due date" - icon={} + icon={} buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} buttonClassName={ shouldHighlightIssueDueDate(issue?.target_date || null, stateDetails?.group) ? "text-red-500" : "" diff --git a/apps/web/core/components/modules/analytics-sidebar/root.tsx b/apps/web/core/components/modules/analytics-sidebar/root.tsx index fa47aa146..5e33b6e43 100644 --- a/apps/web/core/components/modules/analytics-sidebar/root.tsx +++ b/apps/web/core/components/modules/analytics-sidebar/root.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { Controller, useForm } from "react-hook-form"; -import { CalendarClock, ChevronDown, ChevronRight, Info, Plus, SquareUser, Users } from "lucide-react"; +import { ChevronDown, ChevronRight, Info, Plus, SquareUser } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; import { MODULE_STATUS, @@ -16,7 +16,7 @@ import { } from "@plane/constants"; // plane types import { useTranslation } from "@plane/i18n"; -import { ModuleStatusIcon, WorkItemsIcon } from "@plane/propel/icons"; +import { MembersPropertyIcon, ModuleStatusIcon, WorkItemsIcon, StartDatePropertyIcon } from "@plane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { ILinkDetails, IModule, ModuleLink } from "@plane/types"; // plane ui @@ -303,7 +303,7 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => {
- + {t("date_range")}
@@ -371,7 +371,7 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => {
- + {t("members")}
= (props) => { // get Layout icon const icons = { - list: List, - board: LayoutGrid, - gantt: GanttChartSquare, + list: ListLayoutIcon, + board: GridLayoutIcon, + gantt: TimelineLayoutIcon, }; const Icon = icons[layoutType ?? "list"]; @@ -28,10 +28,10 @@ export const ModuleLayoutIcon: React.FC = (props) => { <> {withContainer ? (
- +
) : ( - + )} ); diff --git a/apps/web/core/components/modules/select/status.tsx b/apps/web/core/components/modules/select/status.tsx index e93f557b7..e89fe2f7b 100644 --- a/apps/web/core/components/modules/select/status.tsx +++ b/apps/web/core/components/modules/select/status.tsx @@ -7,7 +7,7 @@ import type { FieldError, Control } from "react-hook-form"; import { Controller } from "react-hook-form"; import { MODULE_STATUS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { DoubleCircleIcon, ModuleStatusIcon } from "@plane/propel/icons"; +import { StatePropertyIcon, ModuleStatusIcon } from "@plane/propel/icons"; import type { IModule } from "@plane/types"; // ui import { CustomSelect } from "@plane/ui"; @@ -37,7 +37,7 @@ export const ModuleStatusSelect: React.FC = ({ control, error, tabIndex } {value ? ( ) : ( - + )} {(selectedValue && t(selectedValue?.i18n_label)) ?? ( Status diff --git a/apps/web/core/components/modules/sidebar-select/select-status.tsx b/apps/web/core/components/modules/sidebar-select/select-status.tsx index 90d18409b..83e6cf6dd 100644 --- a/apps/web/core/components/modules/sidebar-select/select-status.tsx +++ b/apps/web/core/components/modules/sidebar-select/select-status.tsx @@ -7,7 +7,7 @@ import type { Control, UseFormWatch } from "react-hook-form"; import { Controller } from "react-hook-form"; import { MODULE_STATUS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { DoubleCircleIcon } from "@plane/propel/icons"; +import { StatePropertyIcon } from "@plane/propel/icons"; import type { IModule } from "@plane/types"; // ui import { CustomSelect } from "@plane/ui"; @@ -26,7 +26,7 @@ export const SidebarStatusSelect: React.FC = ({ control, submitChanges, w return (
- +

Status

diff --git a/apps/web/core/components/profile/overview/stats.tsx b/apps/web/core/components/profile/overview/stats.tsx index 5998b9a7a..13bb3c8d8 100644 --- a/apps/web/core/components/profile/overview/stats.tsx +++ b/apps/web/core/components/profile/overview/stats.tsx @@ -4,9 +4,8 @@ import Link from "next/link"; import { useParams } from "next/navigation"; // ui -import { UserCircle2 } from "lucide-react"; import { useTranslation } from "@plane/i18n"; -import { CreateIcon, LayerStackIcon } from "@plane/propel/icons"; +import { UserCirclePropertyIcon, CreateIcon, LayerStackIcon } from "@plane/propel/icons"; import type { IUserProfileData } from "@plane/types"; import { Loader, Card, ECardSpacing, ECardDirection } from "@plane/ui"; // types @@ -28,7 +27,7 @@ export const ProfileStats: React.FC = ({ userProfile }) => { value: userProfile?.created_issues ?? "...", }, { - icon: UserCircle2, + icon: UserCirclePropertyIcon, route: "assigned", i18n_title: "profile.stats.assigned", value: userProfile?.assigned_issues ?? "...", diff --git a/apps/web/core/components/readonly/estimate.tsx b/apps/web/core/components/readonly/estimate.tsx index ea71ce3d5..e67ee7408 100644 --- a/apps/web/core/components/readonly/estimate.tsx +++ b/apps/web/core/components/readonly/estimate.tsx @@ -2,9 +2,8 @@ import { useEffect } from "react"; import { observer } from "mobx-react"; -import { Triangle } from "lucide-react"; -// plane imports import { useTranslation } from "@plane/i18n"; +import { EstimatePropertyIcon } from "@plane/propel/icons"; import { EEstimateSystem } from "@plane/types"; import { cn, convertMinutesToHoursMinutesString } from "@plane/utils"; // hooks @@ -46,7 +45,7 @@ export const ReadonlyEstimate: React.FC = observer((prop return (
- {!hideIcon && } + {!hideIcon && } {displayValue ?? placeholder ?? t("common.none")}
); diff --git a/packages/constants/src/issue/common.ts b/packages/constants/src/issue/common.ts index 665fa930a..e19a752b2 100644 --- a/packages/constants/src/issue/common.ts +++ b/packages/constants/src/issue/common.ts @@ -241,7 +241,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "A", descendingOrderKey: "-assignees__first_name", descendingOrderTitle: "Z", - icon: "Users", + icon: "MembersPropertyIcon", }, created_on: { i18n_title: "common.sort.created_on", @@ -257,7 +257,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "New", descendingOrderKey: "target_date", descendingOrderTitle: "Old", - icon: "CalendarCheck2", + icon: "DueDatePropertyIcon", }, estimate: { i18n_title: "common.estimate", @@ -265,7 +265,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "Low", descendingOrderKey: "-estimate_point__key", descendingOrderTitle: "High", - icon: "Triangle", + icon: "EstimatePropertyIcon", }, labels: { i18n_title: "common.labels", @@ -273,7 +273,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "A", descendingOrderKey: "-labels__name", descendingOrderTitle: "Z", - icon: "Tag", + icon: "LabelPropertyIcon", }, modules: { i18n_title: "common.modules", @@ -297,7 +297,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "None", descendingOrderKey: "-priority", descendingOrderTitle: "Urgent", - icon: "Signal", + icon: "PriorityPropertyIcon", }, start_date: { i18n_title: "common.order_by.start_date", @@ -305,7 +305,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "New", descendingOrderKey: "start_date", descendingOrderTitle: "Old", - icon: "CalendarClock", + icon: "StartDatePropertyIcon", }, state: { i18n_title: "common.state", @@ -313,7 +313,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "A", descendingOrderKey: "-state__name", descendingOrderTitle: "Z", - icon: "DoubleCircleIcon", + icon: "StatePropertyIcon", }, updated_on: { i18n_title: "common.sort.updated_on", diff --git a/packages/propel/src/icons/add-icon.tsx b/packages/propel/src/icons/actions/add-icon.tsx similarity index 62% rename from packages/propel/src/icons/add-icon.tsx rename to packages/propel/src/icons/actions/add-icon.tsx index 13d87f3d3..c7e0a1c0f 100644 --- a/packages/propel/src/icons/add-icon.tsx +++ b/packages/propel/src/icons/actions/add-icon.tsx @@ -1,33 +1,17 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const AddIcon: React.FC = ({ - width = "16", - height = "16", - className = "text-current", - color = "currentColor", - ...rest -}) => ( - - +export const AddIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + - - - - - - - -); + + ); +}; diff --git a/packages/propel/src/icons/actions/index.ts b/packages/propel/src/icons/actions/index.ts new file mode 100644 index 000000000..0ef51f721 --- /dev/null +++ b/packages/propel/src/icons/actions/index.ts @@ -0,0 +1 @@ +export * from "./add-icon"; diff --git a/packages/propel/src/icons/constants.tsx b/packages/propel/src/icons/constants.tsx new file mode 100644 index 000000000..4dc86d08d --- /dev/null +++ b/packages/propel/src/icons/constants.tsx @@ -0,0 +1,62 @@ +import { Icon } from "./icon"; +export const ActionsIconsMap = [{ icon: , title: "AddIcon" }]; + +export const WorkspaceIconsMap = [ + { icon: , title: "AnalyticsIcon" }, + { icon: , title: "ArchiveIcon" }, + { icon: , title: "DashboardIcon" }, + { icon: , title: "DraftIcon" }, + { icon: , title: "HomeIcon" }, + { icon: , title: "InboxIcon" }, + { icon: , title: "ProjectIcon" }, + { icon: , title: "YourWorkIcon" }, +]; + +export const ProjectIconsMap = [ + { icon: , title: "CycleIcon" }, + { icon: , title: "EpicIcon" }, + { icon: , title: "IntakeIcon" }, + { icon: , title: "ModuleIcon" }, + { icon: , title: "PageIcon" }, + { icon: , title: "ViewIcon" }, + { icon: , title: "WorkItemsIcon" }, +]; + +export const SubBrandIconsMap = [ + { icon: , title: "PiChatLogo" }, + { icon: , title: "PlaneIcon" }, + { icon: , title: "WikiIcon" }, +]; + +export const LayoutIconsMap = [ + { icon: , title: "CalendarLayoutIcon" }, + { icon: , title: "CardLayoutIcon" }, + { icon: , title: "TimelineLayoutIcon" }, + { icon: , title: "GridLayoutIcon" }, + { icon: , title: "BoardLayoutIcon" }, + { icon: , title: "ListLayoutIcon" }, + { icon: , title: "SheetLayoutIcon" }, +]; + +export const PropertyIconsMap = [ + { icon: , title: "BooleanPropertyIcon" }, + { icon: , title: "DropdownPropertyIcon" }, + { icon: , title: "DueDatePropertyIcon" }, + { icon: , title: "DuplicatePropertyIcon" }, + { icon: , title: "EstimatePropertyIcon" }, + { icon: , title: "HashPropertyIcon" }, + { icon: , title: "LabelPropertyIcon" }, + { icon: , title: "MembersPropertyIcon" }, + { icon: , title: "OverdueDatePropertyIcon" }, + { icon: , title: "ParentPropertyIcon" }, + { icon: , title: "PriorityPropertyIcon" }, + { icon: , title: "RelatesToPropertyIcon" }, + { icon: , title: "RelationPropertyIcon" }, + { icon: , title: "ScopePropertyIcon" }, + { icon: , title: "StartDatePropertyIcon" }, + { icon: , title: "StatePropertyIcon" }, + { icon: , title: "UserCirclePropertyIcon" }, + { icon: , title: "UserPropertyIcon" }, + { icon: , title: "UserSquarePropertyIcon" }, + { icon: , title: "WorkflowsPropertyIcon" }, +]; diff --git a/packages/propel/src/icons/cycle/cycle-group-icon.tsx b/packages/propel/src/icons/cycle/cycle-group-icon.tsx index 6062ac36e..6e095d6f0 100644 --- a/packages/propel/src/icons/cycle/cycle-group-icon.tsx +++ b/packages/propel/src/icons/cycle/cycle-group-icon.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { CircleDotDashed, Circle } from "lucide-react"; -import { CycleIcon } from "../cycle-icon"; +import { CycleIcon } from "../project/cycle-icon"; import { CircleDotFullIcon } from "./circle-dot-full-icon"; import { CYCLE_GROUP_COLORS, ICycleGroupIcon } from "./helper"; diff --git a/packages/propel/src/icons/default-icon.tsx b/packages/propel/src/icons/default-icon.tsx new file mode 100644 index 000000000..cf1fbed94 --- /dev/null +++ b/packages/propel/src/icons/default-icon.tsx @@ -0,0 +1,10 @@ +import * as React from "react"; + +import { IconWrapper } from "./icon-wrapper"; +import { ISvgIcons } from "./type"; + +export const DefaultIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/epic-icon.tsx b/packages/propel/src/icons/epic-icon.tsx deleted file mode 100644 index c78a98261..000000000 --- a/packages/propel/src/icons/epic-icon.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from "react"; - -export type Props = { - className?: string; - width?: string | number; - height?: string | number; - color?: string; -}; - -export const EpicIcon: React.FC = ({ width = "16", height = "16", className }) => ( - - - - -); diff --git a/packages/propel/src/icons/helpers.ts b/packages/propel/src/icons/helpers.ts new file mode 100644 index 000000000..0cf0bf64b --- /dev/null +++ b/packages/propel/src/icons/helpers.ts @@ -0,0 +1,29 @@ +import { ICON_REGISTRY, IconName } from "./registry"; + +/** + * Get the icon component by name + * @param name - The icon name from the registry + * @returns The icon component or default icon if not found + */ +export const getIconComponent = (name: IconName) => ICON_REGISTRY[name] || ICON_REGISTRY.default; + +/** + * Check if the icon name exists in the registry + * @param name - The icon name to check + * @returns True if the icon exists in the registry + */ +export const isValidIconName = (name: string): name is IconName => name in ICON_REGISTRY; + +/** + * Get all available icon names + * @returns Array of all icon names in the registry + */ +export const getIconNames = (): IconName[] => Object.keys(ICON_REGISTRY) as IconName[]; + +/** + * Get icons by category + * @param category - The category prefix (e.g., 'workspace', 'project') + * @returns Array of icon names matching the category + */ +export const getIconsByCategory = (category: string): IconName[] => + getIconNames().filter((name) => name.startsWith(`${category}.`)); diff --git a/packages/propel/src/icons/icon-wrapper.tsx b/packages/propel/src/icons/icon-wrapper.tsx new file mode 100644 index 000000000..fb6123353 --- /dev/null +++ b/packages/propel/src/icons/icon-wrapper.tsx @@ -0,0 +1,42 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +interface IIconWrapper extends ISvgIcons { + children: React.ReactNode; + clipPathId?: string; + viewBox?: string; +} + +export const IconWrapper: React.FC = ({ + width = "16", + height = "16", + className = "text-current", + children, + clipPathId, + viewBox = "0 0 16 16", + ...rest +}) => ( + + {clipPathId ? ( + <> + {children} + + + + + + + ) : ( + children + )} + +); diff --git a/packages/propel/src/icons/icon.tsx b/packages/propel/src/icons/icon.tsx new file mode 100644 index 000000000..68bc0e349 --- /dev/null +++ b/packages/propel/src/icons/icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { ICON_REGISTRY, IconName } from "./registry"; +import { ISvgIcons } from "./type"; + +export interface IconProps extends Omit { + name: IconName; +} + +export const Icon: React.FC = ({ name, ...props }) => { + const IconComponent = ICON_REGISTRY[name] || ICON_REGISTRY.default; + return ; +}; diff --git a/packages/propel/src/icons/icons.stories.tsx b/packages/propel/src/icons/icons.stories.tsx new file mode 100644 index 000000000..4bab5581c --- /dev/null +++ b/packages/propel/src/icons/icons.stories.tsx @@ -0,0 +1,196 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { + ActionsIconsMap, + LayoutIconsMap, + ProjectIconsMap, + PropertyIconsMap, + SubBrandIconsMap, + WorkspaceIconsMap, +} from "./constants"; +import { Icon } from "./icon"; +import { CycleIcon } from "./project/cycle-icon"; +import { HomeIcon } from "./workspace/home-icon"; +import { ProjectIcon } from "./workspace/project-icon"; + +const meta: Meta = { + title: "Icons", + parameters: { + layout: "padded", + docs: { + description: { + component: + "A comprehensive collection of all icons used throughout the application. Supports both direct imports and registry-based usage.", + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const AllIcons: Story = { + render: () => ( +
+
+

Sub-Brand Icons

+
+ {SubBrandIconsMap.map((item) => ( +
+
{item.icon}
+

{item.title}

+
+ ))} +
+
+ +
+

Workspace Icons

+
+ {WorkspaceIconsMap.map((item) => ( +
+
{item.icon}
+

{item.title}

+
+ ))} +
+
+ +
+

Project Icons

+
+ {ProjectIconsMap.map((item) => ( +
+
{item.icon}
+

{item.title}

+
+ ))} +
+
+ +
+

Layout Icons

+
+ {LayoutIconsMap.map((item) => ( +
+
{item.icon}
+

{item.title}

+
+ ))} +
+
+ +
+

Property Icons

+
+ {PropertyIconsMap.map((item) => ( +
+
{item.icon}
+

{item.title}

+
+ ))} +
+
+ +
+

Actions Icons

+
+ {ActionsIconsMap.map((item) => ( +
+
{item.icon}
+

{item.title}

+
+ ))} +
+
+
+ ), +}; + +export const RegistryUsage: Story = { + render: () => ( +
+
+

Registry-Based Usage

+

+ Use the Icon component with{" "} + name prop for dynamic icon selection. +

+
+
+ +

workspace.home

+
+
+ +

project.cycle

+
+
+ +

layout.board

+
+
+ +

property.priority

+
+
+
+ +
+

Direct Import Usage

+

+ Import icon components directly for better tree-shaking and type safety. +

+
+
+ +

HomeIcon

+
+
+ +

CycleIcon

+
+
+ +

ProjectIcon

+
+
+
+
+ ), +}; + +export const IconSizes: Story = { + render: () => ( +
+
+

Icon Sizes

+

+ Icons can be rendered in different sizes using width and height props. +

+
+ +
+
+ +

12x12

+
+
+ +

16x16 (default)

+
+
+ +

24x24

+
+
+ +

32x32

+
+
+ +

48x48

+
+
+
+ ), +}; diff --git a/packages/propel/src/icons/index.ts b/packages/propel/src/icons/index.ts index 0c0529a22..ace2a4225 100644 --- a/packages/propel/src/icons/index.ts +++ b/packages/propel/src/icons/index.ts @@ -1,9 +1,10 @@ export type { ISvgIcons } from "./type"; +export type { IconName } from "./registry"; +export type { IconProps } from "./icon"; +export { ICON_REGISTRY } from "./registry"; +export * from "./actions"; export * from "./activity-icon"; -export * from "./add-icon"; export * from "./ai-icon"; -export * from "./analytics-icon"; -export * from "./archive-icon"; export * from "./at-risk-icon"; export * from "./bar-icon"; export * from "./blocked-icon"; @@ -15,58 +16,51 @@ export * from "./center-panel-icon"; export * from "./comment-fill-icon"; export * from "./create-icon"; export * from "./cycle"; -export * from "./cycle-icon"; -export * from "./dashboard-icon"; +export * from "./default-icon"; export * from "./dice-icon"; export * from "./discord-icon"; export * from "./display-properties"; export * from "./done-icon"; -export * from "./draft-icon"; export * from "./dropdown-icon"; -export * from "./epic-icon"; export * from "./favorite-folder-icon"; export * from "./full-screen-panel-icon"; export * from "./github-icon"; export * from "./gitlab-icon"; -export * from "./home-icon"; -export * from "./inbox-icon"; +export * from "./helpers"; +export * from "./icon"; +export * from "./icon-wrapper"; export * from "./info-fill-icon"; export * from "./info-icon"; export * from "./in-progress-icon"; export * from "./intake"; -export * from "./intake-icon"; export * from "./layer-stack"; export * from "./layers-icon"; +export * from "./layouts"; export * from "./lead-icon"; export * from "./module"; -export * from "./module-icon"; export * from "./monospace-icon"; export * from "./multiple-sticky"; export * from "./off-track-icon"; export * from "./on-track-icon"; export * from "./overview-icon"; -export * from "./page-icon"; export * from "./pending-icon"; export * from "./photo-filter-icon"; -export * from "./pi-chat"; export * from "./planned-icon"; -export * from "./plane-icon"; export * from "./priority-icon"; -export * from "./project-icon"; +export * from "./project"; +export * from "./properties"; export * from "./related-icon"; export * from "./sans-serif-icon"; export * from "./serif-icon"; export * from "./side-panel-icon"; export * from "./state"; export * from "./sticky-note-icon"; +export * from "./sub-brand"; export * from "./suspended-user"; export * from "./teams"; export * from "./transfer-icon"; export * from "./tree-map-icon"; export * from "./updates-icon"; export * from "./user-activity-icon"; -export * from "./view-icon"; -export * from "./wiki-icon"; -export * from "./work-items-icon"; +export * from "./workspace"; export * from "./workspace-icon"; -export * from "./your-work-icon"; diff --git a/packages/propel/src/icons/layouts/board-icon.tsx b/packages/propel/src/icons/layouts/board-icon.tsx new file mode 100644 index 000000000..aac682c63 --- /dev/null +++ b/packages/propel/src/icons/layouts/board-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const BoardLayoutIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/layouts/calendar-icon.tsx b/packages/propel/src/icons/layouts/calendar-icon.tsx new file mode 100644 index 000000000..803d4a60a --- /dev/null +++ b/packages/propel/src/icons/layouts/calendar-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const CalendarLayoutIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/layouts/card-icon.tsx b/packages/propel/src/icons/layouts/card-icon.tsx new file mode 100644 index 000000000..64be0629c --- /dev/null +++ b/packages/propel/src/icons/layouts/card-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const CardLayoutIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/layouts/grid-icon.tsx b/packages/propel/src/icons/layouts/grid-icon.tsx new file mode 100644 index 000000000..91cb6b717 --- /dev/null +++ b/packages/propel/src/icons/layouts/grid-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const GridLayoutIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/layouts/index.ts b/packages/propel/src/icons/layouts/index.ts new file mode 100644 index 000000000..649d93d3b --- /dev/null +++ b/packages/propel/src/icons/layouts/index.ts @@ -0,0 +1,7 @@ +export * from "./calendar-icon"; +export * from "./card-icon"; +export * from "./timeline-icon"; +export * from "./grid-icon"; +export * from "./board-icon"; +export * from "./list-icon"; +export * from "./sheet-icon"; diff --git a/packages/propel/src/icons/layouts/list-icon.tsx b/packages/propel/src/icons/layouts/list-icon.tsx new file mode 100644 index 000000000..7d993c218 --- /dev/null +++ b/packages/propel/src/icons/layouts/list-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const ListLayoutIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/layouts/sheet-icon.tsx b/packages/propel/src/icons/layouts/sheet-icon.tsx new file mode 100644 index 000000000..eea1b9fe1 --- /dev/null +++ b/packages/propel/src/icons/layouts/sheet-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const SheetLayoutIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/layouts/timeline-icon.tsx b/packages/propel/src/icons/layouts/timeline-icon.tsx new file mode 100644 index 000000000..36950de1c --- /dev/null +++ b/packages/propel/src/icons/layouts/timeline-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const TimelineLayoutIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/cycle-icon.tsx b/packages/propel/src/icons/project/cycle-icon.tsx similarity index 52% rename from packages/propel/src/icons/cycle-icon.tsx rename to packages/propel/src/icons/project/cycle-icon.tsx index 181a7dd74..7eee10ca4 100644 --- a/packages/propel/src/icons/cycle-icon.tsx +++ b/packages/propel/src/icons/project/cycle-icon.tsx @@ -1,24 +1,13 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const CycleIcon: React.FC = ({ - width = "16", - height = "16", - className = "text-current", - color = "currentColor", - ...rest -}) => ( - - +export const CycleIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + = ({ d="M7.99951 12.3337C10.3928 12.3337 12.3328 10.3936 12.3328 8.00033C12.3328 5.60709 10.3928 3.66699 7.99951 3.66699V12.3337Z" fill={color} /> - - - - - - - -); + + ); +}; diff --git a/packages/propel/src/icons/project/epic-icon.tsx b/packages/propel/src/icons/project/epic-icon.tsx new file mode 100644 index 000000000..35bebaafc --- /dev/null +++ b/packages/propel/src/icons/project/epic-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const EpicIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/project/index.ts b/packages/propel/src/icons/project/index.ts new file mode 100644 index 000000000..0ace8ff74 --- /dev/null +++ b/packages/propel/src/icons/project/index.ts @@ -0,0 +1,7 @@ +export * from "./cycle-icon"; +export * from "./epic-icon"; +export * from "./intake-icon"; +export * from "./module-icon"; +export * from "./page-icon"; +export * from "./view-icon"; +export * from "./work-items-icon"; diff --git a/packages/propel/src/icons/intake-icon.tsx b/packages/propel/src/icons/project/intake-icon.tsx similarity index 72% rename from packages/propel/src/icons/intake-icon.tsx rename to packages/propel/src/icons/project/intake-icon.tsx index 38daabf48..6bee44333 100644 --- a/packages/propel/src/icons/intake-icon.tsx +++ b/packages/propel/src/icons/project/intake-icon.tsx @@ -1,33 +1,17 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const IntakeIcon: React.FC = ({ - width = "16", - height = "16", - className = "text-current", - color = "currentColor", - ...rest -}) => ( - - +export const IntakeIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + - - - - - - - -); + + ); +}; diff --git a/packages/propel/src/icons/module-icon.tsx b/packages/propel/src/icons/project/module-icon.tsx similarity index 91% rename from packages/propel/src/icons/module-icon.tsx rename to packages/propel/src/icons/project/module-icon.tsx index 188b570fa..6c4341a8d 100644 --- a/packages/propel/src/icons/module-icon.tsx +++ b/packages/propel/src/icons/project/module-icon.tsx @@ -1,23 +1,10 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const ModuleIcon: React.FC = ({ - width = "16", - height = "16", - className = "text-current", - color = "currentColor", - ...rest -}) => ( - +export const ModuleIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + = ({ d="M6.54286 9H5.45714C5.29713 9 5.21712 9 5.156 9.03114C5.10224 9.05854 5.05853 9.10223 5.03114 9.156C5 9.21711 5 9.29712 5 9.45714V10.5429C5 10.7029 5 10.7829 5.03114 10.844C5.05853 10.8978 5.10224 10.9415 5.156 10.9689C5.21712 11 5.29713 11 5.45714 11H6.54286C6.70287 11 6.78288 11 6.844 10.9689C6.89776 10.9415 6.94147 10.8978 6.96886 10.844C7 10.7829 7 10.7029 7 10.5429V9.45714C7 9.29712 7 9.21711 6.96886 9.156C6.94147 9.10223 6.89776 9.05854 6.844 9.03114C6.78288 9 6.70287 9 6.54286 9Z" fill={color} /> - + ); diff --git a/packages/propel/src/icons/page-icon.tsx b/packages/propel/src/icons/project/page-icon.tsx similarity index 89% rename from packages/propel/src/icons/page-icon.tsx rename to packages/propel/src/icons/project/page-icon.tsx index c9eeb486e..a98a740d8 100644 --- a/packages/propel/src/icons/page-icon.tsx +++ b/packages/propel/src/icons/project/page-icon.tsx @@ -1,26 +1,13 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const PageIcon: React.FC = ({ - width = "16", - height = "16", - className = "text-current", - color = "currentColor", - ...rest -}) => ( - +export const PageIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + - + ); diff --git a/packages/propel/src/icons/view-icon.tsx b/packages/propel/src/icons/project/view-icon.tsx similarity index 74% rename from packages/propel/src/icons/view-icon.tsx rename to packages/propel/src/icons/project/view-icon.tsx index e51a7044f..1b6f6a38b 100644 --- a/packages/propel/src/icons/view-icon.tsx +++ b/packages/propel/src/icons/project/view-icon.tsx @@ -1,33 +1,17 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const ViewsIcon: React.FC = ({ - width = "16", - height = "16", - className = "text-current", - color = "currentColor", - ...rest -}) => ( - - +export const ViewsIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + - - - - - - - -); + + ); +}; diff --git a/packages/propel/src/icons/project/work-items-icon.tsx b/packages/propel/src/icons/project/work-items-icon.tsx new file mode 100644 index 000000000..d48c8e58c --- /dev/null +++ b/packages/propel/src/icons/project/work-items-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const WorkItemsIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/properties/boolean-icon.tsx b/packages/propel/src/icons/properties/boolean-icon.tsx new file mode 100644 index 000000000..5c9588c90 --- /dev/null +++ b/packages/propel/src/icons/properties/boolean-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const BooleanPropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/properties/dropdown-icon.tsx b/packages/propel/src/icons/properties/dropdown-icon.tsx new file mode 100644 index 000000000..b0215524d --- /dev/null +++ b/packages/propel/src/icons/properties/dropdown-icon.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const DropdownPropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + + + + ); +}; diff --git a/packages/propel/src/icons/properties/due-date-icon.tsx b/packages/propel/src/icons/properties/due-date-icon.tsx new file mode 100644 index 000000000..d8f73c04a --- /dev/null +++ b/packages/propel/src/icons/properties/due-date-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const DueDatePropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/properties/duplicate-icon.tsx b/packages/propel/src/icons/properties/duplicate-icon.tsx new file mode 100644 index 000000000..8384fc8c1 --- /dev/null +++ b/packages/propel/src/icons/properties/duplicate-icon.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const DuplicatePropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + + + + ); +}; diff --git a/packages/propel/src/icons/properties/estimate-icon.tsx b/packages/propel/src/icons/properties/estimate-icon.tsx new file mode 100644 index 000000000..ad59eb02b --- /dev/null +++ b/packages/propel/src/icons/properties/estimate-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const EstimatePropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/properties/hash-icon.tsx b/packages/propel/src/icons/properties/hash-icon.tsx new file mode 100644 index 000000000..c218a7971 --- /dev/null +++ b/packages/propel/src/icons/properties/hash-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const HashPropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/properties/index.ts b/packages/propel/src/icons/properties/index.ts new file mode 100644 index 000000000..52e36a7f4 --- /dev/null +++ b/packages/propel/src/icons/properties/index.ts @@ -0,0 +1,20 @@ +export * from "./boolean-icon"; +export * from "./dropdown-icon"; +export * from "./due-date-icon"; +export * from "./duplicate-icon"; +export * from "./estimate-icon"; +export * from "./hash-icon"; +export * from "./label-icon"; +export * from "./members-icon"; +export * from "./overdue-date-icon"; +export * from "./parent-icon"; +export * from "./priority-icon"; +export * from "./relates-to-icon"; +export * from "./relation-icon"; +export * from "./scope-icon"; +export * from "./start-date-icon"; +export * from "./state-icon"; +export * from "./user-circle-icon"; +export * from "./user-icon"; +export * from "./user-square-icon"; +export * from "./workflows-icon"; diff --git a/packages/propel/src/icons/properties/label-icon.tsx b/packages/propel/src/icons/properties/label-icon.tsx new file mode 100644 index 000000000..97bd7f5b7 --- /dev/null +++ b/packages/propel/src/icons/properties/label-icon.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const LabelPropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + + + + + ); +}; diff --git a/packages/propel/src/icons/properties/members-icon.tsx b/packages/propel/src/icons/properties/members-icon.tsx new file mode 100644 index 000000000..3627ec3a1 --- /dev/null +++ b/packages/propel/src/icons/properties/members-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const MembersPropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/properties/overdue-date-icon.tsx b/packages/propel/src/icons/properties/overdue-date-icon.tsx new file mode 100644 index 000000000..1c4152c63 --- /dev/null +++ b/packages/propel/src/icons/properties/overdue-date-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const OverdueDatePropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/properties/parent-icon.tsx b/packages/propel/src/icons/properties/parent-icon.tsx new file mode 100644 index 000000000..1104f2481 --- /dev/null +++ b/packages/propel/src/icons/properties/parent-icon.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const ParentPropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + + + +); diff --git a/packages/propel/src/icons/properties/priority-icon.tsx b/packages/propel/src/icons/properties/priority-icon.tsx new file mode 100644 index 000000000..904235557 --- /dev/null +++ b/packages/propel/src/icons/properties/priority-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const PriorityPropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/properties/relates-to-icon.tsx b/packages/propel/src/icons/properties/relates-to-icon.tsx new file mode 100644 index 000000000..625999e6d --- /dev/null +++ b/packages/propel/src/icons/properties/relates-to-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const RelatesToPropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/properties/relation-icon.tsx b/packages/propel/src/icons/properties/relation-icon.tsx new file mode 100644 index 000000000..82326b2d0 --- /dev/null +++ b/packages/propel/src/icons/properties/relation-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const RelationPropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/properties/scope-icon.tsx b/packages/propel/src/icons/properties/scope-icon.tsx new file mode 100644 index 000000000..0f66ee10a --- /dev/null +++ b/packages/propel/src/icons/properties/scope-icon.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const ScopePropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + + + + ); +}; diff --git a/packages/propel/src/icons/properties/start-date-icon.tsx b/packages/propel/src/icons/properties/start-date-icon.tsx new file mode 100644 index 000000000..28741aecf --- /dev/null +++ b/packages/propel/src/icons/properties/start-date-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const StartDatePropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/properties/state-icon.tsx b/packages/propel/src/icons/properties/state-icon.tsx new file mode 100644 index 000000000..fc69eace6 --- /dev/null +++ b/packages/propel/src/icons/properties/state-icon.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const StatePropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + + + + ); +}; diff --git a/packages/propel/src/icons/properties/user-circle-icon.tsx b/packages/propel/src/icons/properties/user-circle-icon.tsx new file mode 100644 index 000000000..06fd6f3eb --- /dev/null +++ b/packages/propel/src/icons/properties/user-circle-icon.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const UserCirclePropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + + + + ); +}; diff --git a/packages/propel/src/icons/properties/user-icon.tsx b/packages/propel/src/icons/properties/user-icon.tsx new file mode 100644 index 000000000..d1dd3ca83 --- /dev/null +++ b/packages/propel/src/icons/properties/user-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const UserPropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/properties/user-square-icon.tsx b/packages/propel/src/icons/properties/user-square-icon.tsx new file mode 100644 index 000000000..3e7521f28 --- /dev/null +++ b/packages/propel/src/icons/properties/user-square-icon.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const UserSquarePropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + + + + ); +}; diff --git a/packages/propel/src/icons/properties/workflows-icon.tsx b/packages/propel/src/icons/properties/workflows-icon.tsx new file mode 100644 index 000000000..ed3a2b6bd --- /dev/null +++ b/packages/propel/src/icons/properties/workflows-icon.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const WorkflowsPropertyIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + + + +); diff --git a/packages/propel/src/icons/registry.ts b/packages/propel/src/icons/registry.ts new file mode 100644 index 000000000..5bff85850 --- /dev/null +++ b/packages/propel/src/icons/registry.ts @@ -0,0 +1,115 @@ +import { AddIcon } from "./actions/add-icon"; +import { DefaultIcon } from "./default-icon"; +import { BoardLayoutIcon } from "./layouts/board-icon"; +import { CalendarLayoutIcon } from "./layouts/calendar-icon"; +import { CardLayoutIcon } from "./layouts/card-icon"; +import { GridLayoutIcon } from "./layouts/grid-icon"; +import { ListLayoutIcon } from "./layouts/list-icon"; +import { SheetLayoutIcon } from "./layouts/sheet-icon"; +import { TimelineLayoutIcon } from "./layouts/timeline-icon"; +import { CycleIcon } from "./project/cycle-icon"; +import { EpicIcon } from "./project/epic-icon"; +import { IntakeIcon } from "./project/intake-icon"; +import { ModuleIcon } from "./project/module-icon"; +import { PageIcon } from "./project/page-icon"; +import { ViewsIcon } from "./project/view-icon"; +import { WorkItemsIcon } from "./project/work-items-icon"; +import { BooleanPropertyIcon } from "./properties/boolean-icon"; +import { DropdownPropertyIcon } from "./properties/dropdown-icon"; +import { DueDatePropertyIcon } from "./properties/due-date-icon"; +import { DuplicatePropertyIcon } from "./properties/duplicate-icon"; +import { EstimatePropertyIcon } from "./properties/estimate-icon"; +import { HashPropertyIcon } from "./properties/hash-icon"; +import { LabelPropertyIcon } from "./properties/label-icon"; +import { MembersPropertyIcon } from "./properties/members-icon"; +import { OverdueDatePropertyIcon } from "./properties/overdue-date-icon"; +import { ParentPropertyIcon } from "./properties/parent-icon"; +import { PriorityPropertyIcon } from "./properties/priority-icon"; +import { RelatesToPropertyIcon } from "./properties/relates-to-icon"; +import { RelationPropertyIcon } from "./properties/relation-icon"; +import { ScopePropertyIcon } from "./properties/scope-icon"; +import { StartDatePropertyIcon } from "./properties/start-date-icon"; +import { StatePropertyIcon } from "./properties/state-icon"; +import { UserCirclePropertyIcon } from "./properties/user-circle-icon"; +import { UserPropertyIcon } from "./properties/user-icon"; +import { UserSquarePropertyIcon } from "./properties/user-square-icon"; +import { WorkflowsPropertyIcon } from "./properties/workflows-icon"; +import { PiChatLogo } from "./sub-brand/pi-chat"; +import { PlaneNewIcon } from "./sub-brand/plane-icon"; +import { WikiIcon } from "./sub-brand/wiki-icon"; +import { AnalyticsIcon } from "./workspace/analytics-icon"; +import { ArchiveIcon } from "./workspace/archive-icon"; +import { DashboardIcon } from "./workspace/dashboard-icon"; +import { DraftIcon } from "./workspace/draft-icon"; +import { HomeIcon } from "./workspace/home-icon"; +import { InboxIcon } from "./workspace/inbox-icon"; +import { ProjectIcon } from "./workspace/project-icon"; +import { YourWorkIcon } from "./workspace/your-work-icon"; + +export const ICON_REGISTRY = { + // Sub-brand icons + "sub-brand.plane": PlaneNewIcon, + "sub-brand.wiki": WikiIcon, + "sub-brand.pi-chat": PiChatLogo, + + // Workspace icons + "workspace.analytics": AnalyticsIcon, + "workspace.archive": ArchiveIcon, + "workspace.cycle": CycleIcon, + "workspace.dashboard": DashboardIcon, + "workspace.draft": DraftIcon, + "workspace.home": HomeIcon, + "workspace.inbox": InboxIcon, + "workspace.page": PageIcon, + "workspace.project": ProjectIcon, + "workspace.views": ViewsIcon, + "workspace.your-work": YourWorkIcon, + + // Project icons + "project.cycle": CycleIcon, + "project.epic": EpicIcon, + "project.intake": IntakeIcon, + "project.module": ModuleIcon, + "project.page": PageIcon, + "project.view": ViewsIcon, + "project.work-items": WorkItemsIcon, + + // Layout icons + "layout.calendar": CalendarLayoutIcon, + "layout.card": CardLayoutIcon, + "layout.timeline": TimelineLayoutIcon, + "layout.grid": GridLayoutIcon, + "layout.board": BoardLayoutIcon, + "layout.list": ListLayoutIcon, + "layout.sheet": SheetLayoutIcon, + + // Property icons + "property.boolean": BooleanPropertyIcon, + "property.dropdown": DropdownPropertyIcon, + "property.due-date": DueDatePropertyIcon, + "property.duplicate": DuplicatePropertyIcon, + "property.estimate": EstimatePropertyIcon, + "property.hash": HashPropertyIcon, + "property.label": LabelPropertyIcon, + "property.members": MembersPropertyIcon, + "property.overdue-date": OverdueDatePropertyIcon, + "property.parent": ParentPropertyIcon, + "property.priority": PriorityPropertyIcon, + "property.relates-to": RelatesToPropertyIcon, + "property.relation": RelationPropertyIcon, + "property.scope": ScopePropertyIcon, + "property.start-date": StartDatePropertyIcon, + "property.state": StatePropertyIcon, + "property.user-circle": UserCirclePropertyIcon, + "property.user": UserPropertyIcon, + "property.user-square": UserSquarePropertyIcon, + "property.workflows": WorkflowsPropertyIcon, + + // Action icons + "action.add": AddIcon, + + // Default fallback + default: DefaultIcon, +} as const; + +export type IconName = keyof typeof ICON_REGISTRY; diff --git a/packages/propel/src/icons/sub-brand/index.ts b/packages/propel/src/icons/sub-brand/index.ts new file mode 100644 index 000000000..7a0d0cefb --- /dev/null +++ b/packages/propel/src/icons/sub-brand/index.ts @@ -0,0 +1,3 @@ +export * from "./pi-chat"; +export * from "./plane-icon"; +export * from "./wiki-icon"; diff --git a/packages/propel/src/icons/pi-chat.tsx b/packages/propel/src/icons/sub-brand/pi-chat.tsx similarity index 82% rename from packages/propel/src/icons/pi-chat.tsx rename to packages/propel/src/icons/sub-brand/pi-chat.tsx index e2e49a28f..1f9477066 100644 --- a/packages/propel/src/icons/pi-chat.tsx +++ b/packages/propel/src/icons/sub-brand/pi-chat.tsx @@ -1,21 +1,15 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const PiChatLogo: React.FC = ({ width = "16", height = "16", className, color = "currentColor" }) => ( - +export const PiChatLogo: React.FC = ({ color = "currentColor", ...rest }) => ( + - + ); diff --git a/packages/propel/src/icons/plane-icon.tsx b/packages/propel/src/icons/sub-brand/plane-icon.tsx similarity index 54% rename from packages/propel/src/icons/plane-icon.tsx rename to packages/propel/src/icons/sub-brand/plane-icon.tsx index 113d2dfd0..e645dedf7 100644 --- a/packages/propel/src/icons/plane-icon.tsx +++ b/packages/propel/src/icons/sub-brand/plane-icon.tsx @@ -1,22 +1,13 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const PlaneNewIcon: React.FC = ({ - width = "16", - height = "16", - className, - color = "currentColor", -}) => ( - - +export const PlaneNewIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + = ({ d="M14.66 0H6.49582C5.75553 0 5.1543 0.600183 5.1543 1.34152V5.15488H9.50615C10.2464 5.15488 10.8477 5.75506 10.8477 6.49641V10.8483H14.661C15.4013 10.8483 16.0026 10.2481 16.0026 9.50673V1.34152C16.0026 0.601229 15.4024 0 14.661 0H14.66Z" fill={color} /> - - - - - - - -); + + ); +}; diff --git a/packages/propel/src/icons/wiki-icon.tsx b/packages/propel/src/icons/sub-brand/wiki-icon.tsx similarity index 54% rename from packages/propel/src/icons/wiki-icon.tsx rename to packages/propel/src/icons/sub-brand/wiki-icon.tsx index 5a3b992c7..8d534dc0d 100644 --- a/packages/propel/src/icons/wiki-icon.tsx +++ b/packages/propel/src/icons/sub-brand/wiki-icon.tsx @@ -1,26 +1,17 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const WikiIcon: React.FC = ({ width = "16", height = "16", className, color = "currentColor" }) => ( - - +export const WikiIcon: React.FC = ({ color = "currentColor", ...rest }) => { + const clipPathId = React.useId(); + + return ( + - - - - - - - -); + + ); +}; diff --git a/packages/propel/src/icons/work-items-icon.tsx b/packages/propel/src/icons/work-items-icon.tsx deleted file mode 100644 index f07145b33..000000000 --- a/packages/propel/src/icons/work-items-icon.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from "react"; - -import { ISvgIcons } from "./type"; - -export const WorkItemsIcon: React.FC = ({ - width = "16", - height = "16", - className = "text-current", - color = "currentColor", - ...rest -}) => ( - - - -); diff --git a/packages/propel/src/icons/analytics-icon.tsx b/packages/propel/src/icons/workspace/analytics-icon.tsx similarity index 87% rename from packages/propel/src/icons/analytics-icon.tsx rename to packages/propel/src/icons/workspace/analytics-icon.tsx index 4e1f65023..d0be0ebb3 100644 --- a/packages/propel/src/icons/analytics-icon.tsx +++ b/packages/propel/src/icons/workspace/analytics-icon.tsx @@ -1,26 +1,13 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const AnalyticsIcon: React.FC = ({ - width = "16", - height = "16", - className = "text-current", - color = "currentColor", - ...rest -}) => ( - +export const AnalyticsIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + - + ); diff --git a/packages/propel/src/icons/archive-icon.tsx b/packages/propel/src/icons/workspace/archive-icon.tsx similarity index 89% rename from packages/propel/src/icons/archive-icon.tsx rename to packages/propel/src/icons/workspace/archive-icon.tsx index b83ce69a8..1d359d360 100644 --- a/packages/propel/src/icons/archive-icon.tsx +++ b/packages/propel/src/icons/workspace/archive-icon.tsx @@ -1,12 +1,13 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const ArchiveIcon: React.FC = ({ className = "text-current", color = "currentColor", ...rest }) => ( - +export const ArchiveIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + - + ); diff --git a/packages/propel/src/icons/dashboard-icon.tsx b/packages/propel/src/icons/workspace/dashboard-icon.tsx similarity index 86% rename from packages/propel/src/icons/dashboard-icon.tsx rename to packages/propel/src/icons/workspace/dashboard-icon.tsx index a451d7984..fab6d145b 100644 --- a/packages/propel/src/icons/dashboard-icon.tsx +++ b/packages/propel/src/icons/workspace/dashboard-icon.tsx @@ -1,26 +1,13 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const DashboardIcon: React.FC = ({ - width = "16", - height = "16", - className = "text-current", - color = "currentColor", - ...rest -}) => ( - +export const DashboardIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + - + ); diff --git a/packages/propel/src/icons/draft-icon.tsx b/packages/propel/src/icons/workspace/draft-icon.tsx similarity index 82% rename from packages/propel/src/icons/draft-icon.tsx rename to packages/propel/src/icons/workspace/draft-icon.tsx index d821fa358..bec049e6b 100644 --- a/packages/propel/src/icons/draft-icon.tsx +++ b/packages/propel/src/icons/workspace/draft-icon.tsx @@ -1,26 +1,13 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const DraftIcon: React.FC = ({ - width = "16", - height = "16", - className = "text-current", - color = "currentColor", - ...rest -}) => ( - +export const DraftIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + - + ); diff --git a/packages/propel/src/icons/home-icon.tsx b/packages/propel/src/icons/workspace/home-icon.tsx similarity index 91% rename from packages/propel/src/icons/home-icon.tsx rename to packages/propel/src/icons/workspace/home-icon.tsx index dfcd1a182..cfc3c06f5 100644 --- a/packages/propel/src/icons/home-icon.tsx +++ b/packages/propel/src/icons/workspace/home-icon.tsx @@ -1,19 +1,13 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const HomeIcon: React.FC = ({ width = "16", height = "16", className, color = "currentColor" }) => ( - +export const HomeIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + - + ); diff --git a/packages/propel/src/icons/inbox-icon.tsx b/packages/propel/src/icons/workspace/inbox-icon.tsx similarity index 88% rename from packages/propel/src/icons/inbox-icon.tsx rename to packages/propel/src/icons/workspace/inbox-icon.tsx index e6a1c20eb..c4a6151b4 100644 --- a/packages/propel/src/icons/inbox-icon.tsx +++ b/packages/propel/src/icons/workspace/inbox-icon.tsx @@ -1,26 +1,13 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const InboxIcon: React.FC = ({ - width = "16", - height = "16", - className, - color = "currentColor", - ...rest -}) => ( - +export const InboxIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + - + ); diff --git a/packages/propel/src/icons/workspace/index.ts b/packages/propel/src/icons/workspace/index.ts new file mode 100644 index 000000000..dd1b8a059 --- /dev/null +++ b/packages/propel/src/icons/workspace/index.ts @@ -0,0 +1,8 @@ +export * from "./analytics-icon"; +export * from "./archive-icon"; +export * from "./dashboard-icon"; +export * from "./draft-icon"; +export * from "./home-icon"; +export * from "./inbox-icon"; +export * from "./project-icon"; +export * from "./your-work-icon"; diff --git a/packages/propel/src/icons/project-icon.tsx b/packages/propel/src/icons/workspace/project-icon.tsx similarity index 91% rename from packages/propel/src/icons/project-icon.tsx rename to packages/propel/src/icons/workspace/project-icon.tsx index 8a9ad6280..357f03339 100644 --- a/packages/propel/src/icons/project-icon.tsx +++ b/packages/propel/src/icons/workspace/project-icon.tsx @@ -1,26 +1,13 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const ProjectIcon: React.FC = ({ - width = "16", - height = "16", - className = "text-current", - color = "currentColor", - ...rest -}) => ( - +export const ProjectIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + - + ); diff --git a/packages/propel/src/icons/your-work-icon.tsx b/packages/propel/src/icons/workspace/your-work-icon.tsx similarity index 81% rename from packages/propel/src/icons/your-work-icon.tsx rename to packages/propel/src/icons/workspace/your-work-icon.tsx index 2f003a239..75a2ea79e 100644 --- a/packages/propel/src/icons/your-work-icon.tsx +++ b/packages/propel/src/icons/workspace/your-work-icon.tsx @@ -1,23 +1,10 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; -export const YourWorkIcon: React.FC = ({ - width = "16", - height = "16", - className = "text-current", - color = "currentColor", - ...rest -}) => ( - +export const YourWorkIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + = ({ d="M11.6338 9.27539C11.9026 9.27559 12.1415 9.44806 12.2265 9.70312L13.0332 12.124L13.1406 11.8027L13.1797 11.7109C13.2861 11.5072 13.4981 11.375 13.7334 11.375H14.667C15.0119 11.3752 15.2917 11.6551 15.292 12C15.292 12.3451 15.012 12.6248 14.667 12.625H14.1836L13.6259 14.2979C13.5409 14.5531 13.3022 14.7256 13.0332 14.7256C12.7642 14.7255 12.5255 14.553 12.4404 14.2979L11.6328 11.877L11.5263 12.1982C11.4412 12.4533 11.2024 12.6249 10.9336 12.625H9.99997C9.65479 12.625 9.37497 12.3452 9.37497 12C9.37518 11.655 9.65492 11.375 9.99997 11.375H10.4834L11.04 9.70312C11.1251 9.44791 11.3647 9.27539 11.6338 9.27539Z" fill={color} /> - + ); From c4dd4bd02f9424fc2735e8bd741a1d8393052ec1 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:17:19 +0530 Subject: [PATCH 015/212] [WEB-5155]refactor: simplify user filtering logic in SearchEndpoint #8012 --- apps/api/plane/app/views/search/base.py | 30 +++++++------------------ 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/apps/api/plane/app/views/search/base.py b/apps/api/plane/app/views/search/base.py index a598d1ee1..3942b0a44 100644 --- a/apps/api/plane/app/views/search/base.py +++ b/apps/api/plane/app/views/search/base.py @@ -294,29 +294,15 @@ class SearchEndpoint(BaseAPIView): .order_by("-created_at") ) - if issue_id: - issue_created_by = ( - Issue.objects.filter(id=issue_id).values_list("created_by_id", flat=True).first() - ) - users = ( - users.filter(Q(role__gt=10) | Q(member_id=issue_created_by)) - .distinct() - .values( - "member__avatar_url", - "member__display_name", - "member__id", - ) - ) - else: - users = ( - users.filter(Q(role__gt=10)) - .distinct() - .values( - "member__avatar_url", - "member__display_name", - "member__id", - ) + users = ( + users + .distinct() + .values( + "member__avatar_url", + "member__display_name", + "member__id", ) + ) response_data["user_mention"] = list(users[:count]) From 1d4cde9ba0cc02db1768bbe3508b1c3281f41e4c Mon Sep 17 00:00:00 2001 From: Surya Prashanth Date: Mon, 27 Oct 2025 16:19:52 +0530 Subject: [PATCH 016/212] [WEB-5245] feat: auto-populate logo_props in project creation #8013 --- apps/api/plane/api/serializers/project.py | 54 ++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/apps/api/plane/api/serializers/project.py b/apps/api/plane/api/serializers/project.py index 3228c5ad9..c7a134348 100644 --- a/apps/api/plane/api/serializers/project.py +++ b/apps/api/plane/api/serializers/project.py @@ -1,4 +1,5 @@ # Third party imports +import random from rest_framework import serializers # Module imports @@ -24,6 +25,47 @@ class ProjectCreateSerializer(BaseSerializer): and workspace association for new project initialization. """ + PROJECT_ICON_DEFAULT_COLORS = [ + "#95999f", + "#6d7b8a", + "#5e6ad2", + "#02b5ed", + "#02b55c", + "#f2be02", + "#e57a00", + "#f38e82", + ] + PROJECT_ICON_DEFAULT_ICONS = [ + "home", + "apps", + "settings", + "star", + "favorite", + "done", + "check_circle", + "add_task", + "create_new_folder", + "dataset", + "terminal", + "key", + "rocket", + "public", + "quiz", + "mood", + "gavel", + "eco", + "diamond", + "forest", + "bolt", + "sync", + "cached", + "library_add", + "view_timeline", + "view_kanban", + "empty_dashboard", + "cycle", + ] + class Meta: model = Project fields = [ @@ -44,7 +86,6 @@ class ProjectCreateSerializer(BaseSerializer): "archive_in", "close_in", "timezone", - "logo_props", "external_source", "external_id", "is_issue_type_enabled", @@ -57,6 +98,7 @@ class ProjectCreateSerializer(BaseSerializer): "updated_at", "created_by", "updated_by", + "logo_props", ] def validate(self, data): @@ -86,6 +128,16 @@ class ProjectCreateSerializer(BaseSerializer): if ProjectIdentifier.objects.filter(name=identifier, workspace_id=self.context["workspace_id"]).exists(): raise serializers.ValidationError(detail="Project Identifier is taken") + if validated_data.get("logo_props", None) is None: + # Generate a random icon and color for the project icon + validated_data["logo_props"] = { + "in_use": "icon", + "icon": { + "name": random.choice(self.PROJECT_ICON_DEFAULT_ICONS), + "color": random.choice(self.PROJECT_ICON_DEFAULT_COLORS), + }, + } + project = Project.objects.create(**validated_data, workspace_id=self.context["workspace_id"]) return project From c07f7b7c1e9ff31d13a10081f6940b44ec0a2e78 Mon Sep 17 00:00:00 2001 From: Surya Prashanth Date: Mon, 27 Oct 2025 16:54:03 +0530 Subject: [PATCH 017/212] [WEB-5245] fix: add is_time_tracking_enabled field in project serializer (#8016) --- apps/api/plane/api/serializers/project.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/plane/api/serializers/project.py b/apps/api/plane/api/serializers/project.py index c7a134348..5b3070361 100644 --- a/apps/api/plane/api/serializers/project.py +++ b/apps/api/plane/api/serializers/project.py @@ -89,6 +89,7 @@ class ProjectCreateSerializer(BaseSerializer): "external_source", "external_id", "is_issue_type_enabled", + "is_time_tracking_enabled", ] read_only_fields = [ From be0d1871f0fdd920ecaa8379b37dbfbb9e858421 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:02:22 +0530 Subject: [PATCH 018/212] [WEB-5241] fix: project features toggle #8015 --- apps/web/core/components/project/project-feature-update.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/core/components/project/project-feature-update.tsx b/apps/web/core/components/project/project-feature-update.tsx index 602f2e530..e411e33ba 100644 --- a/apps/web/core/components/project/project-feature-update.tsx +++ b/apps/web/core/components/project/project-feature-update.tsx @@ -10,9 +10,10 @@ import { Button, getButtonStyling } from "@plane/propel/button"; import { Row } from "@plane/ui"; // components import { Logo } from "@/components/common/logo"; -import { ProjectFeaturesList } from "@/components/project/settings/features-list"; // hooks import { useProject } from "@/hooks/store/use-project"; +// plane web imports +import { ProjectFeaturesList } from "@/plane-web/components/projects/settings/features-list"; type Props = { workspaceSlug: string; From 3faf7681126108d5caa933880256fdf95ebae714 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:03:35 +0530 Subject: [PATCH 019/212] [WEB-3567] chore: empty state refactoring and translation fix (#8014) * chore: empty state component improvement and code refactor * chore: translation code refactor * chore: empty state code refactor --- .../settings/(workspace)/webhooks/page.tsx | 34 +++++------ .../cycles/archived-cycles/root.tsx | 12 ++-- .../core/components/exporter/prev-exports.tsx | 17 +++--- .../core/components/inbox/sidebar/root.tsx | 1 + .../empty-states/archived-issues.tsx | 56 +++++++++---------- .../modules/archived-modules/root.tsx | 12 ++-- packages/i18n/src/constants/language.ts | 2 +- packages/i18n/src/locales/index.ts | 38 ++++++------- .../src/empty-state/assets/asset-registry.tsx | 2 + .../src/empty-state/assets/asset-types.ts | 1 + .../assets/vertical-stack/constant.tsx | 5 ++ .../src/empty-state/compact-empty-state.tsx | 28 ++++++---- .../src/empty-state/detailed-empty-state.tsx | 28 ++++++---- packages/propel/src/empty-state/types.ts | 1 + 14 files changed, 121 insertions(+), 116 deletions(-) diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx index 06be0fd85..efbe979ba 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx @@ -8,9 +8,9 @@ import useSWR from "swr"; import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // components +import { EmptyStateCompact } from "@plane/propel/empty-state"; import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; import { PageHead } from "@/components/core/page-title"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; import { SettingsHeading } from "@/components/settings/heading"; import { WebhookSettingsLoader } from "@/components/ui/loader/settings/web-hook"; @@ -20,7 +20,6 @@ import { captureClick } from "@/helpers/event-tracker.helper"; import { useWebhook } from "@/hooks/store/use-webhook"; import { useWorkspace } from "@/hooks/store/use-workspace"; import { useUserPermissions } from "@/hooks/store/user"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; const WebhooksListPage = observer(() => { // states @@ -35,7 +34,6 @@ const WebhooksListPage = observer(() => { const { currentWorkspace } = useWorkspace(); // derived values const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/webhooks" }); useSWR( workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null, @@ -90,21 +88,23 @@ const WebhooksListPage = observer(() => { ) : (
- { - captureClick({ - elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_WEBHOOK_BUTTON, - }); - setShowCreateWebhookModal(true); + { + captureClick({ + elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_WEBHOOK_BUTTON, + }); + setShowCreateWebhookModal(true); + }, }, - }} + ]} + align="start" + rootClassName="py-20" />
diff --git a/apps/web/core/components/cycles/archived-cycles/root.tsx b/apps/web/core/components/cycles/archived-cycles/root.tsx index 93c18a048..fdf527de8 100644 --- a/apps/web/core/components/cycles/archived-cycles/root.tsx +++ b/apps/web/core/components/cycles/archived-cycles/root.tsx @@ -4,15 +4,14 @@ import { useParams } from "next/navigation"; import useSWR from "swr"; // plane imports import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import type { TCycleFilters } from "@plane/types"; import { calculateTotalFilters } from "@plane/utils"; // components -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader"; // hooks import { useCycle } from "@/hooks/store/use-cycle"; import { useCycleFilter } from "@/hooks/store/use-cycle-filter"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // local imports import { CycleAppliedFiltersList } from "../applied-filters"; import { ArchivedCyclesView } from "./view"; @@ -28,7 +27,6 @@ export const ArchivedCycleLayoutRoot: React.FC = observer(() => { const { clearAllFilters, currentProjectArchivedFilters, updateFilters } = useCycleFilter(); // derived values const totalArchivedCycles = currentProjectArchivedCycleIds?.length ?? 0; - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/archived/empty-cycles" }); useSWR( workspaceSlug && projectId ? `ARCHIVED_CYCLES_${workspaceSlug.toString()}_${projectId.toString()}` : null, @@ -69,10 +67,10 @@ export const ArchivedCycleLayoutRoot: React.FC = observer(() => { )} {totalArchivedCycles === 0 ? (
-
) : ( diff --git a/apps/web/core/components/exporter/prev-exports.tsx b/apps/web/core/components/exporter/prev-exports.tsx index 5ee589e7d..8498a6ca2 100644 --- a/apps/web/core/components/exporter/prev-exports.tsx +++ b/apps/web/core/components/exporter/prev-exports.tsx @@ -4,15 +4,13 @@ import useSWR, { mutate } from "swr"; import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; // plane imports import { useTranslation } from "@plane/i18n"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; import type { IExportData } from "@plane/types"; import { Table } from "@plane/ui"; // components -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { ImportExportSettingsLoader } from "@/components/ui/loader/settings/import-and-export"; // constants import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys"; -// hooks -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // services import { IntegrationService } from "@/services/integrations"; // local imports @@ -35,7 +33,6 @@ export const PrevExports = observer((props: Props) => { // hooks const { t } = useTranslation(); const columns = useExportColumns(); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/exports" }); const { data: exporterServices } = useSWR( workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null, @@ -125,12 +122,12 @@ export const PrevExports = observer((props: Props) => {
) : (
-
) diff --git a/apps/web/core/components/inbox/sidebar/root.tsx b/apps/web/core/components/inbox/sidebar/root.tsx index 88b59dc62..ba43a7a93 100644 --- a/apps/web/core/components/inbox/sidebar/root.tsx +++ b/apps/web/core/components/inbox/sidebar/root.tsx @@ -162,6 +162,7 @@ export const InboxSidebar: FC = observer((props) => { title="No request closed yet" description="All the work items whether accepted or declined can be found here." assetClassName="size-20" + className="px-10" /> )}
diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx index a54aff362..5b0c5728a 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -3,15 +3,12 @@ import { useParams } from "next/navigation"; // plane imports import { EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { EIssuesStoreType, EUserProjectRoles } from "@plane/types"; -// components -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; // hooks -import { useIssues } from "@/hooks/store/use-issues"; import { useUserPermissions } from "@/hooks/store/user"; import { useWorkItemFilterInstance } from "@/hooks/store/work-item-filters/use-work-item-filter-instance"; import { useAppRouter } from "@/hooks/use-app-router"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const ProjectArchivedEmptyState: React.FC = observer(() => { // router @@ -22,48 +19,45 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { // plane hooks const { t } = useTranslation(); // store hooks - const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); const { allowPermissions } = useUserPermissions(); // derived values const archivedWorkItemFilter = projectId ? useWorkItemFilterInstance(EIssuesStoreType.ARCHIVED, projectId) : undefined; - const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const additionalPath = archivedWorkItemFilter?.hasActiveFilters ? (activeLayout ?? "list") : undefined; const canPerformEmptyStateActions = allowPermissions( [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], EUserPermissionsLevel.PROJECT ); - const emptyFilterResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/empty-filters/", - additionalPath: additionalPath, - }); - const archivedIssuesResolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/archived/empty-issues", - }); return (
{archivedWorkItemFilter?.hasActiveFilters ? ( - ) : ( - router.push(`/${workspaceSlug}/settings/projects/${projectId}/automations`), - disabled: !canPerformEmptyStateActions, - }} + router.push(`/${workspaceSlug}/settings/projects/${projectId}/automations`), + disabled: !canPerformEmptyStateActions, + variant: "primary", + }, + ]} /> )}
diff --git a/apps/web/core/components/modules/archived-modules/root.tsx b/apps/web/core/components/modules/archived-modules/root.tsx index 1628192a9..31fb196ca 100644 --- a/apps/web/core/components/modules/archived-modules/root.tsx +++ b/apps/web/core/components/modules/archived-modules/root.tsx @@ -4,17 +4,16 @@ import { useParams } from "next/navigation"; import useSWR from "swr"; // plane imports import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import type { TModuleFilters } from "@plane/types"; // components import { calculateTotalFilters } from "@plane/utils"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { ArchivedModulesView, ModuleAppliedFiltersList } from "@/components/modules"; import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader"; // helpers // hooks import { useModule } from "@/hooks/store/use-module"; import { useModuleFilter } from "@/hooks/store/use-module-filter"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const ArchivedModuleLayoutRoot: React.FC = observer(() => { // router @@ -26,7 +25,6 @@ export const ArchivedModuleLayoutRoot: React.FC = observer(() => { const { clearAllFilters, currentProjectArchivedFilters, updateFilters } = useModuleFilter(); // derived values const totalArchivedModules = projectArchivedModuleIds?.length ?? 0; - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/archived/empty-modules" }); useSWR( workspaceSlug && projectId ? `ARCHIVED_MODULES_${workspaceSlug.toString()}_${projectId.toString()}` : null, @@ -72,10 +70,10 @@ export const ArchivedModuleLayoutRoot: React.FC = observer(() => { )} {totalArchivedModules === 0 ? (
-
) : ( diff --git a/packages/i18n/src/constants/language.ts b/packages/i18n/src/constants/language.ts index efcc9aada..8e54c189d 100644 --- a/packages/i18n/src/constants/language.ts +++ b/packages/i18n/src/constants/language.ts @@ -32,7 +32,7 @@ export enum ETranslationFiles { TRANSLATIONS = "translations", ACCESSIBILITY = "accessibility", EDITOR = "editor", - EMPTY_STATE = "emptyState", + EMPTY_STATE = "empty-state", } export const LANGUAGE_STORAGE_KEY = "userLanguage"; diff --git a/packages/i18n/src/locales/index.ts b/packages/i18n/src/locales/index.ts index 50188889e..6bd62cf41 100644 --- a/packages/i18n/src/locales/index.ts +++ b/packages/i18n/src/locales/index.ts @@ -12,114 +12,114 @@ export const locales = { translations: () => import("./en/translations"), accessibility: () => import("./en/accessibility"), editor: () => import("./en/editor"), - emptyState: () => import("./en/empty-state"), + "empty-state": () => import("./en/empty-state"), }, fr: { translations: () => import("./fr/translations"), accessibility: () => import("./fr/accessibility"), editor: () => import("./fr/editor"), - emptyState: () => import("./fr/empty-state"), + "empty-state": () => import("./fr/empty-state"), }, es: { translations: () => import("./es/translations"), accessibility: () => import("./es/accessibility"), editor: () => import("./es/editor"), - emptyState: () => import("./es/empty-state"), + "empty-state": () => import("./es/empty-state"), }, ja: { translations: () => import("./ja/translations"), accessibility: () => import("./ja/accessibility"), editor: () => import("./ja/editor"), - emptyState: () => import("./ja/empty-state"), + "empty-state": () => import("./ja/empty-state"), }, "zh-CN": { translations: () => import("./zh-CN/translations"), accessibility: () => import("./zh-CN/accessibility"), editor: () => import("./zh-CN/editor"), - emptyState: () => import("./zh-CN/empty-state"), + "empty-state": () => import("./zh-CN/empty-state"), }, "zh-TW": { translations: () => import("./zh-TW/translations"), accessibility: () => import("./zh-TW/accessibility"), editor: () => import("./zh-TW/editor"), - emptyState: () => import("./zh-TW/empty-state"), + "empty-state": () => import("./zh-TW/empty-state"), }, ru: { translations: () => import("./ru/translations"), accessibility: () => import("./ru/accessibility"), editor: () => import("./ru/editor"), - emptyState: () => import("./ru/empty-state"), + "empty-state": () => import("./ru/empty-state"), }, it: { translations: () => import("./it/translations"), accessibility: () => import("./it/accessibility"), editor: () => import("./it/editor"), - emptyState: () => import("./it/empty-state"), + "empty-state": () => import("./it/empty-state"), }, cs: { translations: () => import("./cs/translations"), accessibility: () => import("./cs/accessibility"), editor: () => import("./cs/editor"), - emptyState: () => import("./cs/empty-state"), + "empty-state": () => import("./cs/empty-state"), }, sk: { translations: () => import("./sk/translations"), accessibility: () => import("./sk/accessibility"), editor: () => import("./sk/editor"), - emptyState: () => import("./sk/empty-state"), + "empty-state": () => import("./sk/empty-state"), }, de: { translations: () => import("./de/translations"), accessibility: () => import("./de/accessibility"), editor: () => import("./de/editor"), - emptyState: () => import("./de/empty-state"), + "empty-state": () => import("./de/empty-state"), }, ua: { translations: () => import("./ua/translations"), accessibility: () => import("./ua/accessibility"), editor: () => import("./ua/editor"), - emptyState: () => import("./ua/empty-state"), + "empty-state": () => import("./ua/empty-state"), }, pl: { translations: () => import("./pl/translations"), accessibility: () => import("./pl/accessibility"), editor: () => import("./pl/editor"), - emptyState: () => import("./pl/empty-state"), + "empty-state": () => import("./pl/empty-state"), }, ko: { translations: () => import("./ko/translations"), accessibility: () => import("./ko/accessibility"), editor: () => import("./ko/editor"), - emptyState: () => import("./ko/empty-state"), + "empty-state": () => import("./ko/empty-state"), }, "pt-BR": { translations: () => import("./pt-BR/translations"), accessibility: () => import("./pt-BR/accessibility"), editor: () => import("./pt-BR/editor"), - emptyState: () => import("./pt-BR/empty-state"), + "empty-state": () => import("./pt-BR/empty-state"), }, id: { translations: () => import("./id/translations"), accessibility: () => import("./id/accessibility"), editor: () => import("./id/editor"), - emptyState: () => import("./id/empty-state"), + "empty-state": () => import("./id/empty-state"), }, ro: { translations: () => import("./ro/translations"), accessibility: () => import("./ro/accessibility"), editor: () => import("./ro/editor"), - emptyState: () => import("./ro/empty-state"), + "empty-state": () => import("./ro/empty-state"), }, "vi-VN": { translations: () => import("./vi-VN/translations"), accessibility: () => import("./vi-VN/accessibility"), editor: () => import("./vi-VN/editor"), - emptyState: () => import("./vi-VN/empty-state"), + "empty-state": () => import("./vi-VN/empty-state"), }, "tr-TR": { translations: () => import("./tr-TR/translations"), accessibility: () => import("./tr-TR/accessibility"), editor: () => import("./tr-TR/editor"), - emptyState: () => import("./tr-TR/empty-state"), + "empty-state": () => import("./tr-TR/empty-state"), }, }; diff --git a/packages/propel/src/empty-state/assets/asset-registry.tsx b/packages/propel/src/empty-state/assets/asset-registry.tsx index 42ac821e6..5ad23bb09 100644 --- a/packages/propel/src/empty-state/assets/asset-registry.tsx +++ b/packages/propel/src/empty-state/assets/asset-registry.tsx @@ -39,6 +39,7 @@ import { DraftVerticalStackIllustration, EpicVerticalStackIllustration, Error404VerticalStackIllustration, + InitiativeVerticalStackIllustration, InvalidLinkVerticalStackIllustration, ModuleVerticalStackIllustration, NoAccessVerticalStackIllustration, @@ -85,6 +86,7 @@ export const VERTICAL_STACK_ASSETS: Record, title: "Error404VerticalStackIllustration", }, + { + asset: , + title: "InitiativeVerticalStackIllustration", + }, { asset: , title: "InvalidLinkVerticalStackIllustration", diff --git a/packages/propel/src/empty-state/compact-empty-state.tsx b/packages/propel/src/empty-state/compact-empty-state.tsx index 33f135d97..5a4523d1f 100644 --- a/packages/propel/src/empty-state/compact-empty-state.tsx +++ b/packages/propel/src/empty-state/compact-empty-state.tsx @@ -15,6 +15,7 @@ export const EmptyStateCompact: React.FC = ({ rootClassName, assetClassName, align = "center", + customButton, }) => { // Determine which asset to use: assetKey takes precedence, fallback to custom asset const resolvedAsset = assetKey ? getCompactAsset(assetKey as CompactAssetType, assetClassName) : asset; @@ -39,18 +40,21 @@ export const EmptyStateCompact: React.FC = ({ title &&

{title}

)} - {actions && actions.length > 0 && ( -
- {actions.map((action, index) => { - const { label, variant, ...rest } = action; - return ( - - ); - })} -
- )} + {customButton + ? customButton + : actions && + actions.length > 0 && ( +
+ {actions.map((action, index) => { + const { label, variant, ...rest } = action; + return ( + + ); + })} +
+ )}
diff --git a/packages/propel/src/empty-state/detailed-empty-state.tsx b/packages/propel/src/empty-state/detailed-empty-state.tsx index b6fd4af0c..9680aea71 100644 --- a/packages/propel/src/empty-state/detailed-empty-state.tsx +++ b/packages/propel/src/empty-state/detailed-empty-state.tsx @@ -14,6 +14,7 @@ export const EmptyStateDetailed: React.FC = ({ className, rootClassName, assetClassName, + customButton, }) => { // Determine which asset to use: assetKey takes precedence, fallback to custom asset const resolvedAsset = assetKey ? getDetailedAsset(assetKey as DetailedAssetType, assetClassName) : asset; @@ -31,18 +32,21 @@ export const EmptyStateDetailed: React.FC = ({
)} - {actions && actions.length > 0 && ( -
- {actions.map((action, index) => { - const { label, variant, ...rest } = action; - return ( - - ); - })} -
- )} + {customButton + ? customButton + : actions && + actions.length > 0 && ( +
+ {actions.map((action, index) => { + const { label, variant, ...rest } = action; + return ( + + ); + })} +
+ )}
diff --git a/packages/propel/src/empty-state/types.ts b/packages/propel/src/empty-state/types.ts index 8c2886799..ae161bdb0 100644 --- a/packages/propel/src/empty-state/types.ts +++ b/packages/propel/src/empty-state/types.ts @@ -21,4 +21,5 @@ export interface BaseEmptyStateCommonProps { assetKey?: CompactAssetType | DetailedAssetType; asset?: React.ReactNode; align?: TAlign; + customButton?: React.ReactNode; } From cf7f891bcb3c5df41a33e39efd860238db7bb9eb Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:55:46 +0530 Subject: [PATCH 020/212] [WEB-5248] chore: empty state code refactor and translation fix (#8017) * chore: empty state code refactor and translation fix * chore: code refactor --- .../(projects)/analytics/[tabId]/page.tsx | 33 ++++++++----------- .../core/components/inbox/sidebar/root.tsx | 4 +-- .../empty-states/archived-issues.tsx | 4 +-- .../issue-layouts/empty-states/cycle.tsx | 6 ++-- .../issue-layouts/empty-states/module.tsx | 6 ++-- .../empty-states/project-issues.tsx | 4 +-- .../components/modules/modules-list-view.tsx | 4 +-- .../pages/pages-list-main-content.tsx | 4 +-- .../web/core/components/project/card-list.tsx | 4 +-- apps/web/core/components/views/views-list.tsx | 4 +-- packages/i18n/src/locales/cs/empty-state.ts | 2 +- packages/i18n/src/locales/de/empty-state.ts | 2 +- packages/i18n/src/locales/en/empty-state.ts | 2 +- packages/i18n/src/locales/es/empty-state.ts | 2 +- packages/i18n/src/locales/fr/empty-state.ts | 2 +- packages/i18n/src/locales/id/empty-state.ts | 2 +- packages/i18n/src/locales/it/empty-state.ts | 2 +- packages/i18n/src/locales/ja/empty-state.ts | 2 +- packages/i18n/src/locales/ko/empty-state.ts | 2 +- packages/i18n/src/locales/pl/empty-state.ts | 2 +- .../i18n/src/locales/pt-BR/empty-state.ts | 2 +- packages/i18n/src/locales/ro/empty-state.ts | 2 +- packages/i18n/src/locales/ru/empty-state.ts | 2 +- packages/i18n/src/locales/sk/empty-state.ts | 2 +- .../i18n/src/locales/tr-TR/empty-state.ts | 2 +- packages/i18n/src/locales/ua/empty-state.ts | 2 +- .../i18n/src/locales/vi-VN/empty-state.ts | 2 +- .../i18n/src/locales/zh-CN/empty-state.ts | 2 +- .../i18n/src/locales/zh-TW/empty-state.ts | 2 +- 29 files changed, 52 insertions(+), 59 deletions(-) diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx index 5c3f94e32..cf092aab5 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx @@ -6,20 +6,18 @@ import { useRouter } from "next/navigation"; // plane package imports import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { Tabs } from "@plane/ui"; import type { TabItem } from "@plane/ui"; // components import AnalyticsFilterActions from "@/components/analytics/analytics-filter-actions"; import { PageHead } from "@/components/core/page-title"; -import { ComicBoxButton } from "@/components/empty-state/comic-box-button"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; // hooks import { captureClick } from "@/helpers/event-tracker.helper"; import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useProject } from "@/hooks/store/use-project"; import { useWorkspace } from "@/hooks/store/use-workspace"; import { useUserPermissions } from "@/hooks/store/user"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { getAnalyticsTabs } from "@/plane-web/components/analytics/tabs"; type Props = { @@ -46,9 +44,6 @@ const AnalyticsPage = observer((props: Props) => { const { currentWorkspace } = useWorkspace(); const { allowPermissions } = useUserPermissions(); - // helper hooks - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" }); - // permissions const canPerformEmptyStateActions = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], @@ -96,22 +91,20 @@ const AnalyticsPage = observer((props: Props) => { />
) : ( - { + { toggleCreateProjectModal(true); captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON }); - }} - disabled={!canPerformEmptyStateActions} - /> - } + }, + disabled: !canPerformEmptyStateActions, + }, + ]} /> )} diff --git a/apps/web/core/components/inbox/sidebar/root.tsx b/apps/web/core/components/inbox/sidebar/root.tsx index ba43a7a93..ef050ae41 100644 --- a/apps/web/core/components/inbox/sidebar/root.tsx +++ b/apps/web/core/components/inbox/sidebar/root.tsx @@ -137,8 +137,8 @@ export const InboxSidebar: FC = observer((props) => { {getAppliedFiltersCount > 0 ? ( ) : currentTab === EInboxIssueCurrentTab.OPEN ? ( diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx index 5b0c5728a..8d76603ac 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -34,8 +34,8 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { {archivedWorkItemFilter?.hasActiveFilters ? ( { ) : cycleWorkItemFilter?.hasActiveFilters ? ( { {moduleWorkItemFilter?.hasActiveFilters ? ( { {projectWorkItemFilter?.hasActiveFilters ? ( { return ( ); diff --git a/apps/web/core/components/pages/pages-list-main-content.tsx b/apps/web/core/components/pages/pages-list-main-content.tsx index 42fbf1cce..960657bbf 100644 --- a/apps/web/core/components/pages/pages-list-main-content.tsx +++ b/apps/web/core/components/pages/pages-list-main-content.tsx @@ -162,8 +162,8 @@ export const PagesListMainContent: React.FC = observer((props) => { return ( ); diff --git a/apps/web/core/components/project/card-list.tsx b/apps/web/core/components/project/card-list.tsx index 05a74028b..caf0c5166 100644 --- a/apps/web/core/components/project/card-list.tsx +++ b/apps/web/core/components/project/card-list.tsx @@ -78,13 +78,13 @@ export const ProjectCardList = observer((props: TProjectCardListProps) => { currentWorkspaceDisplayFilters?.archived_projects && calculateTotalFilters(currentWorkspaceFilters ?? {}) === 0 ? t("workspace.projects_archived.title") - : t("common.search.title") + : t("common_empty_state.search.title") } description={ currentWorkspaceDisplayFilters?.archived_projects && calculateTotalFilters(currentWorkspaceFilters ?? {}) === 0 ? t("workspace.projects_archived.description") - : t("common.search.description") + : t("common_empty_state.search.description") } assetKey={ currentWorkspaceDisplayFilters?.archived_projects && diff --git a/apps/web/core/components/views/views-list.tsx b/apps/web/core/components/views/views-list.tsx index e078131b3..530404664 100644 --- a/apps/web/core/components/views/views-list.tsx +++ b/apps/web/core/components/views/views-list.tsx @@ -37,8 +37,8 @@ export const ProjectViewsList = observer(() => { return ( ); } diff --git a/packages/i18n/src/locales/cs/empty-state.ts b/packages/i18n/src/locales/cs/empty-state.ts index 3c1641a0f..c877543ea 100644 --- a/packages/i18n/src/locales/cs/empty-state.ts +++ b/packages/i18n/src/locales/cs/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Zatím nejsou k dispozici žádné metriky pokroku.", description: "Začněte nastavovat hodnoty vlastností v pracovních položkách, abyste zde viděli metriky pokroku.", diff --git a/packages/i18n/src/locales/de/empty-state.ts b/packages/i18n/src/locales/de/empty-state.ts index 0c7a27eb7..85ec7b4b9 100644 --- a/packages/i18n/src/locales/de/empty-state.ts +++ b/packages/i18n/src/locales/de/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Es gibt noch keine Fortschrittsmetriken anzuzeigen.", description: diff --git a/packages/i18n/src/locales/en/empty-state.ts b/packages/i18n/src/locales/en/empty-state.ts index ef8265b12..adf030d64 100644 --- a/packages/i18n/src/locales/en/empty-state.ts +++ b/packages/i18n/src/locales/en/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "There're no progress metrics to show yet.", description: "Start setting property values in work items to see progress metrics here.", diff --git a/packages/i18n/src/locales/es/empty-state.ts b/packages/i18n/src/locales/es/empty-state.ts index 3ee97c54f..d328620a9 100644 --- a/packages/i18n/src/locales/es/empty-state.ts +++ b/packages/i18n/src/locales/es/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Todavía no hay métricas de progreso para mostrar.", description: diff --git a/packages/i18n/src/locales/fr/empty-state.ts b/packages/i18n/src/locales/fr/empty-state.ts index cf86b413c..26890b985 100644 --- a/packages/i18n/src/locales/fr/empty-state.ts +++ b/packages/i18n/src/locales/fr/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Il n'y a pas encore de métriques de progression à afficher.", description: diff --git a/packages/i18n/src/locales/id/empty-state.ts b/packages/i18n/src/locales/id/empty-state.ts index c6238e184..d5e42daea 100644 --- a/packages/i18n/src/locales/id/empty-state.ts +++ b/packages/i18n/src/locales/id/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Belum ada metrik progres untuk ditampilkan.", description: "Mulai mengatur nilai properti dalam item kerja untuk melihat metrik progres di sini.", diff --git a/packages/i18n/src/locales/it/empty-state.ts b/packages/i18n/src/locales/it/empty-state.ts index 3417eab8d..24370ef8d 100644 --- a/packages/i18n/src/locales/it/empty-state.ts +++ b/packages/i18n/src/locales/it/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Non ci sono ancora metriche di progresso da mostrare.", description: diff --git a/packages/i18n/src/locales/ja/empty-state.ts b/packages/i18n/src/locales/ja/empty-state.ts index 90a3af7e1..f41d58abf 100644 --- a/packages/i18n/src/locales/ja/empty-state.ts +++ b/packages/i18n/src/locales/ja/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "表示する進捗メトリクスがまだありません。", description: "作業項目にプロパティ値を設定して、ここに進捗メトリクスを表示します。", diff --git a/packages/i18n/src/locales/ko/empty-state.ts b/packages/i18n/src/locales/ko/empty-state.ts index c85b87e2f..c3aed417c 100644 --- a/packages/i18n/src/locales/ko/empty-state.ts +++ b/packages/i18n/src/locales/ko/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "아직 표시할 진행 지표가 없습니다.", description: "작업 항목에서 속성 값을 설정하여 여기에서 진행 지표를 확인하세요.", diff --git a/packages/i18n/src/locales/pl/empty-state.ts b/packages/i18n/src/locales/pl/empty-state.ts index 0fe9c9fb4..7a6fccfc6 100644 --- a/packages/i18n/src/locales/pl/empty-state.ts +++ b/packages/i18n/src/locales/pl/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Nie ma jeszcze metryk postępu do wyświetlenia.", description: "Zacznij ustawiać wartości właściwości w elementach roboczych, aby zobaczyć tutaj metryki postępu.", diff --git a/packages/i18n/src/locales/pt-BR/empty-state.ts b/packages/i18n/src/locales/pt-BR/empty-state.ts index 6c3ebc94a..b020a899a 100644 --- a/packages/i18n/src/locales/pt-BR/empty-state.ts +++ b/packages/i18n/src/locales/pt-BR/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Ainda não há métricas de progresso para mostrar.", description: diff --git a/packages/i18n/src/locales/ro/empty-state.ts b/packages/i18n/src/locales/ro/empty-state.ts index 71d2c3031..1068fe673 100644 --- a/packages/i18n/src/locales/ro/empty-state.ts +++ b/packages/i18n/src/locales/ro/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Nu există încă metrici de progres de afișat.", description: diff --git a/packages/i18n/src/locales/ru/empty-state.ts b/packages/i18n/src/locales/ru/empty-state.ts index 3bdc6d393..ecef1145f 100644 --- a/packages/i18n/src/locales/ru/empty-state.ts +++ b/packages/i18n/src/locales/ru/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Пока нет показателей прогресса для отображения.", description: diff --git a/packages/i18n/src/locales/sk/empty-state.ts b/packages/i18n/src/locales/sk/empty-state.ts index 8478fde4a..221c3c17b 100644 --- a/packages/i18n/src/locales/sk/empty-state.ts +++ b/packages/i18n/src/locales/sk/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Zatiaľ nie sú k dispozícii žiadne metriky pokroku.", description: "Začnite nastavovať hodnoty vlastností v pracovných položkách, aby ste tu videli metriky pokroku.", diff --git a/packages/i18n/src/locales/tr-TR/empty-state.ts b/packages/i18n/src/locales/tr-TR/empty-state.ts index a1cb6296d..726933884 100644 --- a/packages/i18n/src/locales/tr-TR/empty-state.ts +++ b/packages/i18n/src/locales/tr-TR/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Henüz gösterilecek ilerleme metriği yok.", description: "İlerleme metriklerini burada görmek için iş öğelerinde özellik değerleri belirlemeye başlayın.", diff --git a/packages/i18n/src/locales/ua/empty-state.ts b/packages/i18n/src/locales/ua/empty-state.ts index 12cc622e2..9efe17c0c 100644 --- a/packages/i18n/src/locales/ua/empty-state.ts +++ b/packages/i18n/src/locales/ua/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Ще немає метрик прогресу для відображення.", description: diff --git a/packages/i18n/src/locales/vi-VN/empty-state.ts b/packages/i18n/src/locales/vi-VN/empty-state.ts index 6899c509e..569a83c22 100644 --- a/packages/i18n/src/locales/vi-VN/empty-state.ts +++ b/packages/i18n/src/locales/vi-VN/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "Chưa có số liệu tiến độ để hiển thị.", description: "Bắt đầu đặt giá trị thuộc tính trong các mục công việc để xem số liệu tiến độ ở đây.", diff --git a/packages/i18n/src/locales/zh-CN/empty-state.ts b/packages/i18n/src/locales/zh-CN/empty-state.ts index 7d0cfb531..fd78cc19d 100644 --- a/packages/i18n/src/locales/zh-CN/empty-state.ts +++ b/packages/i18n/src/locales/zh-CN/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "暂无进度指标可显示。", description: "开始在工作项中设置属性值以在此查看进度指标。", diff --git a/packages/i18n/src/locales/zh-TW/empty-state.ts b/packages/i18n/src/locales/zh-TW/empty-state.ts index b7c3f4bdc..eb397cf58 100644 --- a/packages/i18n/src/locales/zh-TW/empty-state.ts +++ b/packages/i18n/src/locales/zh-TW/empty-state.ts @@ -1,5 +1,5 @@ export default { - common: { + common_empty_state: { progress: { title: "暫無進度指標可顯示。", description: "開始在工作項中設定屬性值以在此查看進度指標。", From b7aa25f2c6340b2b30f60aed9b9c39a00bf75eca Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:31:06 +0530 Subject: [PATCH 021/212] [WEB-5248] chore: translations and empty state refactor (#8021) * chore: translations code refactoring * chore: empty state translation refactor * chore: code refactor --- .../[projectId]/cycles/(list)/page.tsx | 6 ++--- .../settings/(workspace)/webhooks/page.tsx | 6 ++--- .../settings/account/api-tokens/page.tsx | 6 ++--- .../analytics/insight-table/data-table.tsx | 2 +- .../analytics/overview/project-insights.tsx | 2 +- .../work-items/created-vs-resolved.tsx | 2 +- .../analytics/work-items/priority-chart.tsx | 2 +- .../cycles/archived-cycles/root.tsx | 4 ++-- apps/web/core/components/estimates/root.tsx | 6 ++--- .../core/components/exporter/prev-exports.tsx | 4 ++-- .../home/widgets/empty-states/links.tsx | 2 +- apps/web/core/components/inbox/root.tsx | 6 ++++- .../core/components/inbox/sidebar/root.tsx | 6 ++--- .../empty-states/archived-issues.tsx | 6 ++--- .../issue-layouts/empty-states/cycle.tsx | 8 +++---- .../issue-layouts/empty-states/module.tsx | 8 +++---- .../empty-states/project-issues.tsx | 6 ++--- .../issues/workspace-draft/empty-state.tsx | 6 ++--- .../labels/project-setting-label-list.tsx | 6 ++--- .../modules/archived-modules/root.tsx | 4 ++-- .../components/modules/modules-list-view.tsx | 6 ++--- .../pages/pages-list-main-content.tsx | 22 +++++++++---------- .../overview/priority-distribution.tsx | 2 +- .../profile/overview/state-distribution.tsx | 2 +- .../web/core/components/project/card-list.tsx | 4 ++-- apps/web/core/components/views/views-list.tsx | 6 ++--- .../sidebar/empty-state.tsx | 4 ++-- packages/i18n/src/locales/cs/empty-state.ts | 6 ++--- packages/i18n/src/locales/de/empty-state.ts | 6 ++--- packages/i18n/src/locales/en/empty-state.ts | 6 ++--- packages/i18n/src/locales/es/empty-state.ts | 6 ++--- packages/i18n/src/locales/fr/empty-state.ts | 6 ++--- packages/i18n/src/locales/id/empty-state.ts | 6 ++--- packages/i18n/src/locales/it/empty-state.ts | 6 ++--- packages/i18n/src/locales/ja/empty-state.ts | 6 ++--- packages/i18n/src/locales/ko/empty-state.ts | 6 ++--- packages/i18n/src/locales/pl/empty-state.ts | 6 ++--- .../i18n/src/locales/pt-BR/empty-state.ts | 6 ++--- packages/i18n/src/locales/ro/empty-state.ts | 6 ++--- packages/i18n/src/locales/ru/empty-state.ts | 6 ++--- packages/i18n/src/locales/sk/empty-state.ts | 6 ++--- .../i18n/src/locales/tr-TR/empty-state.ts | 6 ++--- packages/i18n/src/locales/ua/empty-state.ts | 6 ++--- .../i18n/src/locales/vi-VN/empty-state.ts | 6 ++--- .../i18n/src/locales/zh-CN/empty-state.ts | 6 ++--- .../i18n/src/locales/zh-TW/empty-state.ts | 6 ++--- 46 files changed, 131 insertions(+), 127 deletions(-) diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx index 643ee2850..a4dbf9d64 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx @@ -98,11 +98,11 @@ const ProjectCyclesPage = observer(() => {
setCreateModal(true), variant: "primary", disabled: !hasMemberLevelPermission, diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx index efbe979ba..bfcfbd444 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx @@ -90,11 +90,11 @@ const WebhooksListPage = observer(() => {
{ captureClick({ elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_WEBHOOK_BUTTON, diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx index 0cf57fa18..5e9c17a17 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx @@ -85,11 +85,11 @@ const ApiTokensPage = observer(() => { { captureClick({ elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_PAT_BUTTON, diff --git a/apps/web/core/components/analytics/insight-table/data-table.tsx b/apps/web/core/components/analytics/insight-table/data-table.tsx index b2729411d..83892ceb4 100644 --- a/apps/web/core/components/analytics/insight-table/data-table.tsx +++ b/apps/web/core/components/analytics/insight-table/data-table.tsx @@ -157,7 +157,7 @@ export function DataTable({ columns, data, searchPlaceholder, act assetKey="unknown" assetClassName="size-20" rootClassName="border border-custom-border-100 px-5 py-10 md:py-20 md:px-20" - title={t("workspace.analytics_work_items.title")} + title={t("workspace_empty_state.analytics_work_items.title")} /> diff --git a/apps/web/core/components/analytics/overview/project-insights.tsx b/apps/web/core/components/analytics/overview/project-insights.tsx index c1fd6584e..df7efddb7 100644 --- a/apps/web/core/components/analytics/overview/project-insights.tsx +++ b/apps/web/core/components/analytics/overview/project-insights.tsx @@ -58,7 +58,7 @@ const ProjectInsights = observer(() => { assetKey="unknown" assetClassName="size-20" rootClassName="border border-custom-border-100 px-5 py-10 md:py-20 md:px-20" - title={t("workspace.analytics_work_items.title")} + title={t("workspace_empty_state.analytics_work_items.title")} /> ) : (
diff --git a/apps/web/core/components/analytics/work-items/created-vs-resolved.tsx b/apps/web/core/components/analytics/work-items/created-vs-resolved.tsx index 7b40ccdb9..f92c42d04 100644 --- a/apps/web/core/components/analytics/work-items/created-vs-resolved.tsx +++ b/apps/web/core/components/analytics/work-items/created-vs-resolved.tsx @@ -123,7 +123,7 @@ const CreatedVsResolved = observer(() => { assetKey="unknown" assetClassName="size-20" rootClassName="border border-custom-border-100 px-5 py-10 md:py-20 md:px-20" - title={t("workspace.analytics_work_items.title")} + title={t("workspace_empty_state.analytics_work_items.title")} /> )} diff --git a/apps/web/core/components/analytics/work-items/priority-chart.tsx b/apps/web/core/components/analytics/work-items/priority-chart.tsx index 857eca203..4fb0304d6 100644 --- a/apps/web/core/components/analytics/work-items/priority-chart.tsx +++ b/apps/web/core/components/analytics/work-items/priority-chart.tsx @@ -234,7 +234,7 @@ const PriorityChart = observer((props: Props) => { assetKey="unknown" assetClassName="size-20" rootClassName="border border-custom-border-100 px-5 py-10 md:py-20 md:px-20" - title={t("workspace.analytics_work_items.title")} + title={t("workspace_empty_state.analytics_work_items.title")} /> )}
diff --git a/apps/web/core/components/cycles/archived-cycles/root.tsx b/apps/web/core/components/cycles/archived-cycles/root.tsx index fdf527de8..a171b9aa5 100644 --- a/apps/web/core/components/cycles/archived-cycles/root.tsx +++ b/apps/web/core/components/cycles/archived-cycles/root.tsx @@ -69,8 +69,8 @@ export const ArchivedCycleLayoutRoot: React.FC = observer(() => {
) : ( diff --git a/apps/web/core/components/estimates/root.tsx b/apps/web/core/components/estimates/root.tsx index 01ac55a6a..9e6f9bd7b 100644 --- a/apps/web/core/components/estimates/root.tsx +++ b/apps/web/core/components/estimates/root.tsx @@ -79,11 +79,11 @@ export const EstimateRoot: FC = observer((props) => { setIsEstimateCreateModalOpen(true), }, ]} diff --git a/apps/web/core/components/exporter/prev-exports.tsx b/apps/web/core/components/exporter/prev-exports.tsx index 8498a6ca2..c7a7b3bee 100644 --- a/apps/web/core/components/exporter/prev-exports.tsx +++ b/apps/web/core/components/exporter/prev-exports.tsx @@ -124,8 +124,8 @@ export const PrevExports = observer((props: Props) => {
diff --git a/apps/web/core/components/home/widgets/empty-states/links.tsx b/apps/web/core/components/home/widgets/empty-states/links.tsx index d44293432..d375d5d87 100644 --- a/apps/web/core/components/home/widgets/empty-states/links.tsx +++ b/apps/web/core/components/home/widgets/empty-states/links.tsx @@ -8,7 +8,7 @@ export const LinksEmptyState = () => {
); diff --git a/apps/web/core/components/inbox/root.tsx b/apps/web/core/components/inbox/root.tsx index a8660ecce..ec2411a47 100644 --- a/apps/web/core/components/inbox/root.tsx +++ b/apps/web/core/components/inbox/root.tsx @@ -101,7 +101,11 @@ export const InboxIssueRoot: FC = observer((props) => { inboxIssueId={inboxIssueId.toString()} /> ) : ( - + )}
diff --git a/apps/web/core/components/inbox/sidebar/root.tsx b/apps/web/core/components/inbox/sidebar/root.tsx index ef050ae41..16558d2ff 100644 --- a/apps/web/core/components/inbox/sidebar/root.tsx +++ b/apps/web/core/components/inbox/sidebar/root.tsx @@ -144,12 +144,12 @@ export const InboxSidebar: FC = observer((props) => { ) : currentTab === EInboxIssueCurrentTab.OPEN ? ( router.push(`/${workspaceSlug}/projects/${projectId}/intake`), variant: "primary", }, diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx index 8d76603ac..5563e6f90 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -48,11 +48,11 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { ) : ( router.push(`/${workspaceSlug}/settings/projects/${projectId}/automations`), disabled: !canPerformEmptyStateActions, variant: "primary", diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/cycle.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/cycle.tsx index 137e5a6d7..0f636b11a 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/cycle.tsx @@ -103,11 +103,11 @@ export const CycleEmptyState: React.FC = observer(() => { ) : ( { captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.CYCLE }); toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); @@ -117,7 +117,7 @@ export const CycleEmptyState: React.FC = observer(() => { "data-ph-element": WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.CYCLE, }, { - label: t("project.cycle_work_items.cta_secondary"), + label: t("project_empty_state.cycle_work_items.cta_secondary"), onClick: () => setCycleIssuesListModal(true), disabled: !canPerformEmptyStateActions, variant: "outline-primary", diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/module.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/module.tsx index d8df95b51..aa60aedc9 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/module.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/module.tsx @@ -90,11 +90,11 @@ export const ModuleEmptyState: React.FC = observer(() => { ) : ( { captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.MODULE }); toggleCreateIssueModal(true, EIssuesStoreType.MODULE); @@ -103,7 +103,7 @@ export const ModuleEmptyState: React.FC = observer(() => { variant: "primary", }, { - label: t("project.module_work_items.cta_secondary"), + label: t("project_empty_state.module_work_items.cta_secondary"), onClick: () => setModuleIssuesListModal(true), disabled: !canPerformEmptyStateActions, variant: "outline-primary", diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx index 4765b687d..a5a5e9ccb 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -48,11 +48,11 @@ export const ProjectEmptyState: React.FC = observer(() => { ) : ( { captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.WORK_ITEMS }); toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); diff --git a/apps/web/core/components/issues/workspace-draft/empty-state.tsx b/apps/web/core/components/issues/workspace-draft/empty-state.tsx index 89ff7cb14..37e19ca4c 100644 --- a/apps/web/core/components/issues/workspace-draft/empty-state.tsx +++ b/apps/web/core/components/issues/workspace-draft/empty-state.tsx @@ -34,13 +34,13 @@ export const WorkspaceDraftEmptyState: FC = observer(() => { />
{ setIsDraftIssueModalOpen(true); }, diff --git a/apps/web/core/components/labels/project-setting-label-list.tsx b/apps/web/core/components/labels/project-setting-label-list.tsx index 62f60250e..bf4c13693 100644 --- a/apps/web/core/components/labels/project-setting-label-list.tsx +++ b/apps/web/core/components/labels/project-setting-label-list.tsx @@ -112,11 +112,11 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { { newLabel(); captureClick({ diff --git a/apps/web/core/components/modules/archived-modules/root.tsx b/apps/web/core/components/modules/archived-modules/root.tsx index 31fb196ca..6598458ad 100644 --- a/apps/web/core/components/modules/archived-modules/root.tsx +++ b/apps/web/core/components/modules/archived-modules/root.tsx @@ -72,8 +72,8 @@ export const ArchivedModuleLayoutRoot: React.FC = observer(() => {
) : ( diff --git a/apps/web/core/components/modules/modules-list-view.tsx b/apps/web/core/components/modules/modules-list-view.tsx index c62159fc8..039faec59 100644 --- a/apps/web/core/components/modules/modules-list-view.tsx +++ b/apps/web/core/components/modules/modules-list-view.tsx @@ -50,11 +50,11 @@ export const ModulesListView: React.FC = observer(() => { return ( toggleCreateModuleModal(true), disabled: !canPerformEmptyStateActions, variant: "primary", diff --git a/apps/web/core/components/pages/pages-list-main-content.tsx b/apps/web/core/components/pages/pages-list-main-content.tsx index 960657bbf..1746973a9 100644 --- a/apps/web/core/components/pages/pages-list-main-content.tsx +++ b/apps/web/core/components/pages/pages-list-main-content.tsx @@ -94,11 +94,11 @@ export const PagesListMainContent: React.FC = observer((props) => { return ( { handleCreatePage(); captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON }); @@ -114,11 +114,11 @@ export const PagesListMainContent: React.FC = observer((props) => { return ( { handleCreatePage(); captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON }); @@ -133,11 +133,11 @@ export const PagesListMainContent: React.FC = observer((props) => { return ( { handleCreatePage(); captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON }); @@ -152,8 +152,8 @@ export const PagesListMainContent: React.FC = observer((props) => { return ( ); } diff --git a/apps/web/core/components/profile/overview/priority-distribution.tsx b/apps/web/core/components/profile/overview/priority-distribution.tsx index 98f79a518..0dc98b4ba 100644 --- a/apps/web/core/components/profile/overview/priority-distribution.tsx +++ b/apps/web/core/components/profile/overview/priority-distribution.tsx @@ -62,7 +62,7 @@ export const ProfilePriorityDistribution: React.FC = ({ userProfile }) => )} diff --git a/apps/web/core/components/profile/overview/state-distribution.tsx b/apps/web/core/components/profile/overview/state-distribution.tsx index 0b4024f21..0ef6a7b5f 100644 --- a/apps/web/core/components/profile/overview/state-distribution.tsx +++ b/apps/web/core/components/profile/overview/state-distribution.tsx @@ -74,7 +74,7 @@ export const ProfileStateDistribution: React.FC = ({ stateDistribution, u )} diff --git a/apps/web/core/components/project/card-list.tsx b/apps/web/core/components/project/card-list.tsx index caf0c5166..d4e0ef879 100644 --- a/apps/web/core/components/project/card-list.tsx +++ b/apps/web/core/components/project/card-list.tsx @@ -77,13 +77,13 @@ export const ProjectCardList = observer((props: TProjectCardListProps) => { title={ currentWorkspaceDisplayFilters?.archived_projects && calculateTotalFilters(currentWorkspaceFilters ?? {}) === 0 - ? t("workspace.projects_archived.title") + ? t("workspace_empty_state.projects_archived.title") : t("common_empty_state.search.title") } description={ currentWorkspaceDisplayFilters?.archived_projects && calculateTotalFilters(currentWorkspaceFilters ?? {}) === 0 - ? t("workspace.projects_archived.description") + ? t("workspace_empty_state.projects_archived.description") : t("common_empty_state.search.description") } assetKey={ diff --git a/apps/web/core/components/views/views-list.tsx b/apps/web/core/components/views/views-list.tsx index 530404664..96889d1da 100644 --- a/apps/web/core/components/views/views-list.tsx +++ b/apps/web/core/components/views/views-list.tsx @@ -58,11 +58,11 @@ export const ProjectViewsList = observer(() => { ) : ( toggleCreateViewModal(true), disabled: !canPerformEmptyStateActions, variant: "primary", diff --git a/apps/web/core/components/workspace-notifications/sidebar/empty-state.tsx b/apps/web/core/components/workspace-notifications/sidebar/empty-state.tsx index 0c81aed47..cacdf7cf9 100644 --- a/apps/web/core/components/workspace-notifications/sidebar/empty-state.tsx +++ b/apps/web/core/components/workspace-notifications/sidebar/empty-state.tsx @@ -22,8 +22,8 @@ export const NotificationEmptyState: FC = observer assetClassName="size-24" title={ currentNotificationTab === ENotificationTab.ALL - ? t("workspace.inbox_sidebar_all.title") - : t("workspace.inbox_sidebar_mentions.title") + ? t("workspace_empty_state.inbox_sidebar_all.title") + : t("workspace_empty_state.inbox_sidebar_mentions.title") } className="max-w-56" /> diff --git a/packages/i18n/src/locales/cs/empty-state.ts b/packages/i18n/src/locales/cs/empty-state.ts index c877543ea..4e552ea0b 100644 --- a/packages/i18n/src/locales/cs/empty-state.ts +++ b/packages/i18n/src/locales/cs/empty-state.ts @@ -23,7 +23,7 @@ export default { cta_primary: "Zkuste znovu načíst", }, }, - project: { + project_empty_state: { work_items: { title: "Začněte s vaší první pracovní položkou.", description: @@ -92,7 +92,7 @@ export default { title: "Vyberte příchozí pracovní položku pro zobrazení jejích podrobností", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Zatím žádné archivované pracovní položky", description: @@ -156,7 +156,7 @@ export default { title: "Nastavte příjem pro správu příchozích požadavků a sledování, jak jsou přijímány a odmítány", }, }, - settings: { + settings_empty_state: { estimates: { title: "Zatím žádné odhady", description: "Definujte, jak váš tým měří úsilí, a sledujte to konzistentně napříč všemi pracovními položkami.", diff --git a/packages/i18n/src/locales/de/empty-state.ts b/packages/i18n/src/locales/de/empty-state.ts index 85ec7b4b9..f5b2fae25 100644 --- a/packages/i18n/src/locales/de/empty-state.ts +++ b/packages/i18n/src/locales/de/empty-state.ts @@ -25,7 +25,7 @@ export default { cta_primary: "Versuchen Sie neu zu laden", }, }, - project: { + project_empty_state: { work_items: { title: "Beginnen Sie mit Ihrem ersten Arbeitselement.", description: @@ -94,7 +94,7 @@ export default { title: "Wählen Sie ein Intake-Arbeitselement aus, um seine Details anzuzeigen", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Noch keine archivierten Arbeitselemente", description: @@ -162,7 +162,7 @@ export default { "Richten Sie Intake ein, um eingehende Anfragen zu verwalten und zu verfolgen, wie sie akzeptiert und abgelehnt werden", }, }, - settings: { + settings_empty_state: { estimates: { title: "Noch keine Schätzungen", description: diff --git a/packages/i18n/src/locales/en/empty-state.ts b/packages/i18n/src/locales/en/empty-state.ts index adf030d64..8ef996f4f 100644 --- a/packages/i18n/src/locales/en/empty-state.ts +++ b/packages/i18n/src/locales/en/empty-state.ts @@ -23,7 +23,7 @@ export default { cta_primary: "Try reloading", }, }, - project: { + project_empty_state: { work_items: { title: "Start with your first work item.", description: @@ -90,7 +90,7 @@ export default { title: "Select an Intake work item to view its details", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "No archived work items yet", description: @@ -152,7 +152,7 @@ export default { title: "Set up intake to manage incoming requests and track how they're accepted and rejected", }, }, - settings: { + settings_empty_state: { estimates: { title: "No estimates yet", description: "Define how your team measures effort and track it consistently across all work items.", diff --git a/packages/i18n/src/locales/es/empty-state.ts b/packages/i18n/src/locales/es/empty-state.ts index d328620a9..0c062cdb4 100644 --- a/packages/i18n/src/locales/es/empty-state.ts +++ b/packages/i18n/src/locales/es/empty-state.ts @@ -25,7 +25,7 @@ export default { cta_primary: "Intentar recargar", }, }, - project: { + project_empty_state: { work_items: { title: "Comienza con tu primer elemento de trabajo.", description: @@ -93,7 +93,7 @@ export default { title: "Selecciona un elemento de trabajo de Entrada para ver sus detalles", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Aún no hay elementos de trabajo archivados", description: @@ -158,7 +158,7 @@ export default { title: "Configura la entrada para gestionar las solicitudes entrantes y rastrear cómo se aceptan y rechazan", }, }, - settings: { + settings_empty_state: { estimates: { title: "Aún no hay estimaciones", description: diff --git a/packages/i18n/src/locales/fr/empty-state.ts b/packages/i18n/src/locales/fr/empty-state.ts index 26890b985..061fb19b7 100644 --- a/packages/i18n/src/locales/fr/empty-state.ts +++ b/packages/i18n/src/locales/fr/empty-state.ts @@ -26,7 +26,7 @@ export default { cta_primary: "Essayer de recharger", }, }, - project: { + project_empty_state: { work_items: { title: "Commencez avec votre premier élément de travail.", description: @@ -95,7 +95,7 @@ export default { title: "Sélectionnez un élément de travail Intake pour voir ses détails", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Aucun élément de travail archivé pour le moment", description: @@ -160,7 +160,7 @@ export default { title: "Configurez l'intake pour gérer les demandes entrantes et suivre comment elles sont acceptées et rejetées", }, }, - settings: { + settings_empty_state: { estimates: { title: "Aucune estimation pour le moment", description: diff --git a/packages/i18n/src/locales/id/empty-state.ts b/packages/i18n/src/locales/id/empty-state.ts index d5e42daea..8224ec78e 100644 --- a/packages/i18n/src/locales/id/empty-state.ts +++ b/packages/i18n/src/locales/id/empty-state.ts @@ -24,7 +24,7 @@ export default { cta_primary: "Coba muat ulang", }, }, - project: { + project_empty_state: { work_items: { title: "Mulai dengan item kerja pertama Anda.", description: @@ -92,7 +92,7 @@ export default { title: "Pilih item kerja Masuk untuk melihat detailnya", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Belum ada item kerja yang diarsipkan", description: @@ -158,7 +158,7 @@ export default { title: "Siapkan masukan untuk mengelola permintaan masuk dan melacak bagaimana mereka diterima dan ditolak", }, }, - settings: { + settings_empty_state: { estimates: { title: "Belum ada estimasi", description: "Tentukan bagaimana tim Anda mengukur upaya dan lacak secara konsisten di semua item kerja.", diff --git a/packages/i18n/src/locales/it/empty-state.ts b/packages/i18n/src/locales/it/empty-state.ts index 24370ef8d..279241ebb 100644 --- a/packages/i18n/src/locales/it/empty-state.ts +++ b/packages/i18n/src/locales/it/empty-state.ts @@ -26,7 +26,7 @@ export default { cta_primary: "Prova a ricaricare", }, }, - project: { + project_empty_state: { work_items: { title: "Inizia con il tuo primo elemento di lavoro.", description: @@ -95,7 +95,7 @@ export default { title: "Seleziona un elemento di lavoro di Intake per visualizzarne i dettagli", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Nessun elemento di lavoro archiviato ancora", description: @@ -159,7 +159,7 @@ export default { title: "Imposta intake per gestire le richieste in arrivo e tracciare come vengono accettate e rifiutate", }, }, - settings: { + settings_empty_state: { estimates: { title: "Nessuna stima ancora", description: diff --git a/packages/i18n/src/locales/ja/empty-state.ts b/packages/i18n/src/locales/ja/empty-state.ts index f41d58abf..6463c5ccf 100644 --- a/packages/i18n/src/locales/ja/empty-state.ts +++ b/packages/i18n/src/locales/ja/empty-state.ts @@ -23,7 +23,7 @@ export default { cta_primary: "再読み込みを試す", }, }, - project: { + project_empty_state: { work_items: { title: "最初の作業項目から始めましょう。", description: @@ -89,7 +89,7 @@ export default { title: "インテーク作業項目を選択して詳細を表示", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "アーカイブされた作業項目はまだありません", description: @@ -151,7 +151,7 @@ export default { title: "インテークを設定して、受信リクエストを管理し、承認と拒否を追跡します", }, }, - settings: { + settings_empty_state: { estimates: { title: "まだ見積もりはありません", description: "チームが労力をどのように測定するかを定義し、すべての作業項目で一貫して追跡します。", diff --git a/packages/i18n/src/locales/ko/empty-state.ts b/packages/i18n/src/locales/ko/empty-state.ts index c3aed417c..093091f65 100644 --- a/packages/i18n/src/locales/ko/empty-state.ts +++ b/packages/i18n/src/locales/ko/empty-state.ts @@ -23,7 +23,7 @@ export default { cta_primary: "다시 로드 시도", }, }, - project: { + project_empty_state: { work_items: { title: "첫 번째 작업 항목으로 시작하세요.", description: @@ -89,7 +89,7 @@ export default { title: "접수 작업 항목을 선택하여 세부 정보 보기", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "아직 보관된 작업 항목이 없습니다", description: @@ -150,7 +150,7 @@ export default { title: "접수를 설정하여 들어오는 요청을 관리하고 승인 및 거부 방법을 추적하세요", }, }, - settings: { + settings_empty_state: { estimates: { title: "아직 추정치가 없습니다", description: "팀이 노력을 측정하는 방법을 정의하고 모든 작업 항목에서 일관되게 추적하세요.", diff --git a/packages/i18n/src/locales/pl/empty-state.ts b/packages/i18n/src/locales/pl/empty-state.ts index 7a6fccfc6..30fc30a6a 100644 --- a/packages/i18n/src/locales/pl/empty-state.ts +++ b/packages/i18n/src/locales/pl/empty-state.ts @@ -23,7 +23,7 @@ export default { cta_primary: "Spróbuj przeładować", }, }, - project: { + project_empty_state: { work_items: { title: "Zacznij od swojego pierwszego elementu roboczego.", description: @@ -93,7 +93,7 @@ export default { title: "Wybierz element roboczy Intake, aby wyświetlić jego szczegóły", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Jeszcze brak zarchiwizowanych elementów roboczych", description: @@ -158,7 +158,7 @@ export default { "Skonfiguruj przyjmowanie, aby zarządzać przychodzącymi zgłoszeniami i śledzić, jak są akceptowane i odrzucane", }, }, - settings: { + settings_empty_state: { estimates: { title: "Jeszcze brak szacunków", description: diff --git a/packages/i18n/src/locales/pt-BR/empty-state.ts b/packages/i18n/src/locales/pt-BR/empty-state.ts index b020a899a..8d92f4ff1 100644 --- a/packages/i18n/src/locales/pt-BR/empty-state.ts +++ b/packages/i18n/src/locales/pt-BR/empty-state.ts @@ -25,7 +25,7 @@ export default { cta_primary: "Tentar recarregar", }, }, - project: { + project_empty_state: { work_items: { title: "Comece com seu primeiro item de trabalho.", description: @@ -94,7 +94,7 @@ export default { title: "Selecione um item de trabalho de Entrada para ver seus detalhes", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Ainda não há itens de trabalho arquivados", description: @@ -158,7 +158,7 @@ export default { title: "Configure a entrada para gerenciar solicitações recebidas e rastrear como elas são aceitas e rejeitadas", }, }, - settings: { + settings_empty_state: { estimates: { title: "Ainda não há estimativas", description: diff --git a/packages/i18n/src/locales/ro/empty-state.ts b/packages/i18n/src/locales/ro/empty-state.ts index 1068fe673..c123d82ab 100644 --- a/packages/i18n/src/locales/ro/empty-state.ts +++ b/packages/i18n/src/locales/ro/empty-state.ts @@ -24,7 +24,7 @@ export default { cta_primary: "Încercați reîncărcarea", }, }, - project: { + project_empty_state: { work_items: { title: "Începeți cu primul dvs. element de lucru.", description: @@ -93,7 +93,7 @@ export default { title: "Selectați un element de lucru de Admitere pentru a vedea detaliile", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Încă nu există elemente de lucru arhivate", description: @@ -157,7 +157,7 @@ export default { title: "Configurați admiterea pentru a gestiona solicitările primite și a urmări cum sunt acceptate și respinse", }, }, - settings: { + settings_empty_state: { estimates: { title: "Încă nu există estimări", description: diff --git a/packages/i18n/src/locales/ru/empty-state.ts b/packages/i18n/src/locales/ru/empty-state.ts index ecef1145f..eebe5d032 100644 --- a/packages/i18n/src/locales/ru/empty-state.ts +++ b/packages/i18n/src/locales/ru/empty-state.ts @@ -24,7 +24,7 @@ export default { cta_primary: "Попробовать перезагрузить", }, }, - project: { + project_empty_state: { work_items: { title: "Начните с вашего первого рабочего элемента.", description: @@ -94,7 +94,7 @@ export default { title: "Выберите рабочий элемент приема, чтобы просмотреть его детали", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Пока нет архивированных рабочих элементов", description: @@ -160,7 +160,7 @@ export default { title: "Настройте прием для управления входящими запросами и отслеживания их принятия и отклонения", }, }, - settings: { + settings_empty_state: { estimates: { title: "Пока нет оценок", description: diff --git a/packages/i18n/src/locales/sk/empty-state.ts b/packages/i18n/src/locales/sk/empty-state.ts index 221c3c17b..29ccb63bd 100644 --- a/packages/i18n/src/locales/sk/empty-state.ts +++ b/packages/i18n/src/locales/sk/empty-state.ts @@ -23,7 +23,7 @@ export default { cta_primary: "Skúste znovu načítať", }, }, - project: { + project_empty_state: { work_items: { title: "Začnite s vašou prvou pracovnou položkou.", description: @@ -92,7 +92,7 @@ export default { title: "Vyberte príchodzí pracovnú položku na zobrazenie jej podrobností", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Zatiaľ žiadne archivované pracovné položky", description: @@ -157,7 +157,7 @@ export default { title: "Nastavte príjem na správu prichádzajúcich požiadaviek a sledovanie, ako sú prijímané a odmietané", }, }, - settings: { + settings_empty_state: { estimates: { title: "Zatiaľ žiadne odhady", description: diff --git a/packages/i18n/src/locales/tr-TR/empty-state.ts b/packages/i18n/src/locales/tr-TR/empty-state.ts index 726933884..e1f74039a 100644 --- a/packages/i18n/src/locales/tr-TR/empty-state.ts +++ b/packages/i18n/src/locales/tr-TR/empty-state.ts @@ -23,7 +23,7 @@ export default { cta_primary: "Yeniden yüklemeyi dene", }, }, - project: { + project_empty_state: { work_items: { title: "İlk iş öğenizle başlayın.", description: @@ -91,7 +91,7 @@ export default { title: "Ayrıntılarını görmek için bir Giriş iş öğesi seçin", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Henüz arşivlenmiş iş öğesi yok", description: @@ -157,7 +157,7 @@ export default { "Gelen istekleri yönetmek ve bunların nasıl kabul edildiğini ve reddedildiğini izlemek için giriş ayarlayın", }, }, - settings: { + settings_empty_state: { estimates: { title: "Henüz tahmin yok", description: "Ekibinizin çabayı nasıl ölçtüğünü tanımlayın ve tüm iş öğelerinde tutarlı bir şekilde takip edin.", diff --git a/packages/i18n/src/locales/ua/empty-state.ts b/packages/i18n/src/locales/ua/empty-state.ts index 9efe17c0c..83dc3fb31 100644 --- a/packages/i18n/src/locales/ua/empty-state.ts +++ b/packages/i18n/src/locales/ua/empty-state.ts @@ -24,7 +24,7 @@ export default { cta_primary: "Спробуйте перезавантажити", }, }, - project: { + project_empty_state: { work_items: { title: "Почніть з вашого першого робочого елемента.", description: @@ -93,7 +93,7 @@ export default { title: "Виберіть робочий елемент Intake, щоб переглянути його деталі", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Ще немає архівованих робочих елементів", description: @@ -158,7 +158,7 @@ export default { "Налаштуйте вхід для управління вхідними запитами та відстеження того, як вони приймаються та відхиляються", }, }, - settings: { + settings_empty_state: { estimates: { title: "Ще немає оцінок", description: diff --git a/packages/i18n/src/locales/vi-VN/empty-state.ts b/packages/i18n/src/locales/vi-VN/empty-state.ts index 569a83c22..367b2f4c9 100644 --- a/packages/i18n/src/locales/vi-VN/empty-state.ts +++ b/packages/i18n/src/locales/vi-VN/empty-state.ts @@ -24,7 +24,7 @@ export default { cta_primary: "Thử tải lại", }, }, - project: { + project_empty_state: { work_items: { title: "Bắt đầu với mục công việc đầu tiên của bạn.", description: @@ -92,7 +92,7 @@ export default { title: "Chọn một mục công việc Tiếp nhận để xem chi tiết của nó", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "Chưa có mục công việc được lưu trữ", description: @@ -157,7 +157,7 @@ export default { title: "Thiết lập tiếp nhận để quản lý các yêu cầu đến và theo dõi cách chúng được chấp nhận và từ chối", }, }, - settings: { + settings_empty_state: { estimates: { title: "Chưa có ước tính", description: diff --git a/packages/i18n/src/locales/zh-CN/empty-state.ts b/packages/i18n/src/locales/zh-CN/empty-state.ts index fd78cc19d..67ffd5dde 100644 --- a/packages/i18n/src/locales/zh-CN/empty-state.ts +++ b/packages/i18n/src/locales/zh-CN/empty-state.ts @@ -23,7 +23,7 @@ export default { cta_primary: "尝试重新加载", }, }, - project: { + project_empty_state: { work_items: { title: "从您的第一个工作项开始。", description: "工作项是项目的构建块 — 分配负责人、设置优先级并轻松跟踪进度。", @@ -87,7 +87,7 @@ export default { title: "选择一个接收工作项以查看其详细信息", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "暂无已归档工作项", description: "通过手动或自动化,您可以归档已完成或已取消的工作项。归档后在此处查找它们。", @@ -146,7 +146,7 @@ export default { title: "设置接收以管理传入请求并跟踪它们的接受和拒绝情况", }, }, - settings: { + settings_empty_state: { estimates: { title: "暂无估算", description: "定义团队如何衡量工作量,并在所有工作项中一致地跟踪它。", diff --git a/packages/i18n/src/locales/zh-TW/empty-state.ts b/packages/i18n/src/locales/zh-TW/empty-state.ts index eb397cf58..77e201bd1 100644 --- a/packages/i18n/src/locales/zh-TW/empty-state.ts +++ b/packages/i18n/src/locales/zh-TW/empty-state.ts @@ -23,7 +23,7 @@ export default { cta_primary: "嘗試重新載入", }, }, - project: { + project_empty_state: { work_items: { title: "從您的第一個工作項開始。", description: "工作項是專案的建構模組 — 指派負責人、設定優先順序並輕鬆追蹤進度。", @@ -87,7 +87,7 @@ export default { title: "選擇一個接收工作項以查看其詳細資訊", }, }, - workspace: { + workspace_empty_state: { archive_work_items: { title: "暫無已封存工作項", description: "透過手動或自動化,您可以封存已完成或已取消的工作項。封存後在此處尋找它們。", @@ -146,7 +146,7 @@ export default { title: "設定接收以管理傳入請求並追蹤它們的接受和拒絕情況", }, }, - settings: { + settings_empty_state: { estimates: { title: "暫無估算", description: "定義團隊如何衡量工作量,並在所有工作項中一致地追蹤它。", From ec6e682044633120b5b57983c8a1162206eb2d7d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:47:10 +0530 Subject: [PATCH 022/212] [WEB-5255] fix: calendar layout weekly view (#8018) * fix: calendar layout weekly view * chore: code refactor --- .../store/issue/issue_calendar_view.store.ts | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/apps/web/core/store/issue/issue_calendar_view.store.ts b/apps/web/core/store/issue/issue_calendar_view.store.ts index 9e00cbd7e..fc9d34143 100644 --- a/apps/web/core/store/issue/issue_calendar_view.store.ts +++ b/apps/web/core/store/issue/issue_calendar_view.store.ts @@ -116,10 +116,30 @@ export class CalendarStore implements ICalendarStore { if (!this.calendarPayload) return undefined; const { activeWeekDate } = this.calendarFilters; + const year = activeWeekDate.getFullYear(); + const month = activeWeekDate.getMonth(); + const dayOfMonth = activeWeekDate.getDate(); - return this.calendarPayload[`y-${activeWeekDate.getFullYear()}`][`m-${activeWeekDate.getMonth()}`][ - `w-${this.activeWeekNumber - 1}` - ]; + // Check if calendar data exists for this year and month + const yearData = this.calendarPayload[`y-${year}`]; + if (!yearData) return undefined; + + const monthData = yearData[`m-${month}`]; + if (!monthData) return undefined; + + // Calculate firstDayOfMonth offset (same logic as calendar generation) + const startOfWeek = this.rootStore?.rootStore?.user?.userProfile?.data?.start_of_the_week ?? EStartOfTheWeek.SUNDAY; + const firstDayOfMonthRaw = new Date(year, month, 1).getDay(); + const firstDayOfMonth = (firstDayOfMonthRaw - startOfWeek + 7) % 7; + + // Calculate which sequential week this date falls into + const weekIndex = Math.floor((dayOfMonth - 1 + firstDayOfMonth) / 7); + + const weekKey = `w-${weekIndex}`; + if (!(weekKey in monthData)) { + return undefined; + } + return monthData[weekKey]; } getStartAndEndDate = computedFn((layout: "week" | "month") => { From 685e3fa2f8eabf5302ab7ebcd25cefd9f850440b Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:47:43 +0530 Subject: [PATCH 023/212] [WEB-5236] chore: attachment icon revamp (#8009) * chore: iconwrapper component added * chore: attachments icon added to propel * chore: code refactor * chore: code refactor --- .../src/icons/attachments/audio-file-icon.tsx | 13 +++++++++++++ .../propel/src/icons/attachments/code-file-icon.tsx | 13 +++++++++++++ .../src/icons/attachments/document-file-icon.tsx | 13 +++++++++++++ .../src/icons/attachments/image-file-icon.tsx | 13 +++++++++++++ packages/propel/src/icons/attachments/index.ts | 5 +++++ .../src/icons/attachments/video-file-icon.tsx | 13 +++++++++++++ packages/propel/src/icons/index.ts | 1 + 7 files changed, 71 insertions(+) create mode 100644 packages/propel/src/icons/attachments/audio-file-icon.tsx create mode 100644 packages/propel/src/icons/attachments/code-file-icon.tsx create mode 100644 packages/propel/src/icons/attachments/document-file-icon.tsx create mode 100644 packages/propel/src/icons/attachments/image-file-icon.tsx create mode 100644 packages/propel/src/icons/attachments/index.ts create mode 100644 packages/propel/src/icons/attachments/video-file-icon.tsx diff --git a/packages/propel/src/icons/attachments/audio-file-icon.tsx b/packages/propel/src/icons/attachments/audio-file-icon.tsx new file mode 100644 index 000000000..ce9477e3d --- /dev/null +++ b/packages/propel/src/icons/attachments/audio-file-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const AudioFileIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/attachments/code-file-icon.tsx b/packages/propel/src/icons/attachments/code-file-icon.tsx new file mode 100644 index 000000000..10a52834a --- /dev/null +++ b/packages/propel/src/icons/attachments/code-file-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const CodeFileIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/attachments/document-file-icon.tsx b/packages/propel/src/icons/attachments/document-file-icon.tsx new file mode 100644 index 000000000..57dbdfbbf --- /dev/null +++ b/packages/propel/src/icons/attachments/document-file-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const DocumentFileIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/attachments/image-file-icon.tsx b/packages/propel/src/icons/attachments/image-file-icon.tsx new file mode 100644 index 000000000..0c3dd8969 --- /dev/null +++ b/packages/propel/src/icons/attachments/image-file-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const ImageFileIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/attachments/index.ts b/packages/propel/src/icons/attachments/index.ts new file mode 100644 index 000000000..408dc37a3 --- /dev/null +++ b/packages/propel/src/icons/attachments/index.ts @@ -0,0 +1,5 @@ +export * from "./audio-file-icon"; +export * from "./code-file-icon"; +export * from "./document-file-icon"; +export * from "./image-file-icon"; +export * from "./video-file-icon"; diff --git a/packages/propel/src/icons/attachments/video-file-icon.tsx b/packages/propel/src/icons/attachments/video-file-icon.tsx new file mode 100644 index 000000000..dea3397ca --- /dev/null +++ b/packages/propel/src/icons/attachments/video-file-icon.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { IconWrapper } from "../icon-wrapper"; +import { ISvgIcons } from "../type"; + +export const VideoFileIcon: React.FC = ({ color = "currentColor", ...rest }) => ( + + + +); diff --git a/packages/propel/src/icons/index.ts b/packages/propel/src/icons/index.ts index ace2a4225..00c8f4472 100644 --- a/packages/propel/src/icons/index.ts +++ b/packages/propel/src/icons/index.ts @@ -6,6 +6,7 @@ export * from "./actions"; export * from "./activity-icon"; export * from "./ai-icon"; export * from "./at-risk-icon"; +export * from "./attachments"; export * from "./bar-icon"; export * from "./blocked-icon"; export * from "./blocker-icon"; From a8b6930486a4ce76d16373a63f6b9336030cac28 Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Tue, 28 Oct 2025 16:49:19 +0530 Subject: [PATCH 024/212] [WEB-5224] chore: raise warning instead of logging an exception for webhook_send_task (#7996) --- apps/api/plane/bgtasks/webhook_task.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/api/plane/bgtasks/webhook_task.py b/apps/api/plane/bgtasks/webhook_task.py index 2504eb734..3d04a65b7 100644 --- a/apps/api/plane/bgtasks/webhook_task.py +++ b/apps/api/plane/bgtasks/webhook_task.py @@ -86,7 +86,6 @@ def get_issue_prefetches(): ] - def save_webhook_log( webhook: Webhook, request_method: str, @@ -98,10 +97,9 @@ def save_webhook_log( retry_count: int, event_type: str, ) -> None: - # webhook_logs mongo_collection = MongoConnection.get_collection("webhook_logs") - + log_data = { "workspace_id": str(webhook.workspace_id), "webhook": str(webhook.id), @@ -123,7 +121,7 @@ def save_webhook_log( logger.info("Webhook log saved successfully to mongo") mongo_save_success = True except Exception as e: - log_exception(e) + log_exception(e, warning=True) logger.error(f"Failed to save webhook log: {e}") mongo_save_success = False @@ -134,7 +132,7 @@ def save_webhook_log( WebhookLog.objects.create(**log_data) logger.info("Webhook log saved successfully to database") except Exception as e: - log_exception(e) + log_exception(e, warning=True) logger.error(f"Failed to save webhook log: {e}") @@ -244,7 +242,7 @@ def send_webhook_deactivation_email(webhook_id: str, receiver_id: str, current_s msg.send() logger.info("Email sent successfully.") except Exception as e: - log_exception(e) + log_exception(e, warning=True) logger.error(f"Failed to send email: {e}") From 044003e7ec1334ab982c7355399c26e5df938678 Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Tue, 28 Oct 2025 16:56:39 +0530 Subject: [PATCH 025/212] [WEB-5211] fix: accessing NoneType current_instance for delete_module_issue_activity bgtask (#7993) * fix: accessing NoneType current_instance * fix: return module_name * fix: add none check --- apps/api/plane/api/views/module.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/api/plane/api/views/module.py b/apps/api/plane/api/views/module.py index d79b94084..7b3e8a104 100644 --- a/apps/api/plane/api/views/module.py +++ b/apps/api/plane/api/views/module.py @@ -871,6 +871,8 @@ class ModuleIssueDetailAPIEndpoint(BaseAPIView): module_id=module_id, issue_id=issue_id, ) + + module_name = module_issue.module.name if module_issue.module is not None else "" module_issue.delete() issue_activity.delay( type="module.activity.deleted", @@ -878,7 +880,7 @@ class ModuleIssueDetailAPIEndpoint(BaseAPIView): actor_id=str(request.user.id), issue_id=str(issue_id), project_id=str(project_id), - current_instance=None, + current_instance=json.dumps({"module_name": module_name}), epoch=int(timezone.now().timestamp()), ) return Response(status=status.HTTP_204_NO_CONTENT) From e09d98649755a08e2c30a5fbf7b3b2e67a660ad2 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:57:07 +0530 Subject: [PATCH 026/212] [WEB-5263] chore: removed the tracking of sanitized HTML (#8023) * chore: removed the html sanitized tracking * chore: added the greater than sign --- apps/api/plane/api/views/cycle.py | 2 +- apps/api/plane/utils/content_validator.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/api/plane/api/views/cycle.py b/apps/api/plane/api/views/cycle.py index 849dab340..c77106dd5 100644 --- a/apps/api/plane/api/views/cycle.py +++ b/apps/api/plane/api/views/cycle.py @@ -1221,7 +1221,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): pk=cycle_id, ) # transfer work items only when cycle is completed (passed the end data) - if old_cycle.end_date is not None and old_cycle.end_date < timezone.now(): + if old_cycle.end_date is not None and old_cycle.end_date > timezone.now(): return Response( {"error": "The old cycle is not completed yet"}, status=status.HTTP_400_BAD_REQUEST, diff --git a/apps/api/plane/utils/content_validator.py b/apps/api/plane/utils/content_validator.py index ff06a562f..caf740e54 100644 --- a/apps/api/plane/utils/content_validator.py +++ b/apps/api/plane/utils/content_validator.py @@ -237,10 +237,6 @@ def validate_html_content(html_content: str): except Exception: summary = str(diff) logger.warning(f"HTML sanitization removals: {summary}") - log_exception( - ValueError(f"HTML sanitization removals: {summary}"), - warning=True, - ) return True, None, clean_html except Exception as e: log_exception(e) From 69fe581fd8cbce5c15b904708adb4e69c27059a6 Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Tue, 28 Oct 2025 18:18:36 +0530 Subject: [PATCH 027/212] [WEB-5228] chore: IssueLink.DoesNotExist on crawl_work_item_link_title (#8003) * chore: wrap IssueLink logic in a try-except block * chore: user logger.warning * fix: add id and url on the error message --- apps/api/plane/bgtasks/work_item_link_task.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/api/plane/bgtasks/work_item_link_task.py b/apps/api/plane/bgtasks/work_item_link_task.py index 721231be1..7ceaacaf5 100644 --- a/apps/api/plane/bgtasks/work_item_link_task.py +++ b/apps/api/plane/bgtasks/work_item_link_task.py @@ -171,8 +171,12 @@ def fetch_and_encode_favicon( @shared_task def crawl_work_item_link_title(id: str, url: str) -> None: meta_data = crawl_work_item_link_title_and_favicon(url) - issue_link = IssueLink.objects.get(id=id) + + try: + issue_link = IssueLink.objects.get(id=id) + except IssueLink.DoesNotExist: + logger.warning(f"IssueLink not found for the id {id} and the url {url}") + return issue_link.metadata = meta_data - issue_link.save() From 1126ca30b0d9d29bdd7e9df4f5db1a073c7d0e84 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 28 Oct 2025 18:53:54 +0530 Subject: [PATCH 028/212] [WEB-5262] feat: gitea sso (#8022) * Feature/7137/gitea sso (#7940) * added gitea auth to admin panel with configs , added api calls * added gitea to oauth root (for signup and signin) * removed log * replace github oauth with gitea ouath error messages * added gitea to auth root * fix: update token expiration handling and remove unused variable in Gitea callback * fix: include Gitea in OAuth enabled checks * fix: improve error handling when fetching emails from Gitea * chore : remove logs and add semicolons * refactor: update Gitea authentication components and imports for consistency * fix: enhance Gitea authentication form to auto-populate host value and improve OAuth checks * refactor: enhance Gitea OAuth provider with improved error handling and URL validation * fix: update authentication success messages to check for string value "1" --------- Co-authored-by: Shivam Jain Co-authored-by: Prateek Shourya --- .../(dashboard)/authentication/gitea/form.tsx | 210 ++++++++++++++++++ .../authentication/gitea/layout.tsx | 10 + .../(dashboard)/authentication/gitea/page.tsx | 104 +++++++++ .../authentication/github/page.tsx | 2 +- .../authentication/gitlab/page.tsx | 2 +- .../authentication/google/page.tsx | 2 +- .../authentication/authentication-modes.tsx | 9 + .../authentication/gitea-config.tsx | 58 +++++ apps/admin/public/logos/gitea-logo.svg | 1 + .../api/plane/authentication/adapter/error.py | 2 + .../api/plane/authentication/adapter/oauth.py | 2 + .../authentication/provider/oauth/gitea.py | 171 ++++++++++++++ apps/api/plane/authentication/urls.py | 17 ++ .../plane/authentication/views/__init__.py | 3 + .../plane/authentication/views/app/gitea.py | 109 +++++++++ .../plane/authentication/views/space/gitea.py | 100 +++++++++ apps/api/plane/license/api/views/instance.py | 6 + .../management/commands/configure_instance.py | 68 +++++- .../account/auth-forms/auth-root.tsx | 17 +- apps/space/public/logos/gitea-logo.svg | 1 + apps/web/public/logos/gitea-logo.svg | 1 + packages/types/src/instance/auth.ts | 8 +- packages/types/src/instance/base.ts | 1 + 23 files changed, 897 insertions(+), 7 deletions(-) create mode 100644 apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx create mode 100644 apps/admin/app/(all)/(dashboard)/authentication/gitea/layout.tsx create mode 100644 apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx create mode 100644 apps/admin/core/components/authentication/gitea-config.tsx create mode 100644 apps/admin/public/logos/gitea-logo.svg create mode 100644 apps/api/plane/authentication/provider/oauth/gitea.py create mode 100644 apps/api/plane/authentication/views/app/gitea.py create mode 100644 apps/api/plane/authentication/views/space/gitea.py create mode 100644 apps/space/public/logos/gitea-logo.svg create mode 100644 apps/web/public/logos/gitea-logo.svg diff --git a/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx b/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx new file mode 100644 index 000000000..fefc1ac89 --- /dev/null +++ b/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx @@ -0,0 +1,210 @@ +"use client"; + +import type { FC } from "react"; +import { useState } from "react"; +import { isEmpty } from "lodash-es"; +import Link from "next/link"; +import { useForm } from "react-hook-form"; +// plane internal packages +import { API_BASE_URL } from "@plane/constants"; +import { TOAST_TYPE, setToast } from "@plane/propel/toast"; +import type { IFormattedInstanceConfiguration, TInstanceGiteaAuthenticationConfigurationKeys } from "@plane/types"; +import { Button, getButtonStyling } from "@plane/ui"; +import { cn } from "@plane/utils"; +// components +import { CodeBlock } from "@/components/common/code-block"; +import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal"; +import type { TControllerInputFormField } from "@/components/common/controller-input"; +import { ControllerInput } from "@/components/common/controller-input"; +import type { TCopyField } from "@/components/common/copy-field"; +import { CopyField } from "@/components/common/copy-field"; +// hooks +import { useInstance } from "@/hooks/store"; + +type Props = { + config: IFormattedInstanceConfiguration; +}; + +type GiteaConfigFormValues = Record; + +export const InstanceGiteaConfigForm: FC = (props) => { + const { config } = props; + // states + const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false); + // store hooks + const { updateInstanceConfigurations } = useInstance(); + // form data + const { + handleSubmit, + control, + reset, + formState: { errors, isDirty, isSubmitting }, + } = useForm({ + defaultValues: { + GITEA_HOST: config["GITEA_HOST"] || "https://gitea.com", + GITEA_CLIENT_ID: config["GITEA_CLIENT_ID"], + GITEA_CLIENT_SECRET: config["GITEA_CLIENT_SECRET"], + }, + }); + + const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : ""; + + const GITEA_FORM_FIELDS: TControllerInputFormField[] = [ + { + key: "GITEA_HOST", + type: "text", + label: "Gitea Host", + description: ( + <>Use the URL of your Gitea instance. For the official Gitea instance, use "https://gitea.com". + ), + placeholder: "https://gitea.com", + error: Boolean(errors.GITEA_HOST), + required: true, + }, + { + key: "GITEA_CLIENT_ID", + type: "text", + label: "Client ID", + description: ( + <> + You will get this from your{" "} + + Gitea OAuth application settings. + + + ), + placeholder: "70a44354520df8bd9bcd", + error: Boolean(errors.GITEA_CLIENT_ID), + required: true, + }, + { + key: "GITEA_CLIENT_SECRET", + type: "password", + label: "Client secret", + description: ( + <> + Your client secret is also found in your{" "} + + Gitea OAuth application settings. + + + ), + placeholder: "9b0050f94ec1b744e32ce79ea4ffacd40d4119cb", + error: Boolean(errors.GITEA_CLIENT_SECRET), + required: true, + }, + ]; + + const GITEA_SERVICE_FIELD: TCopyField[] = [ + { + key: "Callback_URI", + label: "Callback URI", + url: `${originURL}/auth/gitea/callback/`, + description: ( + <> + We will auto-generate this. Paste this into your Authorized Callback URI{" "} + field{" "} + + here. + + + ), + }, + ]; + + const onSubmit = async (formData: GiteaConfigFormValues) => { + const payload: Partial = { ...formData }; + + await updateInstanceConfigurations(payload) + .then((response = []) => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Done!", + message: "Your Gitea authentication is configured. You should test it now.", + }); + reset({ + GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value, + GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value, + GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value, + }); + }) + .catch((err) => console.error(err)); + }; + + const handleGoBack = (e: React.MouseEvent) => { + if (isDirty) { + e.preventDefault(); + setIsDiscardChangesModalOpen(true); + } + }; + + return ( + <> + setIsDiscardChangesModalOpen(false)} + /> +
+
+
+
Gitea-provided details for Plane
+ {GITEA_FORM_FIELDS.map((field) => ( + + ))} +
+
+ + + Go back + +
+
+
+
+
+
Plane-provided details for Gitea
+ {GITEA_SERVICE_FIELD.map((field) => ( + + ))} +
+
+
+
+ + ); +}; diff --git a/apps/admin/app/(all)/(dashboard)/authentication/gitea/layout.tsx b/apps/admin/app/(all)/(dashboard)/authentication/gitea/layout.tsx new file mode 100644 index 000000000..9526d13fb --- /dev/null +++ b/apps/admin/app/(all)/(dashboard)/authentication/gitea/layout.tsx @@ -0,0 +1,10 @@ +import type { ReactNode } from "react"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Gitea Authentication - God Mode", +}; + +export default function GiteaAuthenticationLayout({ children }: { children: ReactNode }) { + return <>{children}; +} diff --git a/apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx b/apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx new file mode 100644 index 000000000..74834638f --- /dev/null +++ b/apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { useState } from "react"; +import { observer } from "mobx-react"; +import Image from "next/image"; +import { useTheme } from "next-themes"; +import useSWR from "swr"; +// plane internal packages +import { setPromiseToast } from "@plane/propel/toast"; +import { Loader, ToggleSwitch } from "@plane/ui"; +// components +import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card"; +// hooks +import { useInstance } from "@/hooks/store"; +// icons +import giteaLogo from "@/public/logos/gitea-logo.svg"; +//local components +import { InstanceGiteaConfigForm } from "./form"; + +const InstanceGiteaAuthenticationPage = observer(() => { + // store + const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance(); + // state + const [isSubmitting, setIsSubmitting] = useState(false); + // theme + const { resolvedTheme } = useTheme(); + // config + const enableGiteaConfig = formattedConfig?.IS_GITEA_ENABLED ?? ""; + useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); + + const updateConfig = async (key: "IS_GITEA_ENABLED", value: string) => { + setIsSubmitting(true); + + const payload = { + [key]: value, + }; + + const updateConfigPromise = updateInstanceConfigurations(payload); + + setPromiseToast(updateConfigPromise, { + loading: "Saving Configuration...", + success: { + title: "Configuration saved", + message: () => `Gitea authentication is now ${value === "1" ? "active" : "disabled"}.`, + }, + error: { + title: "Error", + message: () => "Failed to save configuration", + }, + }); + + await updateConfigPromise + .then(() => { + setIsSubmitting(false); + }) + .catch((err) => { + console.error(err); + setIsSubmitting(false); + }); + }; + + const isGiteaEnabled = enableGiteaConfig === "1"; + + return ( + <> +
+
+ } + config={ + { + updateConfig("IS_GITEA_ENABLED", isGiteaEnabled ? "0" : "1"); + }} + size="sm" + disabled={isSubmitting || !formattedConfig} + /> + } + disabled={isSubmitting || !formattedConfig} + withBorder={false} + /> +
+
+ {formattedConfig ? ( + + ) : ( + + + + + + + + )} +
+
+ + ); +}); + +export default InstanceGiteaAuthenticationPage; diff --git a/apps/admin/app/(all)/(dashboard)/authentication/github/page.tsx b/apps/admin/app/(all)/(dashboard)/authentication/github/page.tsx index 5709ba4ba..4a1d19695 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/github/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/github/page.tsx @@ -44,7 +44,7 @@ const InstanceGithubAuthenticationPage = observer(() => { loading: "Saving Configuration...", success: { title: "Configuration saved", - message: () => `GitHub authentication is now ${value ? "active" : "disabled"}.`, + message: () => `GitHub authentication is now ${value === "1" ? "active" : "disabled"}.`, }, error: { title: "Error", diff --git a/apps/admin/app/(all)/(dashboard)/authentication/gitlab/page.tsx b/apps/admin/app/(all)/(dashboard)/authentication/gitlab/page.tsx index ae85168ae..907c35b8b 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/gitlab/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/gitlab/page.tsx @@ -38,7 +38,7 @@ const InstanceGitlabAuthenticationPage = observer(() => { loading: "Saving Configuration...", success: { title: "Configuration saved", - message: () => `GitLab authentication is now ${value ? "active" : "disabled"}.`, + message: () => `GitLab authentication is now ${value === "1" ? "active" : "disabled"}.`, }, error: { title: "Error", diff --git a/apps/admin/app/(all)/(dashboard)/authentication/google/page.tsx b/apps/admin/app/(all)/(dashboard)/authentication/google/page.tsx index d6ca370d4..f3c6c2808 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/google/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/google/page.tsx @@ -38,7 +38,7 @@ const InstanceGoogleAuthenticationPage = observer(() => { loading: "Saving Configuration...", success: { title: "Configuration saved", - message: () => `Google authentication is now ${value ? "active" : "disabled"}.`, + message: () => `Google authentication is now ${value === "1" ? "active" : "disabled"}.`, }, error: { title: "Error", diff --git a/apps/admin/ce/components/authentication/authentication-modes.tsx b/apps/admin/ce/components/authentication/authentication-modes.tsx index 386e0c05e..0936105f9 100644 --- a/apps/admin/ce/components/authentication/authentication-modes.tsx +++ b/apps/admin/ce/components/authentication/authentication-modes.tsx @@ -12,6 +12,7 @@ import { resolveGeneralTheme } from "@plane/utils"; // components import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card"; import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch"; +import { GiteaConfiguration } from "@/components/authentication/gitea-config"; import { GithubConfiguration } from "@/components/authentication/github-config"; import { GitlabConfiguration } from "@/components/authentication/gitlab-config"; import { GoogleConfiguration } from "@/components/authentication/google-config"; @@ -19,6 +20,7 @@ import { PasswordLoginConfiguration } from "@/components/authentication/password // plane admin components import { UpgradeButton } from "@/plane-admin/components/common"; // assets +import giteaLogo from "@/public/logos/gitea-logo.svg"; import githubLightModeImage from "@/public/logos/github-black.png"; import githubDarkModeImage from "@/public/logos/github-white.png"; import GitlabLogo from "@/public/logos/gitlab-logo.svg"; @@ -80,6 +82,13 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) => icon: GitLab Logo, config: , }, + { + key: "gitea", + name: "Gitea", + description: "Allow members to log in or sign up to plane with their Gitea accounts.", + icon: Gitea Logo, + config: , + }, { key: "oidc", name: "OIDC", diff --git a/apps/admin/core/components/authentication/gitea-config.tsx b/apps/admin/core/components/authentication/gitea-config.tsx new file mode 100644 index 000000000..8b0b9b374 --- /dev/null +++ b/apps/admin/core/components/authentication/gitea-config.tsx @@ -0,0 +1,58 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +// icons +import { Settings2 } from "lucide-react"; +// plane internal packages +import type { TInstanceAuthenticationMethodKeys } from "@plane/types"; +import { ToggleSwitch, getButtonStyling } from "@plane/ui"; +import { cn } from "@plane/utils"; +// hooks +import { useInstance } from "@/hooks/store"; + +type Props = { + disabled: boolean; + updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void; +}; + +export const GiteaConfiguration: React.FC = observer((props) => { + const { disabled, updateConfig } = props; + // store + const { formattedConfig } = useInstance(); + // derived values + const GiteaConfig = formattedConfig?.IS_GITEA_ENABLED ?? ""; + const GiteaConfigured = + !!formattedConfig?.GITEA_HOST && !!formattedConfig?.GITEA_CLIENT_ID && !!formattedConfig?.GITEA_CLIENT_SECRET; + + return ( + <> + {GiteaConfigured ? ( +
+ + Edit + + { + Boolean(parseInt(GiteaConfig)) === true + ? updateConfig("IS_GITEA_ENABLED", "0") + : updateConfig("IS_GITEA_ENABLED", "1"); + }} + size="sm" + disabled={disabled} + /> +
+ ) : ( + + + Configure + + )} + + ); +}); diff --git a/apps/admin/public/logos/gitea-logo.svg b/apps/admin/public/logos/gitea-logo.svg new file mode 100644 index 000000000..43291345d --- /dev/null +++ b/apps/admin/public/logos/gitea-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/api/plane/authentication/adapter/error.py b/apps/api/plane/authentication/adapter/error.py index c8622277e..25a7cf567 100644 --- a/apps/api/plane/authentication/adapter/error.py +++ b/apps/api/plane/authentication/adapter/error.py @@ -38,9 +38,11 @@ AUTHENTICATION_ERROR_CODES = { "GITHUB_NOT_CONFIGURED": 5110, "GITHUB_USER_NOT_IN_ORG": 5122, "GITLAB_NOT_CONFIGURED": 5111, + "GITEA_NOT_CONFIGURED": 5112, "GOOGLE_OAUTH_PROVIDER_ERROR": 5115, "GITHUB_OAUTH_PROVIDER_ERROR": 5120, "GITLAB_OAUTH_PROVIDER_ERROR": 5121, + "GITEA_OAUTH_PROVIDER_ERROR": 5123, # Reset Password "INVALID_PASSWORD_TOKEN": 5125, "EXPIRED_PASSWORD_TOKEN": 5130, diff --git a/apps/api/plane/authentication/adapter/oauth.py b/apps/api/plane/authentication/adapter/oauth.py index ed1201097..d8e423d0e 100644 --- a/apps/api/plane/authentication/adapter/oauth.py +++ b/apps/api/plane/authentication/adapter/oauth.py @@ -48,6 +48,8 @@ class OauthAdapter(Adapter): return "GITHUB_OAUTH_PROVIDER_ERROR" elif self.provider == "gitlab": return "GITLAB_OAUTH_PROVIDER_ERROR" + elif self.provider == "gitea": + return "GITEA_OAUTH_PROVIDER_ERROR" else: return "OAUTH_NOT_CONFIGURED" diff --git a/apps/api/plane/authentication/provider/oauth/gitea.py b/apps/api/plane/authentication/provider/oauth/gitea.py new file mode 100644 index 000000000..ba7d3d16b --- /dev/null +++ b/apps/api/plane/authentication/provider/oauth/gitea.py @@ -0,0 +1,171 @@ +import os +from datetime import datetime, timedelta +from urllib.parse import urlencode, urlparse +import pytz +import requests + +# Module imports +from plane.authentication.adapter.oauth import OauthAdapter +from plane.license.utils.instance_value import get_configuration_value +from plane.authentication.adapter.error import ( + AUTHENTICATION_ERROR_CODES, + AuthenticationException, +) + + +class GiteaOAuthProvider(OauthAdapter): + provider = "gitea" + scope = "openid email profile" + + def __init__(self, request, code=None, state=None, callback=None): + (GITEA_CLIENT_ID, GITEA_CLIENT_SECRET, GITEA_HOST) = get_configuration_value( + [ + { + "key": "GITEA_CLIENT_ID", + "default": os.environ.get("GITEA_CLIENT_ID"), + }, + { + "key": "GITEA_CLIENT_SECRET", + "default": os.environ.get("GITEA_CLIENT_SECRET"), + }, + { + "key": "GITEA_HOST", + "default": os.environ.get("GITEA_HOST"), + }, + ] + ) + + if not (GITEA_CLIENT_ID and GITEA_CLIENT_SECRET and GITEA_HOST): + raise AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["GITEA_NOT_CONFIGURED"], + error_message="GITEA_NOT_CONFIGURED", + ) + + # Enforce scheme and normalize trailing slash(es) + parsed = urlparse(GITEA_HOST) + if not parsed.scheme or parsed.scheme not in ("https", "http"): + raise AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["GITEA_NOT_CONFIGURED"], + error_message="GITEA_NOT_CONFIGURED", # avoid leaking details to query params + ) + GITEA_HOST = GITEA_HOST.rstrip("/") + + # Set URLs based on the host + self.token_url = f"{GITEA_HOST}/login/oauth/access_token" + self.userinfo_url = f"{GITEA_HOST}/api/v1/user" + + client_id = GITEA_CLIENT_ID + client_secret = GITEA_CLIENT_SECRET + + redirect_uri = f"{'https' if request.is_secure() else 'http'}://{request.get_host()}/auth/gitea/callback/" + url_params = { + "client_id": client_id, + "scope": self.scope, + "redirect_uri": redirect_uri, + "response_type": "code", + "state": state, + } + auth_url = f"{GITEA_HOST}/login/oauth/authorize?{urlencode(url_params)}" + + super().__init__( + request, + self.provider, + client_id, + self.scope, + redirect_uri, + auth_url, + self.token_url, + self.userinfo_url, + client_secret, + code, + callback=callback, + ) + + def set_token_data(self): + data = { + "code": self.code, + "client_id": self.client_id, + "client_secret": self.client_secret, + "redirect_uri": self.redirect_uri, + "grant_type": "authorization_code", + } + headers = {"Accept": "application/json"} + token_response = self.get_user_token(data=data, headers=headers) + super().set_token_data( + { + "access_token": token_response.get("access_token"), + "refresh_token": token_response.get("refresh_token", None), + "access_token_expired_at": ( + datetime.now(tz=pytz.utc) + timedelta(seconds=token_response.get("expires_in")) + if token_response.get("expires_in") + else None + ), + "refresh_token_expired_at": ( + datetime.fromtimestamp( + token_response.get("refresh_token_expired_at"), tz=pytz.utc + ) + if token_response.get("refresh_token_expired_at") + else None + ), + "id_token": token_response.get("id_token", ""), + } + ) + + def __get_email(self, headers): + try: + # Gitea may not provide email in user response, so fetch it separately + emails_url = f"{self.userinfo_url}/emails" + response = requests.get(emails_url, headers=headers) + if not response.ok: + raise AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"], + error_message="GITEA_OAUTH_PROVIDER_ERROR: Failed to fetch emails", + ) + emails_response = response.json() + + if not emails_response: + raise AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"], + error_message="GITEA_OAUTH_PROVIDER_ERROR: No emails found", + ) + # Prefer primary+verified, then any verified, then primary, else first + email = next((e.get("email") for e in emails_response if e.get("primary") and e.get("verified")), None) + if not email: + email = next((e.get("email") for e in emails_response if e.get("verified")), None) + if not email: + email = next((e.get("email") for e in emails_response if e.get("primary")), None) + if not email and emails_response: + # If no primary email, use the first one + email = emails_response[0].get("email") + return email + except requests.RequestException: + raise AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"], + error_message="GITEA_OAUTH_PROVIDER_ERROR: Exception occurred while fetching emails", + ) + + def set_user_data(self): + user_info_response = self.get_user_response() + headers = { + "Authorization": f"Bearer {self.token_data.get('access_token')}", + "Accept": "application/json", + } + + # Get email if not provided in user info + email = user_info_response.get("email") + if not email: + email = self.__get_email(headers=headers) + + super().set_user_data( + { + "email": email, + "user": { + "provider_id": str(user_info_response.get("id")), + "email": email, + "avatar": user_info_response.get("avatar_url"), + "first_name": user_info_response.get("full_name") or user_info_response.get("login"), + "last_name": "", # Gitea doesn't provide separate first/last name + "is_password_autoset": True, + }, + } + ) \ No newline at end of file diff --git a/apps/api/plane/authentication/urls.py b/apps/api/plane/authentication/urls.py index d8b5799de..64b8e654c 100644 --- a/apps/api/plane/authentication/urls.py +++ b/apps/api/plane/authentication/urls.py @@ -36,6 +36,10 @@ from .views import ( SignInAuthSpaceEndpoint, SignUpAuthSpaceEndpoint, SignOutAuthSpaceEndpoint, + GiteaCallbackEndpoint, + GiteaOauthInitiateEndpoint, + GiteaCallbackSpaceEndpoint, + GiteaOauthInitiateSpaceEndpoint, ) urlpatterns = [ @@ -129,4 +133,17 @@ urlpatterns = [ ), path("change-password/", ChangePasswordEndpoint.as_view(), name="forgot-password"), path("set-password/", SetUserPasswordEndpoint.as_view(), name="set-password"), + ## Gitea Oauth + path("gitea/", GiteaOauthInitiateEndpoint.as_view(), name="gitea-initiate"), + path("gitea/callback/", GiteaCallbackEndpoint.as_view(), name="gitea-callback"), + path( + "spaces/gitea/", + GiteaOauthInitiateSpaceEndpoint.as_view(), + name="space-gitea-initiate", + ), + path( + "spaces/gitea/callback/", + GiteaCallbackSpaceEndpoint.as_view(), + name="space-gitea-callback", + ), ] diff --git a/apps/api/plane/authentication/views/__init__.py b/apps/api/plane/authentication/views/__init__.py index 24ae1f673..2595d2e75 100644 --- a/apps/api/plane/authentication/views/__init__.py +++ b/apps/api/plane/authentication/views/__init__.py @@ -5,6 +5,7 @@ from .app.check import EmailCheckEndpoint from .app.email import SignInAuthEndpoint, SignUpAuthEndpoint from .app.github import GitHubCallbackEndpoint, GitHubOauthInitiateEndpoint from .app.gitlab import GitLabCallbackEndpoint, GitLabOauthInitiateEndpoint +from .app.gitea import GiteaCallbackEndpoint, GiteaOauthInitiateEndpoint from .app.google import GoogleCallbackEndpoint, GoogleOauthInitiateEndpoint from .app.magic import MagicGenerateEndpoint, MagicSignInEndpoint, MagicSignUpEndpoint @@ -17,6 +18,8 @@ from .space.github import GitHubCallbackSpaceEndpoint, GitHubOauthInitiateSpaceE from .space.gitlab import GitLabCallbackSpaceEndpoint, GitLabOauthInitiateSpaceEndpoint +from .space.gitea import GiteaCallbackSpaceEndpoint, GiteaOauthInitiateSpaceEndpoint + from .space.google import GoogleCallbackSpaceEndpoint, GoogleOauthInitiateSpaceEndpoint from .space.magic import ( diff --git a/apps/api/plane/authentication/views/app/gitea.py b/apps/api/plane/authentication/views/app/gitea.py new file mode 100644 index 000000000..fd12f8b33 --- /dev/null +++ b/apps/api/plane/authentication/views/app/gitea.py @@ -0,0 +1,109 @@ +import uuid +from urllib.parse import urlencode, urljoin + +# Django import +from django.http import HttpResponseRedirect +from django.views import View + +# Module imports +from plane.authentication.provider.oauth.gitea import GiteaOAuthProvider +from plane.authentication.utils.login import user_login +from plane.authentication.utils.redirection_path import get_redirection_path +from plane.authentication.utils.user_auth_workflow import post_user_auth_workflow +from plane.license.models import Instance +from plane.authentication.utils.host import base_host +from plane.authentication.adapter.error import ( + AuthenticationException, + AUTHENTICATION_ERROR_CODES, +) +from plane.utils.path_validator import validate_next_path + + +class GiteaOauthInitiateEndpoint(View): + def get(self, request): + # Get host and next path + request.session["host"] = base_host(request=request, is_app=True) + next_path = request.GET.get("next_path") + if next_path: + request.session["next_path"] = str(validate_next_path(next_path)) + + # Check instance configuration + instance = Instance.objects.first() + if instance is None or not instance.is_setup_done: + exc = AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["INSTANCE_NOT_CONFIGURED"], + error_message="INSTANCE_NOT_CONFIGURED", + ) + params = exc.get_error_dict() + if next_path: + params["next_path"] = str(validate_next_path(next_path)) + url = urljoin( + base_host(request=request, is_app=True), "?" + urlencode(params) + ) + return HttpResponseRedirect(url) + try: + state = uuid.uuid4().hex + provider = GiteaOAuthProvider(request=request, state=state) + request.session["state"] = state + auth_url = provider.get_auth_url() + return HttpResponseRedirect(auth_url) + except AuthenticationException as e: + params = e.get_error_dict() + if next_path: + params["next_path"] = str(validate_next_path(next_path)) + url = urljoin( + base_host(request=request, is_app=True), "?" + urlencode(params) + ) + return HttpResponseRedirect(url) + + +class GiteaCallbackEndpoint(View): + def get(self, request): + code = request.GET.get("code") + state = request.GET.get("state") + base_host = request.session.get("host") + next_path = request.session.get("next_path") + + if state != request.session.get("state", ""): + exc = AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"], + error_message="GITEA_OAUTH_PROVIDER_ERROR", + ) + params = exc.get_error_dict() + if next_path: + params["next_path"] = str(next_path) + url = urljoin(base_host, "?" + urlencode(params)) + return HttpResponseRedirect(url) + + if not code: + exc = AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"], + error_message="GITEA_OAUTH_PROVIDER_ERROR", + ) + params = exc.get_error_dict() + if next_path: + params["next_path"] = str(validate_next_path(next_path)) + url = urljoin(base_host, "?" + urlencode(params)) + return HttpResponseRedirect(url) + + try: + provider = GiteaOAuthProvider( + request=request, code=code, callback=post_user_auth_workflow + ) + user = provider.authenticate() + # Login the user and record his device info + user_login(request=request, user=user, is_app=True) + # Get the redirection path + if next_path: + path = str(validate_next_path(next_path)) + else: + path = get_redirection_path(user=user) + # redirect to referer path + url = urljoin(base_host, path) + return HttpResponseRedirect(url) + except AuthenticationException as e: + params = e.get_error_dict() + if next_path: + params["next_path"] = str(validate_next_path(next_path)) + url = urljoin(base_host, "?" + urlencode(params)) + return HttpResponseRedirect(url) diff --git a/apps/api/plane/authentication/views/space/gitea.py b/apps/api/plane/authentication/views/space/gitea.py new file mode 100644 index 000000000..497a1ecc0 --- /dev/null +++ b/apps/api/plane/authentication/views/space/gitea.py @@ -0,0 +1,100 @@ +# Python imports +import uuid +from urllib.parse import urlencode + +# Django import +from django.http import HttpResponseRedirect +from django.views import View + +# Module imports +from plane.authentication.provider.oauth.gitea import GiteaOAuthProvider +from plane.authentication.utils.login import user_login +from plane.license.models import Instance +from plane.authentication.utils.host import base_host +from plane.authentication.adapter.error import ( + AUTHENTICATION_ERROR_CODES, + AuthenticationException, +) +from plane.utils.path_validator import validate_next_path + + +class GiteaOauthInitiateSpaceEndpoint(View): + def get(self, request): + # Get host and next path + request.session["host"] = base_host(request=request, is_space=True) + next_path = request.GET.get("next_path") + if next_path: + request.session["next_path"] = str(validate_next_path(next_path)) + + # Check instance configuration + instance = Instance.objects.first() + if instance is None or not instance.is_setup_done: + exc = AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["INSTANCE_NOT_CONFIGURED"], + error_message="INSTANCE_NOT_CONFIGURED", + ) + params = exc.get_error_dict() + if next_path: + params["next_path"] = str(validate_next_path(next_path)) + url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}" + return HttpResponseRedirect(url) + + try: + state = uuid.uuid4().hex + provider = GiteaOAuthProvider(request=request, state=state) + request.session["state"] = state + auth_url = provider.get_auth_url() + return HttpResponseRedirect(auth_url) + except AuthenticationException as e: + params = e.get_error_dict() + if next_path: + params["next_path"] = str(next_path) + url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}" + return HttpResponseRedirect(url) + + +class GiteaCallbackSpaceEndpoint(View): + def get(self, request): + code = request.GET.get("code") + state = request.GET.get("state") + next_path = request.session.get("next_path") + + if state != request.session.get("state", ""): + exc = AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"], + error_message="GITEA_OAUTH_PROVIDER_ERROR", + ) + params = exc.get_error_dict() + if next_path: + params["next_path"] = str(validate_next_path(next_path)) + url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}" + return HttpResponseRedirect(url) + + if not code: + exc = AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"], + error_message="GITEA_OAUTH_PROVIDER_ERROR", + ) + params = exc.get_error_dict() + if next_path: + params["next_path"] = str(validate_next_path(next_path)) + url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}" + return HttpResponseRedirect(url) + + try: + provider = GiteaOAuthProvider(request=request, code=code) + user = provider.authenticate() + # Login the user and record his device info + user_login(request=request, user=user, is_space=True) + # Process workspace and project invitations + # redirect to referer path + url = ( + f"{base_host(request=request, is_space=True)}{str(validate_next_path(next_path)) if next_path else ''}" + ) + return HttpResponseRedirect(url) + except AuthenticationException as e: + params = e.get_error_dict() + if next_path: + params["next_path"] = str(validate_next_path(next_path)) + url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}" + return HttpResponseRedirect(url) diff --git a/apps/api/plane/license/api/views/instance.py b/apps/api/plane/license/api/views/instance.py index c598acfef..23eeebec1 100644 --- a/apps/api/plane/license/api/views/instance.py +++ b/apps/api/plane/license/api/views/instance.py @@ -50,6 +50,7 @@ class InstanceEndpoint(BaseAPIView): IS_GITHUB_ENABLED, GITHUB_APP_NAME, IS_GITLAB_ENABLED, + IS_GITEA_ENABLED, EMAIL_HOST, ENABLE_MAGIC_LINK_LOGIN, ENABLE_EMAIL_PASSWORD, @@ -86,6 +87,10 @@ class InstanceEndpoint(BaseAPIView): "key": "IS_GITLAB_ENABLED", "default": os.environ.get("IS_GITLAB_ENABLED", "0"), }, + { + "key": "IS_GITEA_ENABLED", + "default": os.environ.get("IS_GITEA_ENABLED", "0"), + }, {"key": "EMAIL_HOST", "default": os.environ.get("EMAIL_HOST", "")}, { "key": "ENABLE_MAGIC_LINK_LOGIN", @@ -134,6 +139,7 @@ class InstanceEndpoint(BaseAPIView): data["is_google_enabled"] = IS_GOOGLE_ENABLED == "1" data["is_github_enabled"] = IS_GITHUB_ENABLED == "1" data["is_gitlab_enabled"] = IS_GITLAB_ENABLED == "1" + data["is_gitea_enabled"] = IS_GITEA_ENABLED == "1" data["is_magic_login_enabled"] = ENABLE_MAGIC_LINK_LOGIN == "1" data["is_email_password_enabled"] = ENABLE_EMAIL_PASSWORD == "1" diff --git a/apps/api/plane/license/management/commands/configure_instance.py b/apps/api/plane/license/management/commands/configure_instance.py index 5611eec52..81c8fc89e 100644 --- a/apps/api/plane/license/management/commands/configure_instance.py +++ b/apps/api/plane/license/management/commands/configure_instance.py @@ -187,6 +187,30 @@ class Command(BaseCommand): "category": "INTERCOM", "is_encrypted": False, }, + { + "key": "IS_GITEA_ENABLED", + "value": os.environ.get("IS_GITEA_ENABLED", "0"), + "category": "GITEA", + "is_encrypted": False, + }, + { + "key": "GITEA_HOST", + "value": os.environ.get("GITEA_HOST"), + "category": "GITEA", + "is_encrypted": False, + }, + { + "key": "GITEA_CLIENT_ID", + "value": os.environ.get("GITEA_CLIENT_ID"), + "category": "GITEA", + "is_encrypted": False, + }, + { + "key": "GITEA_CLIENT_SECRET", + "value": os.environ.get("GITEA_CLIENT_SECRET"), + "category": "GITEA", + "is_encrypted": True, + }, ] for item in config_keys: @@ -203,7 +227,7 @@ class Command(BaseCommand): else: self.stdout.write(self.style.WARNING(f"{obj.key} configuration already exists")) - keys = ["IS_GOOGLE_ENABLED", "IS_GITHUB_ENABLED", "IS_GITLAB_ENABLED"] + keys = ["IS_GOOGLE_ENABLED", "IS_GITHUB_ENABLED", "IS_GITLAB_ENABLED", "IS_GITEA_ENABLED"] if not InstanceConfiguration.objects.filter(key__in=keys).exists(): for key in keys: if key == "IS_GOOGLE_ENABLED": @@ -282,6 +306,48 @@ class Command(BaseCommand): is_encrypted=False, ) self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable.")) + if key == "IS_GITEA_ENABLED": + GITEA_HOST, GITEA_CLIENT_ID, GITEA_CLIENT_SECRET = ( + get_configuration_value( + [ + { + "key": "GITEA_HOST", + "default": os.environ.get( + "GITEA_HOST", "" + ), + }, + { + "key": "GITEA_CLIENT_ID", + "default": os.environ.get("GITEA_CLIENT_ID", ""), + }, + { + "key": "GITEA_CLIENT_SECRET", + "default": os.environ.get( + "GITEA_CLIENT_SECRET", "" + ), + }, + ] + ) + ) + if ( + bool(GITEA_HOST) + and bool(GITEA_CLIENT_ID) + and bool(GITEA_CLIENT_SECRET) + ): + value = "1" + else: + value = "0" + InstanceConfiguration.objects.create( + key="IS_GITEA_ENABLED", + value=value, + category="AUTHENTICATION", + is_encrypted=False, + ) + self.stdout.write( + self.style.SUCCESS( + f"{key} loaded with value from environment variable." + ) + ) else: for key in keys: self.stdout.write(self.style.WARNING(f"{key} configuration already exists")) diff --git a/apps/space/core/components/account/auth-forms/auth-root.tsx b/apps/space/core/components/account/auth-forms/auth-root.tsx index 86452a3c6..5a54b1906 100644 --- a/apps/space/core/components/account/auth-forms/auth-root.tsx +++ b/apps/space/core/components/account/auth-forms/auth-root.tsx @@ -24,6 +24,7 @@ import GithubLightLogo from "/public/logos/github-black.png"; import GithubDarkLogo from "/public/logos/github-dark.svg"; import GitlabLogo from "/public/logos/gitlab-logo.svg"; import GoogleLogo from "/public/logos/google-logo.svg"; +import GiteaLogo from "/public/logos/gitea-logo.svg"; // local imports import { TermsAndConditions } from "../terms-and-conditions"; import { AuthBanner } from "./auth-banner"; @@ -92,7 +93,12 @@ export const AuthRoot: FC = observer(() => { const isMagicLoginEnabled = config?.is_magic_login_enabled || false; const isEmailPasswordEnabled = config?.is_email_password_enabled || false; const isOAuthEnabled = - (config && (config?.is_google_enabled || config?.is_github_enabled || config?.is_gitlab_enabled)) || false; + (config && + (config?.is_google_enabled || + config?.is_github_enabled || + config?.is_gitlab_enabled || + config?.is_gitea_enabled)) || + false; // submit handler- email verification const handleEmailVerification = async (data: IEmailCheckData) => { @@ -189,6 +195,15 @@ export const AuthRoot: FC = observer(() => { }, enabled: config?.is_gitlab_enabled, }, + { + id: "gitea", + text: `${content} with Gitea`, + icon: Gitea Logo, + onClick: () => { + window.location.assign(`${API_BASE_URL}/auth/gitea/${next_path ? `?next_path=${next_path}` : ``}`); + }, + enabled: config?.is_gitea_enabled, + }, ]; return ( diff --git a/apps/space/public/logos/gitea-logo.svg b/apps/space/public/logos/gitea-logo.svg new file mode 100644 index 000000000..43291345d --- /dev/null +++ b/apps/space/public/logos/gitea-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/logos/gitea-logo.svg b/apps/web/public/logos/gitea-logo.svg new file mode 100644 index 000000000..43291345d --- /dev/null +++ b/apps/web/public/logos/gitea-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/types/src/instance/auth.ts b/packages/types/src/instance/auth.ts index c3049bc45..c65f9ebb8 100644 --- a/packages/types/src/instance/auth.ts +++ b/packages/types/src/instance/auth.ts @@ -13,7 +13,8 @@ export type TInstanceAuthenticationMethodKeys = | "ENABLE_EMAIL_PASSWORD" | "IS_GOOGLE_ENABLED" | "IS_GITHUB_ENABLED" - | "IS_GITLAB_ENABLED"; + | "IS_GITLAB_ENABLED" + | "IS_GITEA_ENABLED"; export type TInstanceGoogleAuthenticationConfigurationKeys = "GOOGLE_CLIENT_ID" | "GOOGLE_CLIENT_SECRET"; @@ -27,10 +28,13 @@ export type TInstanceGitlabAuthenticationConfigurationKeys = | "GITLAB_CLIENT_ID" | "GITLAB_CLIENT_SECRET"; +export type TInstanceGiteaAuthenticationConfigurationKeys = "GITEA_HOST" | "GITEA_CLIENT_ID" | "GITEA_CLIENT_SECRET"; + export type TInstanceAuthenticationConfigurationKeys = | TInstanceGoogleAuthenticationConfigurationKeys | TInstanceGithubAuthenticationConfigurationKeys - | TInstanceGitlabAuthenticationConfigurationKeys; + | TInstanceGitlabAuthenticationConfigurationKeys + | TInstanceGiteaAuthenticationConfigurationKeys; export type TInstanceAuthenticationKeys = TInstanceAuthenticationMethodKeys | TInstanceAuthenticationConfigurationKeys; diff --git a/packages/types/src/instance/base.ts b/packages/types/src/instance/base.ts index 79b1e642f..8f3a0c648 100644 --- a/packages/types/src/instance/base.ts +++ b/packages/types/src/instance/base.ts @@ -42,6 +42,7 @@ export interface IInstanceConfig { is_google_enabled: boolean; is_github_enabled: boolean; is_gitlab_enabled: boolean; + is_gitea_enabled: boolean; is_magic_login_enabled: boolean; is_email_password_enabled: boolean; github_app_name: string | undefined; From 79537cd1df36649541d341983790ce3cd095e1a6 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Tue, 28 Oct 2025 19:52:23 +0530 Subject: [PATCH 029/212] chore: code splitting instance config variables (#8028) * chore: code splitting instance config variables * chore: typo fixes --- .../management/commands/configure_instance.py | 243 ++---------------- .../instance_config_variables/__init__.py | 4 + .../utils/instance_config_variables/core.py | 233 +++++++++++++++++ .../instance_config_variables/extended.py | 1 + 4 files changed, 257 insertions(+), 224 deletions(-) create mode 100644 apps/api/plane/utils/instance_config_variables/__init__.py create mode 100644 apps/api/plane/utils/instance_config_variables/core.py create mode 100644 apps/api/plane/utils/instance_config_variables/extended.py diff --git a/apps/api/plane/license/management/commands/configure_instance.py b/apps/api/plane/license/management/commands/configure_instance.py index 81c8fc89e..b3e84dd82 100644 --- a/apps/api/plane/license/management/commands/configure_instance.py +++ b/apps/api/plane/license/management/commands/configure_instance.py @@ -6,6 +6,7 @@ from django.core.management.base import BaseCommand, CommandError # Module imports from plane.license.models import InstanceConfiguration +from plane.utils.instance_config_variables import instance_config_variables class Command(BaseCommand): @@ -21,199 +22,7 @@ class Command(BaseCommand): if not os.environ.get(item): raise CommandError(f"{item} env variable is required.") - config_keys = [ - # Authentication Settings - { - "key": "ENABLE_SIGNUP", - "value": os.environ.get("ENABLE_SIGNUP", "1"), - "category": "AUTHENTICATION", - "is_encrypted": False, - }, - { - "key": "DISABLE_WORKSPACE_CREATION", - "value": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"), - "category": "WORKSPACE_MANAGEMENT", - "is_encrypted": False, - }, - { - "key": "ENABLE_EMAIL_PASSWORD", - "value": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"), - "category": "AUTHENTICATION", - "is_encrypted": False, - }, - { - "key": "ENABLE_MAGIC_LINK_LOGIN", - "value": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"), - "category": "AUTHENTICATION", - "is_encrypted": False, - }, - { - "key": "GOOGLE_CLIENT_ID", - "value": os.environ.get("GOOGLE_CLIENT_ID"), - "category": "GOOGLE", - "is_encrypted": False, - }, - { - "key": "GOOGLE_CLIENT_SECRET", - "value": os.environ.get("GOOGLE_CLIENT_SECRET"), - "category": "GOOGLE", - "is_encrypted": True, - }, - { - "key": "GITHUB_CLIENT_ID", - "value": os.environ.get("GITHUB_CLIENT_ID"), - "category": "GITHUB", - "is_encrypted": False, - }, - { - "key": "GITHUB_CLIENT_SECRET", - "value": os.environ.get("GITHUB_CLIENT_SECRET"), - "category": "GITHUB", - "is_encrypted": True, - }, - { - "key": "GITHUB_ORGANIZATION_ID", - "value": os.environ.get("GITHUB_ORGANIZATION_ID"), - "category": "GITHUB", - "is_encrypted": False, - }, - { - "key": "GITLAB_HOST", - "value": os.environ.get("GITLAB_HOST"), - "category": "GITLAB", - "is_encrypted": False, - }, - { - "key": "GITLAB_CLIENT_ID", - "value": os.environ.get("GITLAB_CLIENT_ID"), - "category": "GITLAB", - "is_encrypted": False, - }, - { - "key": "ENABLE_SMTP", - "value": os.environ.get("ENABLE_SMTP", "0"), - "category": "SMTP", - "is_encrypted": False, - }, - { - "key": "GITLAB_CLIENT_SECRET", - "value": os.environ.get("GITLAB_CLIENT_SECRET"), - "category": "GITLAB", - "is_encrypted": True, - }, - { - "key": "EMAIL_HOST", - "value": os.environ.get("EMAIL_HOST", ""), - "category": "SMTP", - "is_encrypted": False, - }, - { - "key": "EMAIL_HOST_USER", - "value": os.environ.get("EMAIL_HOST_USER", ""), - "category": "SMTP", - "is_encrypted": False, - }, - { - "key": "EMAIL_HOST_PASSWORD", - "value": os.environ.get("EMAIL_HOST_PASSWORD", ""), - "category": "SMTP", - "is_encrypted": True, - }, - { - "key": "EMAIL_PORT", - "value": os.environ.get("EMAIL_PORT", "587"), - "category": "SMTP", - "is_encrypted": False, - }, - { - "key": "EMAIL_FROM", - "value": os.environ.get("EMAIL_FROM", ""), - "category": "SMTP", - "is_encrypted": False, - }, - { - "key": "EMAIL_USE_TLS", - "value": os.environ.get("EMAIL_USE_TLS", "1"), - "category": "SMTP", - "is_encrypted": False, - }, - { - "key": "EMAIL_USE_SSL", - "value": os.environ.get("EMAIL_USE_SSL", "0"), - "category": "SMTP", - "is_encrypted": False, - }, - { - "key": "LLM_API_KEY", - "value": os.environ.get("LLM_API_KEY"), - "category": "AI", - "is_encrypted": True, - }, - { - "key": "LLM_PROVIDER", - "value": os.environ.get("LLM_PROVIDER", "openai"), - "category": "AI", - "is_encrypted": False, - }, - { - "key": "LLM_MODEL", - "value": os.environ.get("LLM_MODEL", "gpt-4o-mini"), - "category": "AI", - "is_encrypted": False, - }, - # Deprecated, use LLM_MODEL - { - "key": "GPT_ENGINE", - "value": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"), - "category": "SMTP", - "is_encrypted": False, - }, - { - "key": "UNSPLASH_ACCESS_KEY", - "value": os.environ.get("UNSPLASH_ACCESS_KEY", ""), - "category": "UNSPLASH", - "is_encrypted": True, - }, - # intercom settings - { - "key": "IS_INTERCOM_ENABLED", - "value": os.environ.get("IS_INTERCOM_ENABLED", "1"), - "category": "INTERCOM", - "is_encrypted": False, - }, - { - "key": "INTERCOM_APP_ID", - "value": os.environ.get("INTERCOM_APP_ID", ""), - "category": "INTERCOM", - "is_encrypted": False, - }, - { - "key": "IS_GITEA_ENABLED", - "value": os.environ.get("IS_GITEA_ENABLED", "0"), - "category": "GITEA", - "is_encrypted": False, - }, - { - "key": "GITEA_HOST", - "value": os.environ.get("GITEA_HOST"), - "category": "GITEA", - "is_encrypted": False, - }, - { - "key": "GITEA_CLIENT_ID", - "value": os.environ.get("GITEA_CLIENT_ID"), - "category": "GITEA", - "is_encrypted": False, - }, - { - "key": "GITEA_CLIENT_SECRET", - "value": os.environ.get("GITEA_CLIENT_SECRET"), - "category": "GITEA", - "is_encrypted": True, - }, - ] - - for item in config_keys: + for item in instance_config_variables: obj, created = InstanceConfiguration.objects.get_or_create(key=item.get("key")) if created: obj.category = item.get("category") @@ -307,33 +116,23 @@ class Command(BaseCommand): ) self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable.")) if key == "IS_GITEA_ENABLED": - GITEA_HOST, GITEA_CLIENT_ID, GITEA_CLIENT_SECRET = ( - get_configuration_value( - [ - { - "key": "GITEA_HOST", - "default": os.environ.get( - "GITEA_HOST", "" - ), - }, - { - "key": "GITEA_CLIENT_ID", - "default": os.environ.get("GITEA_CLIENT_ID", ""), - }, - { - "key": "GITEA_CLIENT_SECRET", - "default": os.environ.get( - "GITEA_CLIENT_SECRET", "" - ), - }, - ] - ) + GITEA_HOST, GITEA_CLIENT_ID, GITEA_CLIENT_SECRET = get_configuration_value( + [ + { + "key": "GITEA_HOST", + "default": os.environ.get("GITEA_HOST", ""), + }, + { + "key": "GITEA_CLIENT_ID", + "default": os.environ.get("GITEA_CLIENT_ID", ""), + }, + { + "key": "GITEA_CLIENT_SECRET", + "default": os.environ.get("GITEA_CLIENT_SECRET", ""), + }, + ] ) - if ( - bool(GITEA_HOST) - and bool(GITEA_CLIENT_ID) - and bool(GITEA_CLIENT_SECRET) - ): + if bool(GITEA_HOST) and bool(GITEA_CLIENT_ID) and bool(GITEA_CLIENT_SECRET): value = "1" else: value = "0" @@ -343,11 +142,7 @@ class Command(BaseCommand): category="AUTHENTICATION", is_encrypted=False, ) - self.stdout.write( - self.style.SUCCESS( - f"{key} loaded with value from environment variable." - ) - ) + self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable.")) else: for key in keys: self.stdout.write(self.style.WARNING(f"{key} configuration already exists")) diff --git a/apps/api/plane/utils/instance_config_variables/__init__.py b/apps/api/plane/utils/instance_config_variables/__init__.py new file mode 100644 index 000000000..6818ca9bf --- /dev/null +++ b/apps/api/plane/utils/instance_config_variables/__init__.py @@ -0,0 +1,4 @@ +from .core import core_config_variables +from .extended import extended_config_variables + +instance_config_variables = [*core_config_variables, *extended_config_variables] diff --git a/apps/api/plane/utils/instance_config_variables/core.py b/apps/api/plane/utils/instance_config_variables/core.py new file mode 100644 index 000000000..cf8d8d41f --- /dev/null +++ b/apps/api/plane/utils/instance_config_variables/core.py @@ -0,0 +1,233 @@ +# Python imports +import os + +authentication_config_variables = [ + { + "key": "ENABLE_SIGNUP", + "value": os.environ.get("ENABLE_SIGNUP", "1"), + "category": "AUTHENTICATION", + "is_encrypted": False, + }, + { + "key": "ENABLE_EMAIL_PASSWORD", + "value": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"), + "category": "AUTHENTICATION", + "is_encrypted": False, + }, + { + "key": "ENABLE_MAGIC_LINK_LOGIN", + "value": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"), + "category": "AUTHENTICATION", + "is_encrypted": False, + }, +] + +workspace_management_config_variables = [ + { + "key": "DISABLE_WORKSPACE_CREATION", + "value": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"), + "category": "WORKSPACE_MANAGEMENT", + "is_encrypted": False, + }, +] + +google_config_variables = [ + { + "key": "GOOGLE_CLIENT_ID", + "value": os.environ.get("GOOGLE_CLIENT_ID"), + "category": "GOOGLE", + "is_encrypted": False, + }, + { + "key": "GOOGLE_CLIENT_SECRET", + "value": os.environ.get("GOOGLE_CLIENT_SECRET"), + "category": "GOOGLE", + "is_encrypted": True, + }, +] + +github_config_variables = [ + { + "key": "GITHUB_CLIENT_ID", + "value": os.environ.get("GITHUB_CLIENT_ID"), + "category": "GITHUB", + "is_encrypted": False, + }, + { + "key": "GITHUB_CLIENT_SECRET", + "value": os.environ.get("GITHUB_CLIENT_SECRET"), + "category": "GITHUB", + "is_encrypted": True, + }, + { + "key": "GITHUB_ORGANIZATION_ID", + "value": os.environ.get("GITHUB_ORGANIZATION_ID"), + "category": "GITHUB", + "is_encrypted": False, + }, +] + + +gitlab_config_variables = [ + { + "key": "GITLAB_HOST", + "value": os.environ.get("GITLAB_HOST"), + "category": "GITLAB", + "is_encrypted": False, + }, + { + "key": "GITLAB_CLIENT_ID", + "value": os.environ.get("GITLAB_CLIENT_ID"), + "category": "GITLAB", + "is_encrypted": False, + }, + { + "key": "GITLAB_CLIENT_SECRET", + "value": os.environ.get("GITLAB_CLIENT_SECRET"), + "category": "GITLAB", + "is_encrypted": True, + }, +] + +gitea_config_variables = [ + { + "key": "IS_GITEA_ENABLED", + "value": os.environ.get("IS_GITEA_ENABLED", "0"), + "category": "GITEA", + "is_encrypted": False, + }, + { + "key": "GITEA_HOST", + "value": os.environ.get("GITEA_HOST"), + "category": "GITEA", + "is_encrypted": False, + }, + { + "key": "GITEA_CLIENT_ID", + "value": os.environ.get("GITEA_CLIENT_ID"), + "category": "GITEA", + "is_encrypted": False, + }, + { + "key": "GITEA_CLIENT_SECRET", + "value": os.environ.get("GITEA_CLIENT_SECRET"), + "category": "GITEA", + "is_encrypted": True, + }, +] + +smtp_config_variables = [ + { + "key": "ENABLE_SMTP", + "value": os.environ.get("ENABLE_SMTP", "0"), + "category": "SMTP", + "is_encrypted": False, + }, + { + "key": "EMAIL_HOST", + "value": os.environ.get("EMAIL_HOST", ""), + "category": "SMTP", + "is_encrypted": False, + }, + { + "key": "EMAIL_HOST_USER", + "value": os.environ.get("EMAIL_HOST_USER", ""), + "category": "SMTP", + "is_encrypted": False, + }, + { + "key": "EMAIL_HOST_PASSWORD", + "value": os.environ.get("EMAIL_HOST_PASSWORD", ""), + "category": "SMTP", + "is_encrypted": True, + }, + { + "key": "EMAIL_PORT", + "value": os.environ.get("EMAIL_PORT", "587"), + "category": "SMTP", + "is_encrypted": False, + }, + { + "key": "EMAIL_FROM", + "value": os.environ.get("EMAIL_FROM", ""), + "category": "SMTP", + "is_encrypted": False, + }, + { + "key": "EMAIL_USE_TLS", + "value": os.environ.get("EMAIL_USE_TLS", "1"), + "category": "SMTP", + "is_encrypted": False, + }, + { + "key": "EMAIL_USE_SSL", + "value": os.environ.get("EMAIL_USE_SSL", "0"), + "category": "SMTP", + "is_encrypted": False, + }, +] + +llm_config_variables = [ + { + "key": "LLM_API_KEY", + "value": os.environ.get("LLM_API_KEY"), + "category": "AI", + "is_encrypted": True, + }, + { + "key": "LLM_PROVIDER", + "value": os.environ.get("LLM_PROVIDER", "openai"), + "category": "AI", + "is_encrypted": False, + }, + { + "key": "LLM_MODEL", + "value": os.environ.get("LLM_MODEL", "gpt-4o-mini"), + "category": "AI", + "is_encrypted": False, + }, + # Deprecated, use LLM_MODEL + { + "key": "GPT_ENGINE", + "value": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"), + "category": "AI", + "is_encrypted": False, + }, +] + +unsplash_config_variables = [ + { + "key": "UNSPLASH_ACCESS_KEY", + "value": os.environ.get("UNSPLASH_ACCESS_KEY", ""), + "category": "UNSPLASH", + "is_encrypted": True, + }, +] + +intercom_config_variables = [ + { + "key": "IS_INTERCOM_ENABLED", + "value": os.environ.get("IS_INTERCOM_ENABLED", "1"), + "category": "INTERCOM", + "is_encrypted": False, + }, + { + "key": "INTERCOM_APP_ID", + "value": os.environ.get("INTERCOM_APP_ID", ""), + "category": "INTERCOM", + "is_encrypted": False, + }, +] + +core_config_variables = [ + *authentication_config_variables, + *workspace_management_config_variables, + *google_config_variables, + *github_config_variables, + *gitlab_config_variables, + *gitea_config_variables, + *smtp_config_variables, + *llm_config_variables, + *unsplash_config_variables, + *intercom_config_variables, +] diff --git a/apps/api/plane/utils/instance_config_variables/extended.py b/apps/api/plane/utils/instance_config_variables/extended.py new file mode 100644 index 000000000..24c6fefda --- /dev/null +++ b/apps/api/plane/utils/instance_config_variables/extended.py @@ -0,0 +1 @@ +extended_config_variables = [] From 0560849f88559fb41c98a9d6712a850fe220e4a4 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:14:07 +0530 Subject: [PATCH 030/212] [WEB-5271] fix: empty state translation error and code refactoring (#8027) * fix: workspace view empty state translation * chore: search empty state translation fix * chore: workspace view empty state translation --- .../empty-states/archived-issues.tsx | 2 +- .../issue-layouts/empty-states/cycle.tsx | 2 +- .../empty-states/global-view.tsx | 6 +++--- .../issue-layouts/empty-states/module.tsx | 2 +- .../roots/all-issue-layout-root.tsx | 19 ++++++++++--------- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx index 5563e6f90..0e97f9106 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -38,7 +38,7 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { description={t("common_empty_state.search.description")} actions={[ { - label: t("common.search.cta_secondary"), + label: "Clear filters", onClick: archivedWorkItemFilter?.clearFilters, disabled: !canPerformEmptyStateActions || !archivedWorkItemFilter, variant: "outline-primary", diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/cycle.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/cycle.tsx index 0f636b11a..2ed456f50 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/cycle.tsx @@ -93,7 +93,7 @@ export const CycleEmptyState: React.FC = observer(() => { description={t("common_empty_state.search.description")} actions={[ { - label: t("common_empty_state.search.cta_secondary"), + label: "Clear filters", onClick: cycleWorkItemFilter?.clearFilters, disabled: !canPerformEmptyStateActions || !cycleWorkItemFilter, variant: "outline-primary", diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/global-view.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/global-view.tsx index e8ded291e..0114128ea 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/global-view.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/global-view.tsx @@ -47,13 +47,13 @@ export const GlobalViewEmptyState: React.FC = observer(() => { return ( { captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.GLOBAL_VIEW }); toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/module.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/module.tsx index aa60aedc9..6c30b79be 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/module.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/module.tsx @@ -80,7 +80,7 @@ export const ModuleEmptyState: React.FC = observer(() => { description={t("common_empty_state.search.description")} actions={[ { - label: t("common_empty_state.search.cta_secondary"), + label: "Clear filters", onClick: moduleWorkItemFilter?.clearFilters, disabled: !canPerformEmptyStateActions || !moduleWorkItemFilter, variant: "outline-primary", diff --git a/apps/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/apps/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index ee58183a7..6c3daaaf7 100644 --- a/apps/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/apps/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -4,10 +4,10 @@ import { useParams, useSearchParams } from "next/navigation"; import useSWR from "swr"; // plane imports import { GLOBAL_VIEW_TRACKER_ELEMENTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; import type { EIssueLayoutTypes } from "@plane/types"; import { EIssuesStoreType, STATIC_VIEW_TYPES } from "@plane/types"; // components -import { EmptyState } from "@/components/common/empty-state"; import { IssuePeekOverview } from "@/components/issues/peek-overview"; import { WorkspaceActiveLayout } from "@/components/views/helper"; import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level"; @@ -18,8 +18,6 @@ import { useIssues } from "@/hooks/store/use-issues"; import { useAppRouter } from "@/hooks/use-app-router"; import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; -// public imports -import emptyView from "@/public/empty-state/view.svg"; type Props = { isDefaultView: boolean; @@ -109,14 +107,17 @@ export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { // Empty state if (!isLoading && !globalViewsLoading && !issuesLoading && !viewDetails && !isDefaultView) { return ( - router.push(`/${workspaceSlug}/workspace-views/all-issues`), - }} + assetKey="view" + actions={[ + { + label: "Go to All work items", + onClick: () => router.push(`/${workspaceSlug}/workspace-views/all-issues`), + variant: "primary", + }, + ]} /> ); } From 5247fedd23b3ad606c080583f23e1d529f5f32f1 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:34:04 +0530 Subject: [PATCH 031/212] [WEB-5271] chore: your work empty state updated #8029 --- .../issue-layouts/empty-states/profile-view.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/apps/web/core/components/issues/issue-layouts/empty-states/profile-view.tsx b/apps/web/core/components/issues/issue-layouts/empty-states/profile-view.tsx index 26f8e52ab..b714855de 100644 --- a/apps/web/core/components/issues/issue-layouts/empty-states/profile-view.tsx +++ b/apps/web/core/components/issues/issue-layouts/empty-states/profile-view.tsx @@ -2,9 +2,7 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // components import { useTranslation } from "@plane/i18n"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; -// constants -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { EmptyStateDetailed } from "@plane/propel/empty-state"; // TODO: If projectViewId changes, everything breaks. Figure out a better way to handle this. export const ProfileViewEmptyState: React.FC = observer(() => { @@ -12,19 +10,14 @@ export const ProfileViewEmptyState: React.FC = observer(() => { const { t } = useTranslation(); // store hooks const { profileViewId } = useParams(); - // derived values - const resolvedPath = useResolvedAssetPath({ - basePath: "/empty-state/profile/", - additionalPath: profileViewId?.toString(), - }); if (!profileViewId) return null; return ( - ); }); From 73e0e8d529eb9178cfb47743558938632c5177ad Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:49:34 +0530 Subject: [PATCH 032/212] [WEB-4944] feat: add base layouts for kanban and list with drag-and-drop support (#8032) --- .../core/components/base-layouts/constants.ts | 15 +++ .../hooks/use-group-drop-target.ts | 55 ++++++++++ .../base-layouts/hooks/use-layout-state.ts | 58 ++++++++++ .../base-layouts/kanban/group-header.tsx | 14 +++ .../components/base-layouts/kanban/group.tsx | 96 +++++++++++++++++ .../components/base-layouts/kanban/item.tsx | 40 +++++++ .../components/base-layouts/kanban/layout.tsx | 61 +++++++++++ .../base-layouts/layout-switcher.tsx | 50 +++++++++ .../base-layouts/list/group-header.tsx | 12 +++ .../components/base-layouts/list/group.tsx | 85 +++++++++++++++ .../components/base-layouts/list/item.tsx | 38 +++++++ .../components/base-layouts/list/layout.tsx | 61 +++++++++++ .../base-layouts/loaders/layout-loader.tsx | 24 +++++ packages/i18n/src/locales/cs/translations.ts | 2 + packages/i18n/src/locales/de/translations.ts | 2 + packages/i18n/src/locales/en/translations.ts | 2 + packages/i18n/src/locales/es/translations.ts | 2 + packages/i18n/src/locales/fr/translations.ts | 2 + packages/i18n/src/locales/id/translations.ts | 2 + packages/i18n/src/locales/it/translations.ts | 2 + packages/i18n/src/locales/ja/translations.ts | 2 + packages/i18n/src/locales/ko/translations.ts | 2 + packages/i18n/src/locales/pl/translations.ts | 2 + .../i18n/src/locales/pt-BR/translations.ts | 2 + packages/i18n/src/locales/ro/translations.ts | 2 + packages/i18n/src/locales/ru/translations.ts | 2 + packages/i18n/src/locales/sk/translations.ts | 2 + .../i18n/src/locales/tr-TR/translations.ts | 2 + packages/i18n/src/locales/ua/translations.ts | 2 + .../i18n/src/locales/vi-VN/translations.ts | 2 + .../i18n/src/locales/zh-CN/translations.ts | 2 + .../i18n/src/locales/zh-TW/translations.ts | 2 + packages/types/src/base-layouts/base.ts | 101 ++++++++++++++++++ packages/types/src/base-layouts/index.ts | 3 + packages/types/src/base-layouts/kanban.ts | 24 +++++ packages/types/src/base-layouts/list.ts | 20 ++++ packages/types/src/index.ts | 1 + 37 files changed, 796 insertions(+) create mode 100644 apps/web/core/components/base-layouts/constants.ts create mode 100644 apps/web/core/components/base-layouts/hooks/use-group-drop-target.ts create mode 100644 apps/web/core/components/base-layouts/hooks/use-layout-state.ts create mode 100644 apps/web/core/components/base-layouts/kanban/group-header.tsx create mode 100644 apps/web/core/components/base-layouts/kanban/group.tsx create mode 100644 apps/web/core/components/base-layouts/kanban/item.tsx create mode 100644 apps/web/core/components/base-layouts/kanban/layout.tsx create mode 100644 apps/web/core/components/base-layouts/layout-switcher.tsx create mode 100644 apps/web/core/components/base-layouts/list/group-header.tsx create mode 100644 apps/web/core/components/base-layouts/list/group.tsx create mode 100644 apps/web/core/components/base-layouts/list/item.tsx create mode 100644 apps/web/core/components/base-layouts/list/layout.tsx create mode 100644 apps/web/core/components/base-layouts/loaders/layout-loader.tsx create mode 100644 packages/types/src/base-layouts/base.ts create mode 100644 packages/types/src/base-layouts/index.ts create mode 100644 packages/types/src/base-layouts/kanban.ts create mode 100644 packages/types/src/base-layouts/list.ts diff --git a/apps/web/core/components/base-layouts/constants.ts b/apps/web/core/components/base-layouts/constants.ts new file mode 100644 index 000000000..60e45a372 --- /dev/null +++ b/apps/web/core/components/base-layouts/constants.ts @@ -0,0 +1,15 @@ +import { BoardLayoutIcon, ListLayoutIcon } from "@plane/propel/icons"; +import type { IBaseLayoutConfig } from "@plane/types"; + +export const BASE_LAYOUTS: IBaseLayoutConfig[] = [ + { + key: "list", + icon: ListLayoutIcon, + label: "List Layout", + }, + { + key: "kanban", + icon: BoardLayoutIcon, + label: "Board Layout", + }, +]; diff --git a/apps/web/core/components/base-layouts/hooks/use-group-drop-target.ts b/apps/web/core/components/base-layouts/hooks/use-group-drop-target.ts new file mode 100644 index 000000000..5fa232092 --- /dev/null +++ b/apps/web/core/components/base-layouts/hooks/use-group-drop-target.ts @@ -0,0 +1,55 @@ +import { useEffect, useRef, useState } from "react"; +import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; + +interface UseGroupDropTargetProps { + groupId: string; + enableDragDrop?: boolean; + onDrop?: (itemId: string, targetId: string | null, sourceGroupId: string, targetGroupId: string) => void; +} + +interface DragSourceData { + id: string; + groupId: string; + type: "ITEM" | "GROUP"; +} + +/** + * A hook that turns an element into a valid drop target for group drag-and-drop. + * + * @returns groupRef (attach to the droppable container) and isDraggingOver (for visual feedback) + */ +export const useGroupDropTarget = ({ groupId, enableDragDrop = false, onDrop }: UseGroupDropTargetProps) => { + const groupRef = useRef(null); + const [isDraggingOver, setIsDraggingOver] = useState(false); + + useEffect(() => { + const element = groupRef.current; + if (!element || !enableDragDrop || !onDrop) return; + + const cleanup = dropTargetForElements({ + element, + getData: () => ({ groupId, type: "GROUP" }), + + canDrop: ({ source }) => { + const data = (source?.data || {}) as Partial; + return data.type === "ITEM" && !!data.groupId && data.groupId !== groupId; + }, + + onDragEnter: () => setIsDraggingOver(true), + onDragLeave: () => setIsDraggingOver(false), + + onDrop: ({ source }) => { + setIsDraggingOver(false); + const data = (source?.data || {}) as Partial; + if (data.type !== "ITEM" || !data.id || !data.groupId) return; + if (data.groupId !== groupId) { + onDrop(data.id, null, data.groupId, groupId); + } + }, + }); + + return cleanup; + }, [groupId, enableDragDrop, onDrop]); + + return { groupRef, isDraggingOver }; +}; diff --git a/apps/web/core/components/base-layouts/hooks/use-layout-state.ts b/apps/web/core/components/base-layouts/hooks/use-layout-state.ts new file mode 100644 index 000000000..8d8bc75b2 --- /dev/null +++ b/apps/web/core/components/base-layouts/hooks/use-layout-state.ts @@ -0,0 +1,58 @@ +import { useEffect, useRef, useState, useCallback } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; + +type UseLayoutStateProps = + | { + mode: "external"; + externalCollapsedGroups: string[]; + externalOnToggleGroup: (groupId: string) => void; + enableAutoScroll?: boolean; + } + | { + mode?: "internal"; + enableAutoScroll?: boolean; + }; + +/** + * Hook for managing layout state including: + * - Collapsed/expanded group tracking (internal or external) + * - Auto-scroll setup for drag-and-drop + */ +export const useLayoutState = (props: UseLayoutStateProps = { mode: "internal" }) => { + const containerRef = useRef(null); + + // Internal fallback state + const [internalCollapsedGroups, setInternalCollapsedGroups] = useState([]); + + // Stable internal toggle function + const internalToggleGroup = useCallback((groupId: string) => { + setInternalCollapsedGroups((prev) => + prev.includes(groupId) ? prev.filter((id) => id !== groupId) : [...prev, groupId] + ); + }, []); + + const useExternal = props.mode === "external"; + const collapsedGroups = useExternal ? props.externalCollapsedGroups : internalCollapsedGroups; + const onToggleGroup = useExternal ? props.externalOnToggleGroup : internalToggleGroup; + + // Enable auto-scroll for DnD + useEffect(() => { + const element = containerRef.current; + if (!element || !props.enableAutoScroll) return; + + const cleanup = combine( + autoScrollForElements({ + element, + }) + ); + + return cleanup; + }, [props.enableAutoScroll]); + + return { + containerRef, + collapsedGroups, + onToggleGroup, + }; +}; diff --git a/apps/web/core/components/base-layouts/kanban/group-header.tsx b/apps/web/core/components/base-layouts/kanban/group-header.tsx new file mode 100644 index 000000000..be8487995 --- /dev/null +++ b/apps/web/core/components/base-layouts/kanban/group-header.tsx @@ -0,0 +1,14 @@ +import type { IGroupHeaderProps } from "@plane/types"; + +export const GroupHeader = ({ group, itemCount, onToggleGroup }: IGroupHeaderProps) => ( + +); diff --git a/apps/web/core/components/base-layouts/kanban/group.tsx b/apps/web/core/components/base-layouts/kanban/group.tsx new file mode 100644 index 000000000..2cd291897 --- /dev/null +++ b/apps/web/core/components/base-layouts/kanban/group.tsx @@ -0,0 +1,96 @@ +import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; +import type { IBaseLayoutsKanbanItem, IBaseLayoutsKanbanGroupProps } from "@plane/types"; +import { cn } from "@plane/utils"; +import { useGroupDropTarget } from "../hooks/use-group-drop-target"; +import { GroupHeader } from "./group-header"; +import { BaseKanbanItem } from "./item"; + +export const BaseKanbanGroup = observer((props: IBaseLayoutsKanbanGroupProps) => { + const { + group, + itemIds, + items, + renderItem, + renderGroupHeader, + isCollapsed, + onToggleGroup, + enableDragDrop = false, + onDrop, + canDrag, + groupClassName, + loadMoreItems: _loadMoreItems, + } = props; + + const { t } = useTranslation(); + const { groupRef, isDraggingOver } = useGroupDropTarget({ + groupId: group.id, + enableDragDrop, + onDrop, + }); + + return ( +
+ {/* Group Header */} +
+ {renderGroupHeader ? ( + renderGroupHeader({ group, itemCount: itemIds.length, isCollapsed, onToggleGroup }) + ) : ( + + )} +
+ + {/* Group Items */} + {!isCollapsed && ( +
+ {itemIds.map((itemId, index) => { + const item = items[itemId]; + if (!item) return null; + + return ( + + ); + })} + + {itemIds.length === 0 && ( +
+ {t("common.no_items_in_this_group")} +
+ )} +
+ )} + + {isDraggingOver && enableDragDrop && ( +
+
+ {t("common.drop_here_to_move")} +
+
+ )} +
+ ); +}); diff --git a/apps/web/core/components/base-layouts/kanban/item.tsx b/apps/web/core/components/base-layouts/kanban/item.tsx new file mode 100644 index 000000000..325e6f0d6 --- /dev/null +++ b/apps/web/core/components/base-layouts/kanban/item.tsx @@ -0,0 +1,40 @@ +import { useEffect, useRef } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { observer } from "mobx-react"; +import type { IBaseLayoutsKanbanItem, IBaseLayoutsKanbanItemProps } from "@plane/types"; + +export const BaseKanbanItem = observer((props: IBaseLayoutsKanbanItemProps) => { + const { item, groupId, renderItem, enableDragDrop, canDrag } = props; + + const itemRef = useRef(null); + + const isDragAllowed = canDrag ? canDrag(item) : true; + + // Setup draggable and drop target + useEffect(() => { + const element = itemRef.current; + if (!element || !enableDragDrop) return; + + return combine( + draggable({ + element, + canDrag: () => isDragAllowed, + getInitialData: () => ({ id: item.id, type: "ITEM", groupId }), + }), + dropTargetForElements({ + element, + getData: () => ({ id: item.id, groupId, type: "ITEM" }), + canDrop: ({ source }) => source?.data?.id !== item.id, + }) + ); + }, [enableDragDrop, isDragAllowed, item.id, groupId]); + + const renderedItem = renderItem(item, groupId); + + return ( +
+ {renderedItem} +
+ ); +}); diff --git a/apps/web/core/components/base-layouts/kanban/layout.tsx b/apps/web/core/components/base-layouts/kanban/layout.tsx new file mode 100644 index 000000000..447a6dcb4 --- /dev/null +++ b/apps/web/core/components/base-layouts/kanban/layout.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { observer } from "mobx-react"; +import type { IBaseLayoutsKanbanItem, IBaseLayoutsKanbanProps } from "@plane/types"; +import { cn } from "@plane/utils"; +import { useLayoutState } from "../hooks/use-layout-state"; +import { BaseKanbanGroup } from "./group"; + +export const BaseKanbanLayout = observer((props: IBaseLayoutsKanbanProps) => { + const { + items, + groups, + groupedItemIds, + renderItem, + renderGroupHeader, + onDrop, + canDrag, + className, + groupClassName, + showEmptyGroups = true, + enableDragDrop = false, + loadMoreItems, + collapsedGroups: externalCollapsedGroups = [], + onToggleGroup: externalOnToggleGroup = () => {}, + } = props; + + const { containerRef, collapsedGroups, onToggleGroup } = useLayoutState({ + mode: "external", + externalCollapsedGroups, + externalOnToggleGroup, + }); + + return ( +
+ {groups.map((group) => { + const itemIds = groupedItemIds[group.id] || []; + const isCollapsed = collapsedGroups.includes(group.id); + + if (!showEmptyGroups && itemIds.length === 0) return null; + + return ( + + ); + })} +
+ ); +}); diff --git a/apps/web/core/components/base-layouts/layout-switcher.tsx b/apps/web/core/components/base-layouts/layout-switcher.tsx new file mode 100644 index 000000000..5fe9c57cf --- /dev/null +++ b/apps/web/core/components/base-layouts/layout-switcher.tsx @@ -0,0 +1,50 @@ +"use client"; + +import React from "react"; +import { Tooltip } from "@plane/propel/tooltip"; +import type { TBaseLayoutType } from "@plane/types"; +import { usePlatformOS } from "@/hooks/use-platform-os"; +import { BASE_LAYOUTS } from "./constants"; + +type Props = { + layouts?: TBaseLayoutType[]; + onChange: (layout: TBaseLayoutType) => void; + selectedLayout: TBaseLayoutType | undefined; +}; + +export const LayoutSwitcher: React.FC = (props) => { + const { layouts, onChange, selectedLayout } = props; + const { isMobile } = usePlatformOS(); + + const handleOnChange = (layoutKey: TBaseLayoutType) => { + if (selectedLayout !== layoutKey) { + onChange(layoutKey); + } + }; + + return ( +
+ {BASE_LAYOUTS.filter((l) => (layouts ? layouts.includes(l.key) : true)).map((layout) => { + const Icon = layout.icon; + return ( + + + + ); + })} +
+ ); +}; diff --git a/apps/web/core/components/base-layouts/list/group-header.tsx b/apps/web/core/components/base-layouts/list/group-header.tsx new file mode 100644 index 000000000..cbdca7fad --- /dev/null +++ b/apps/web/core/components/base-layouts/list/group-header.tsx @@ -0,0 +1,12 @@ +import type { IGroupHeaderProps } from "@plane/types"; + +export const GroupHeader = ({ group, itemCount, onToggleGroup }: IGroupHeaderProps) => ( + +); diff --git a/apps/web/core/components/base-layouts/list/group.tsx b/apps/web/core/components/base-layouts/list/group.tsx new file mode 100644 index 000000000..15a8079aa --- /dev/null +++ b/apps/web/core/components/base-layouts/list/group.tsx @@ -0,0 +1,85 @@ +import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; +import type { IBaseLayoutsListItem, IBaseLayoutsListGroupProps } from "@plane/types"; +import { cn, Row } from "@plane/ui"; +import { useGroupDropTarget } from "../hooks/use-group-drop-target"; +import { GroupHeader } from "./group-header"; +import { BaseListItem } from "./item"; + +export const BaseListGroup = observer((props: IBaseLayoutsListGroupProps) => { + const { + group, + itemIds, + items, + isCollapsed, + onToggleGroup, + renderItem, + renderGroupHeader, + enableDragDrop = false, + onDrop, + canDrag, + loadMoreItems: _loadMoreItems, + } = props; + + const { t } = useTranslation(); + const { groupRef, isDraggingOver } = useGroupDropTarget({ + groupId: group.id, + enableDragDrop, + onDrop, + }); + + return ( +
+ {/* Group Header */} + + {renderGroupHeader ? ( + renderGroupHeader({ group, itemCount: itemIds.length, isCollapsed, onToggleGroup }) + ) : ( + + )} + + + {/* Group Items */} + {!isCollapsed && ( +
+ {itemIds.map((itemId: string, index: number) => { + const item = items[itemId]; + if (!item) return null; + + return ( + + ); + })} +
+ )} + + {isDraggingOver && enableDragDrop && ( +
+
+ {t("common.drop_here_to_move")} +
+
+ )} +
+ ); +}); diff --git a/apps/web/core/components/base-layouts/list/item.tsx b/apps/web/core/components/base-layouts/list/item.tsx new file mode 100644 index 000000000..cfaa74502 --- /dev/null +++ b/apps/web/core/components/base-layouts/list/item.tsx @@ -0,0 +1,38 @@ +import { useEffect, useRef } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { observer } from "mobx-react"; +import type { IBaseLayoutsListItem, IBaseLayoutsListItemProps } from "@plane/types"; + +export const BaseListItem = observer((props: IBaseLayoutsListItemProps) => { + const { item, groupId, renderItem, enableDragDrop, canDrag, isLast: _isLast, index: _index } = props; + const itemRef = useRef(null); + + const isDragAllowed = canDrag ? canDrag(item) : true; + + useEffect(() => { + const element = itemRef.current; + if (!element || !enableDragDrop) return; + + return combine( + draggable({ + element, + canDrag: () => isDragAllowed, + getInitialData: () => ({ id: item.id, type: "ITEM", groupId }), + }), + dropTargetForElements({ + element, + getData: () => ({ groupId, type: "ITEM" }), + canDrop: ({ source }) => source?.data?.id !== item.id, + }) + ); + }, [enableDragDrop, isDragAllowed, item.id, groupId]); + + const renderedItem = renderItem(item, groupId); + + return ( +
+ {renderedItem} +
+ ); +}); diff --git a/apps/web/core/components/base-layouts/list/layout.tsx b/apps/web/core/components/base-layouts/list/layout.tsx new file mode 100644 index 000000000..e423e16d5 --- /dev/null +++ b/apps/web/core/components/base-layouts/list/layout.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { observer } from "mobx-react"; +import type { IBaseLayoutsListItem, IBaseLayoutsListProps } from "@plane/types"; +import { cn } from "@plane/ui"; +import { useLayoutState } from "../hooks/use-layout-state"; +import { BaseListGroup } from "./group"; + +export const BaseListLayout = observer((props: IBaseLayoutsListProps) => { + const { + items, + groupedItemIds, + groups, + renderItem, + renderGroupHeader, + enableDragDrop = false, + onDrop, + canDrag, + showEmptyGroups = false, + collapsedGroups: externalCollapsedGroups = [], + onToggleGroup: externalOnToggleGroup = () => {}, + loadMoreItems, + className, + } = props; + + const { containerRef, collapsedGroups, onToggleGroup } = useLayoutState({ + mode: "external", + externalCollapsedGroups, + externalOnToggleGroup, + }); + + return ( +
+
+ {groups.map((group) => { + const itemIds = groupedItemIds[group.id] || []; + const isCollapsed = collapsedGroups.includes(group.id); + + if (!showEmptyGroups && itemIds.length === 0) return null; + + return ( + + ); + })} +
+
+ ); +}); diff --git a/apps/web/core/components/base-layouts/loaders/layout-loader.tsx b/apps/web/core/components/base-layouts/loaders/layout-loader.tsx new file mode 100644 index 000000000..d81d6718d --- /dev/null +++ b/apps/web/core/components/base-layouts/loaders/layout-loader.tsx @@ -0,0 +1,24 @@ +import type { TBaseLayoutType } from "@plane/types"; +import { KanbanLayoutLoader } from "@/components/ui/loader/layouts/kanban-layout-loader"; +import { ListLayoutLoader } from "@/components/ui/loader/layouts/list-layout-loader"; + +interface GenericLayoutLoaderProps { + layout: TBaseLayoutType; + /** Optional custom loaders to override defaults */ + customLoaders?: Partial>; +} + +export const GenericLayoutLoader = ({ layout, customLoaders }: GenericLayoutLoaderProps) => { + const CustomLoader = customLoaders?.[layout]; + if (CustomLoader) return ; + + switch (layout) { + case "list": + return ; + case "kanban": + return ; + default: + console.warn(`Unknown layout: ${layout}`); + return null; + } +}; diff --git a/packages/i18n/src/locales/cs/translations.ts b/packages/i18n/src/locales/cs/translations.ts index c59a5f9fa..3ac7803a7 100644 --- a/packages/i18n/src/locales/cs/translations.ts +++ b/packages/i18n/src/locales/cs/translations.ts @@ -639,6 +639,8 @@ export default { }, common: { all: "Vše", + no_items_in_this_group: "V této skupině nejsou žádné položky", + drop_here_to_move: "Přetáhněte sem pro přesunutí", states: "Stavy", state: "Stav", state_groups: "Skupiny stavů", diff --git a/packages/i18n/src/locales/de/translations.ts b/packages/i18n/src/locales/de/translations.ts index 681ae8f25..009233048 100644 --- a/packages/i18n/src/locales/de/translations.ts +++ b/packages/i18n/src/locales/de/translations.ts @@ -653,6 +653,8 @@ export default { }, common: { all: "Alle", + no_items_in_this_group: "Keine Elemente in dieser Gruppe", + drop_here_to_move: "Hier ablegen zum Verschieben", states: "Status", state: "Status", state_groups: "Statusgruppen", diff --git a/packages/i18n/src/locales/en/translations.ts b/packages/i18n/src/locales/en/translations.ts index 5c49e5e41..daa3d9e1f 100644 --- a/packages/i18n/src/locales/en/translations.ts +++ b/packages/i18n/src/locales/en/translations.ts @@ -478,6 +478,8 @@ export default { }, common: { all: "All", + no_items_in_this_group: "No items in this group", + drop_here_to_move: "Drop here to move", states: "States", state: "State", state_groups: "State groups", diff --git a/packages/i18n/src/locales/es/translations.ts b/packages/i18n/src/locales/es/translations.ts index 28390ed4f..05912437e 100644 --- a/packages/i18n/src/locales/es/translations.ts +++ b/packages/i18n/src/locales/es/translations.ts @@ -654,6 +654,8 @@ export default { }, common: { all: "Todo", + no_items_in_this_group: "No hay elementos en este grupo", + drop_here_to_move: "Suelta aquí para mover", states: "Estados", state: "Estado", state_groups: "Grupos de estados", diff --git a/packages/i18n/src/locales/fr/translations.ts b/packages/i18n/src/locales/fr/translations.ts index ded577c97..82cd79968 100644 --- a/packages/i18n/src/locales/fr/translations.ts +++ b/packages/i18n/src/locales/fr/translations.ts @@ -652,6 +652,8 @@ export default { }, common: { all: "Tout", + no_items_in_this_group: "Aucun élément dans ce groupe", + drop_here_to_move: "Déposer ici pour déplacer", states: "États", state: "État", state_groups: "Groupes d'états", diff --git a/packages/i18n/src/locales/id/translations.ts b/packages/i18n/src/locales/id/translations.ts index 50ae0e882..a856549c5 100644 --- a/packages/i18n/src/locales/id/translations.ts +++ b/packages/i18n/src/locales/id/translations.ts @@ -644,6 +644,8 @@ export default { }, common: { all: "Semua", + no_items_in_this_group: "Tidak ada item dalam grup ini", + drop_here_to_move: "Letakkan di sini untuk memindahkan", states: "Negara-negara", state: "Negara", state_groups: "Kelompok negara", diff --git a/packages/i18n/src/locales/it/translations.ts b/packages/i18n/src/locales/it/translations.ts index a8e043e7b..8036a770b 100644 --- a/packages/i18n/src/locales/it/translations.ts +++ b/packages/i18n/src/locales/it/translations.ts @@ -647,6 +647,8 @@ export default { }, common: { all: "Tutti", + no_items_in_this_group: "Nessun elemento in questo gruppo", + drop_here_to_move: "Rilascia qui per spostare", states: "Stati", state: "Stato", state_groups: "Gruppi di stati", diff --git a/packages/i18n/src/locales/ja/translations.ts b/packages/i18n/src/locales/ja/translations.ts index 85e1cf5b5..c074b0319 100644 --- a/packages/i18n/src/locales/ja/translations.ts +++ b/packages/i18n/src/locales/ja/translations.ts @@ -640,6 +640,8 @@ export default { }, common: { all: "すべて", + no_items_in_this_group: "このグループにアイテムはありません", + drop_here_to_move: "移動するにはここにドロップ", states: "ステータス", state: "ステータス", state_groups: "ステータスグループ", diff --git a/packages/i18n/src/locales/ko/translations.ts b/packages/i18n/src/locales/ko/translations.ts index dd377c5a0..2d854b6c3 100644 --- a/packages/i18n/src/locales/ko/translations.ts +++ b/packages/i18n/src/locales/ko/translations.ts @@ -634,6 +634,8 @@ export default { }, common: { all: "모두", + no_items_in_this_group: "이 그룹에 항목이 없습니다", + drop_here_to_move: "이동하려면 여기에 드롭하세요", states: "상태", state: "상태", state_groups: "상태 그룹", diff --git a/packages/i18n/src/locales/pl/translations.ts b/packages/i18n/src/locales/pl/translations.ts index c257acbe8..5d0e1a8b4 100644 --- a/packages/i18n/src/locales/pl/translations.ts +++ b/packages/i18n/src/locales/pl/translations.ts @@ -640,6 +640,8 @@ export default { }, common: { all: "Wszystko", + no_items_in_this_group: "Brak elementów w tej grupie", + drop_here_to_move: "Upuść tutaj, aby przenieść", states: "Stany", state: "Stan", state_groups: "Grupy stanów", diff --git a/packages/i18n/src/locales/pt-BR/translations.ts b/packages/i18n/src/locales/pt-BR/translations.ts index d6482cbf9..417660f59 100644 --- a/packages/i18n/src/locales/pt-BR/translations.ts +++ b/packages/i18n/src/locales/pt-BR/translations.ts @@ -651,6 +651,8 @@ export default { }, common: { all: "Todos", + no_items_in_this_group: "Nenhum item neste grupo", + drop_here_to_move: "Solte aqui para mover", states: "Estados", state: "Estado", state_groups: "Grupos de estado", diff --git a/packages/i18n/src/locales/ro/translations.ts b/packages/i18n/src/locales/ro/translations.ts index ff824443e..e4377cccd 100644 --- a/packages/i18n/src/locales/ro/translations.ts +++ b/packages/i18n/src/locales/ro/translations.ts @@ -646,6 +646,8 @@ export default { }, common: { all: "Toate", + no_items_in_this_group: "Nu există elemente în acest grup", + drop_here_to_move: "Eliberează aici pentru a muta", states: "Stări", state: "Stare", state_groups: "Grupuri de stări", diff --git a/packages/i18n/src/locales/ru/translations.ts b/packages/i18n/src/locales/ru/translations.ts index 26f35b1bf..9dc0c44fd 100644 --- a/packages/i18n/src/locales/ru/translations.ts +++ b/packages/i18n/src/locales/ru/translations.ts @@ -643,6 +643,8 @@ export default { }, common: { all: "Все", + no_items_in_this_group: "В этой группе нет элементов", + drop_here_to_move: "Перетащите сюда для перемещения", states: "Статусы", state: "Статус", state_groups: "Группы статусов", diff --git a/packages/i18n/src/locales/sk/translations.ts b/packages/i18n/src/locales/sk/translations.ts index fd30f4088..5191022e3 100644 --- a/packages/i18n/src/locales/sk/translations.ts +++ b/packages/i18n/src/locales/sk/translations.ts @@ -641,6 +641,8 @@ export default { }, common: { all: "Všetko", + no_items_in_this_group: "V tejto skupine nie sú žiadne položky", + drop_here_to_move: "Presuňte sem na presunutie", states: "Stavy", state: "Stav", state_groups: "Skupiny stavov", diff --git a/packages/i18n/src/locales/tr-TR/translations.ts b/packages/i18n/src/locales/tr-TR/translations.ts index 64eb23ecf..b094e65db 100644 --- a/packages/i18n/src/locales/tr-TR/translations.ts +++ b/packages/i18n/src/locales/tr-TR/translations.ts @@ -641,6 +641,8 @@ export default { }, common: { all: "Tümü", + no_items_in_this_group: "Bu grupta öğe yok", + drop_here_to_move: "Taşımak için buraya bırakın", states: "Durumlar", state: "Durum", state_groups: "Durum grupları", diff --git a/packages/i18n/src/locales/ua/translations.ts b/packages/i18n/src/locales/ua/translations.ts index 07c43d4fa..f0d0f474e 100644 --- a/packages/i18n/src/locales/ua/translations.ts +++ b/packages/i18n/src/locales/ua/translations.ts @@ -643,6 +643,8 @@ export default { }, common: { all: "Усе", + no_items_in_this_group: "У цій групі немає елементів", + drop_here_to_move: "Перетягніть сюди для переміщення", states: "Стани", state: "Стан", state_groups: "Групи станів", diff --git a/packages/i18n/src/locales/vi-VN/translations.ts b/packages/i18n/src/locales/vi-VN/translations.ts index e279c5103..70ca05148 100644 --- a/packages/i18n/src/locales/vi-VN/translations.ts +++ b/packages/i18n/src/locales/vi-VN/translations.ts @@ -648,6 +648,8 @@ export default { }, common: { all: "Tất cả", + no_items_in_this_group: "Không có mục nào trong nhóm này", + drop_here_to_move: "Thả vào đây để di chuyển", states: "Trạng thái", state: "Trạng thái", state_groups: "Nhóm trạng thái", diff --git a/packages/i18n/src/locales/zh-CN/translations.ts b/packages/i18n/src/locales/zh-CN/translations.ts index 8a0014fee..568379dbe 100644 --- a/packages/i18n/src/locales/zh-CN/translations.ts +++ b/packages/i18n/src/locales/zh-CN/translations.ts @@ -629,6 +629,8 @@ export default { }, common: { all: "全部", + no_items_in_this_group: "此组中没有项目", + drop_here_to_move: "拖放到此处以移动", states: "状态", state: "状态", state_groups: "状态组", diff --git a/packages/i18n/src/locales/zh-TW/translations.ts b/packages/i18n/src/locales/zh-TW/translations.ts index b3de9a0a8..eb754bf98 100644 --- a/packages/i18n/src/locales/zh-TW/translations.ts +++ b/packages/i18n/src/locales/zh-TW/translations.ts @@ -628,6 +628,8 @@ export default { }, common: { all: "全部", + no_items_in_this_group: "此群組中沒有項目", + drop_here_to_move: "拖放到此處以移動", states: "狀態", state: "狀態", state_groups: "狀態群組", diff --git a/packages/types/src/base-layouts/base.ts b/packages/types/src/base-layouts/base.ts new file mode 100644 index 000000000..9848ca425 --- /dev/null +++ b/packages/types/src/base-layouts/base.ts @@ -0,0 +1,101 @@ +import type { ReactNode } from "react"; + +// Base Types + +export interface IBaseLayoutsBaseItem { + id: string; + [key: string]: unknown; +} + +export interface IBaseLayoutsBaseGroup { + id: string; + name: string; + icon?: ReactNode; + payload?: Record; + count?: number; +} + +// Drag & Drop Types + +export interface IDragDropHandlers { + enableDragDrop?: boolean; + onDrop?: ( + sourceId: string, + destinationId: string | null, + sourceGroupId: string, + destinationGroupId: string + ) => Promise; + canDrag?: (item: T) => boolean; +} + +// Render Props + +export interface IItemRenderProps { + renderItem: (item: T, groupId: string) => ReactNode; +} + +export interface IGroupHeaderControls { + isCollapsed: boolean; + onToggleGroup: (groupId: string) => void; +} + +export interface IGroupHeaderProps extends IGroupHeaderControls { + group: IBaseLayoutsBaseGroup; + itemCount: number; +} + +export interface IGroupRenderProps { + renderGroupHeader?: (props: IGroupHeaderProps) => ReactNode; +} + +export interface IRenderProps extends IItemRenderProps, IGroupRenderProps {} + +// Layout Configuration + +export type TBaseLayoutType = "list" | "kanban"; + +export interface IBaseLayoutConfig { + key: TBaseLayoutType; + icon: React.ComponentType>; + label: string; +} + +// Base Layout Props +export interface IBaseLayoutsBaseProps extends IDragDropHandlers, IRenderProps { + items: Record; + groupedItemIds: Record; + groups: IBaseLayoutsBaseGroup[]; + + collapsedGroups?: string[]; + onToggleGroup?: (groupId: string) => void; + + isLoading?: boolean; + loadMoreItems?: (groupId: string) => void; + + showEmptyGroups?: boolean; + className?: string; +} + +// Group Props + +export interface IBaseLayoutsBaseGroupProps + extends IDragDropHandlers, + IRenderProps { + group: IBaseLayoutsBaseGroup; + itemIds: string[]; + items: Record; + isCollapsed: boolean; + onToggleGroup: (groupId: string) => void; + loadMoreItems?: (groupId: string) => void; +} + +// Item Props + +export interface IBaseLayoutsBaseItemProps + extends IDragDropHandlers, + IItemRenderProps { + item: T; + index: number; + groupId: string; + isLast: boolean; +} diff --git a/packages/types/src/base-layouts/index.ts b/packages/types/src/base-layouts/index.ts new file mode 100644 index 000000000..2545d81e2 --- /dev/null +++ b/packages/types/src/base-layouts/index.ts @@ -0,0 +1,3 @@ +export * from "./base"; +export * from "./list"; +export * from "./kanban"; diff --git a/packages/types/src/base-layouts/kanban.ts b/packages/types/src/base-layouts/kanban.ts new file mode 100644 index 000000000..318c36419 --- /dev/null +++ b/packages/types/src/base-layouts/kanban.ts @@ -0,0 +1,24 @@ +import type { + IBaseLayoutsBaseItem, + IBaseLayoutsBaseProps, + IBaseLayoutsBaseGroupProps, + IBaseLayoutsBaseItemProps, +} from "./base"; + +export type IBaseLayoutsKanbanItem = IBaseLayoutsBaseItem; + +// Main Kanban Layout Props + +export interface IBaseLayoutsKanbanProps extends IBaseLayoutsBaseProps { + groupClassName?: string; +} + +// Kanban Column/Group Props + +export interface IBaseLayoutsKanbanGroupProps extends IBaseLayoutsBaseGroupProps { + groupClassName?: string; +} + +// Kanban Card/Item Props + +export type IBaseLayoutsKanbanItemProps = IBaseLayoutsBaseItemProps; diff --git a/packages/types/src/base-layouts/list.ts b/packages/types/src/base-layouts/list.ts new file mode 100644 index 000000000..e0ac28275 --- /dev/null +++ b/packages/types/src/base-layouts/list.ts @@ -0,0 +1,20 @@ +import type { + IBaseLayoutsBaseItem, + IBaseLayoutsBaseProps, + IBaseLayoutsBaseGroupProps, + IBaseLayoutsBaseItemProps, +} from "./base"; + +export type IBaseLayoutsListItem = IBaseLayoutsBaseItem; + +// Main List Layout Props + +export type IBaseLayoutsListProps = IBaseLayoutsBaseProps; + +// Group component props + +export type IBaseLayoutsListGroupProps = IBaseLayoutsBaseGroupProps; + +// Item component props + +export type IBaseLayoutsListItemProps = IBaseLayoutsBaseItemProps; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ea6ee4080..c646506a1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -47,3 +47,4 @@ export * from "./workspace"; export * from "./workspace-draft-issues/base"; export * from "./workspace-notifications"; export * from "./workspace-views"; +export * from "./base-layouts"; From 350107d6c11f93d31974f17b8393fc6d6eacf9fa Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:38:30 +0530 Subject: [PATCH 033/212] [WEB-5088] feat: Power K `v2` (#7905) * feat: add project shortcut in command palette * feat: global project switcher shortcut * refactor: generalize command palette entity handling * feat: extend command palette navigation * feat: add issue shortcut to command palette * feat: add modular project selection for cycle navigation * chore: add reusable command palette utilities * fix: update key sequence handling to use window methods for timeout management * fix: build errors * chore: minor ux copy improvements * feat: implement a new command registry and renderer for enhanced command palette functionality * feat: introduce new command palette components and enhance search functionality * feat: enhance command palette components with improved initialization and loading indicators * feat: Implement new command palette architecture with multi-step commands, context-aware filtering, and reusable components. Add comprehensive documentation and integration guides. Enhance command execution with a dedicated executor and context provider. Introduce new command types and improve existing command definitions for better usability and maintainability. * refactor: hook arguments * refactor: folder structure * refactor: update import paths * fix: context prop drilling * refactor: update search components * refactor: create actions * chore: add type to pages * chore: init contextual actions * refactor: context based actions code split * chore: module context-based actions * refactor: streamline command execution flow and enhance multi-step handling in command palette * refactor: remove placeholder management from command execution and implement centralized placeholder mapping * chore: cycle context based actions * refactor: simplify command execution by consolidating selection steps and adding page change handling * chore: added more options to work item contextual actions * chore: page context actions * refactor: update step type definitions and enhance page mapping for command execution * feat: implement Command Palette V2 with global shortcuts and enhanced context handling * refactor: power k v2 * refactor: creation commands * feat: add navigation utility for Power K context handling * feat: implement comprehensive navigation commands for Power K * refactor: work item contextual actions * fix: build errors * refactor: remaining contextual actions * refactor: remove old code * chore: update placeholder * refactor: enhance command registry with observable properties and context-aware shortcut handling * refactor: improve command filtering logic in CommandPaletteModal * chore: context indicator * chore: misc actions * style: shortcut badge * feat: add open entity actions and enhance navigation commands for Power K * refactor: rename and reorganize Power K components for improved clarity and structure * refactor: update CommandPalette components and streamline global shortcuts handling * refactor: adjust debounce timing in CommandPaletteModal for improved responsiveness * feat: implement shortcuts modal and enhance command registry for better shortcut management * fix: search implemented * refactor: search results code split * refactor: search results code split * feat: introduce creation and navigation command modules for Power K, enhancing command organization and functionality * chore: update menu logos * refactor: remove unused PowerKOpenEntityActionsExtended component from command palette * refactor: search menu * fix: clear context on backspace and manual clear * refactor: rename creation command keys for consistency and clarity in Power K * chore: added intake in global search * chore: preferences menu * chore: removed the empty serach params * revert: command palette changes * cleanup * refactor: update command IDs to use underscores for consistency across Power K components * refactor: extended context based actions * chore: modal command item status props * refactor: replace CommandPalette with CommandPaletteProvider in settings and profile layouts * refactor: update settings menu to use translated labels instead of i18n labels * refactor: update command titles to use translation keys for creation actions * refactor: update navigation command titles to use translation keys for consistency * chore: minor cleanup * chore: misc commands added * chore: code split for no search results command * chore: state menu items for work item context based commands * chore: add more props to no search results command * chore: add more props to no search results command * refactor: remove shortcut key for create workspace command * Refactor command palette to use PowerK store - Replaced instances of `useCommandPalette` with `usePowerK` across various components, including `AppSearch`, `CommandModal`, and `CommandPalette`. - Introduced `PowerKStore` to manage modal states and commands, enhancing the command palette functionality. - Updated modal handling to toggle `PowerKModal` and `ShortcutsListModal` instead of the previous command palette modals. - Refactored related components to ensure compatibility with the new store structure and maintain functionality. * Refactor PowerK command handling to remove context dependency - Updated `usePowerKCommands` and `usePowerKCreationCommands` to eliminate the need for a context parameter, simplifying their usage. - Adjusted related command records to utilize the new structure, ensuring consistent access to command configurations. - Enhanced permission checks in creation commands to utilize user project roles for better access control. * chore: add context indicator * chore: update type import * chore: migrate toast implementation from @plane/ui to @plane/propel/toast across multiple command files * refactor: power k modal wrapper and provider * fix: type imports * chore: update creation command shortcuts * fix: page context commands * chore: update navigation and open command shortcuts * fix: work item standalone page modals * fix: context indicator visibility * fix: potential error points * fix: build errors * fix: lint errors * fix: import order --------- Co-authored-by: Vihar Kurama Co-authored-by: Prateek Shourya Co-authored-by: NarayanBavisetti --- apps/api/plane/app/views/search/base.py | 211 +++++-- .../[workspaceSlug]/(projects)/layout.tsx | 33 +- .../[workspaceSlug]/(settings)/layout.tsx | 4 +- .../settings/(workspace)/sidebar.tsx | 4 +- apps/web/app/(all)/layout.tsx | 2 +- apps/web/app/(all)/profile/layout.tsx | 5 +- .../ce/components/command-palette/helpers.tsx | 2 +- .../ce/components/command-palette/index.ts | 1 - .../command-palette/modals/index.ts | 3 - .../{issue-level.tsx => work-item-level.tsx} | 33 +- .../command-palette/power-k/constants.ts | 6 + .../power-k/context-detector.ts | 5 + .../hooks/use-extended-context-indicator.ts | 8 + .../power-k/pages/context-based/index.ts | 1 + .../power-k/pages/context-based/root.tsx | 11 + .../work-item/state-menu-item.tsx | 34 ++ .../power-k/search/no-results-command.tsx | 36 ++ .../power-k/search/search-results-map.tsx | 10 + .../command-palette/power-k/types.ts | 5 + .../workspace/sidebar/app-search.tsx | 9 +- apps/web/ce/store/power-k.store.ts | 13 + .../command-palette/actions/help-actions.tsx | 85 --- .../command-palette/actions/index.ts | 6 - .../actions/issue-actions/actions-list.tsx | 164 ------ .../actions/issue-actions/change-assignee.tsx | 98 ---- .../actions/issue-actions/change-priority.tsx | 57 -- .../actions/issue-actions/change-state.tsx | 46 -- .../actions/issue-actions/index.ts | 4 - .../actions/project-actions.tsx | 94 ---- .../actions/search-results.tsx | 66 --- .../command-palette/actions/theme-actions.tsx | 65 --- .../actions/workspace-settings-actions.tsx | 60 -- .../command-palette/command-modal.tsx | 492 ----------------- .../command-palette/command-palette.tsx | 270 --------- .../core/components/command-palette/index.ts | 4 - .../shortcuts-modal/commands-list.tsx | 99 ---- .../command-palette/shortcuts-modal/index.ts | 2 - .../helper.tsx => power-k/actions/helper.ts} | 0 .../power-k/config/account-commands.ts | 58 ++ .../components/power-k/config/commands.ts | 29 + .../power-k/config/creation/command.ts | 151 +++++ .../power-k/config/creation/root.ts | 18 + .../power-k/config/help-commands.ts | 82 +++ .../power-k/config/miscellaneous-commands.ts | 62 +++ .../power-k/config/navigation/commands.ts | 519 ++++++++++++++++++ .../power-k/config/navigation/root.ts | 45 ++ .../power-k/config/preferences-commands.ts | 153 ++++++ .../power-k/core/context-detector.ts | 18 + .../core/components/power-k/core/registry.ts | 160 ++++++ .../power-k/core/shortcut-handler.ts | 211 +++++++ .../web/core/components/power-k/core/types.ts | 138 +++++ .../components/power-k/global-shortcuts.tsx | 62 +++ .../power-k/hooks/use-context-indicator.ts | 58 ++ .../core/components/power-k/menus/builder.tsx | 51 ++ .../core/components/power-k/menus/cycles.tsx | 28 + .../components/power-k/menus/empty-state.tsx | 9 + .../core/components/power-k/menus/labels.tsx | 31 ++ .../core/components/power-k/menus/members.tsx | 50 ++ .../core/components/power-k/menus/modules.tsx | 28 + .../components/power-k/menus/projects.tsx | 30 + .../components/power-k/menus/settings.tsx | 30 + .../core/components/power-k/menus/views.tsx | 26 + .../components/power-k/menus/workspaces.tsx | 26 + .../power-k/projects-app-provider.tsx | 91 +++ .../ui/modal/command-item-shortcut-badge.tsx | 109 ++++ .../power-k/ui/modal/command-item.tsx | 42 ++ .../power-k/ui/modal/commands-list.tsx | 49 ++ .../components/power-k/ui/modal/constants.ts | 72 +++ .../power-k/ui/modal/context-indicator.tsx | 46 ++ .../components/power-k/ui/modal/footer.tsx | 34 ++ .../components/power-k/ui/modal/header.tsx | 60 ++ .../power-k/ui/modal/search-menu.tsx | 107 ++++ .../power-k/ui/modal/search-results-map.tsx | 113 ++++ .../power-k/ui/modal/search-results.tsx | 74 +++ .../ui/modal/shortcuts-root.tsx} | 15 +- .../components/power-k/ui/modal/wrapper.tsx | 186 +++++++ .../ui/pages/context-based/cycle/commands.ts | 94 ++++ .../power-k/ui/pages/context-based/index.ts | 31 ++ .../pages/context-based/module/commands.tsx | 160 ++++++ .../ui/pages/context-based/module/index.ts | 1 + .../ui/pages/context-based/module/root.tsx | 50 ++ .../context-based/module/status-menu.tsx | 36 ++ .../ui/pages/context-based/page/commands.ts | 183 ++++++ .../power-k/ui/pages/context-based/root.tsx | 46 ++ .../pages/context-based/work-item/commands.ts | 453 +++++++++++++++ .../context-based/work-item/cycles-menu.tsx | 29 + .../work-item/estimates-menu.tsx | 69 +++ .../ui/pages/context-based/work-item/index.ts | 1 + .../context-based/work-item/labels-menu.tsx | 29 + .../context-based/work-item/modules-menu.tsx | 31 ++ .../work-item/priorities-menu.tsx | 33 ++ .../ui/pages/context-based/work-item/root.tsx | 80 +++ .../context-based/work-item/states-menu.tsx | 43 ++ .../components/power-k/ui/pages/default.tsx | 23 + .../core/components/power-k/ui/pages/index.ts | 1 + .../pages/open-entity/project-cycles-menu.tsx | 33 ++ .../open-entity/project-modules-menu.tsx | 33 ++ .../open-entity/project-settings-menu.tsx | 44 ++ .../pages/open-entity/project-views-menu.tsx | 30 + .../ui/pages/open-entity/projects-menu.tsx | 28 + .../power-k/ui/pages/open-entity/root.tsx | 35 ++ .../power-k/ui/pages/open-entity/shared.ts | 8 + .../open-entity/workspace-settings-menu.tsx | 40 ++ .../ui/pages/open-entity/workspaces-menu.tsx | 26 + .../power-k/ui/pages/preferences/index.ts | 1 + .../ui/pages/preferences/languages-menu.tsx | 25 + .../power-k/ui/pages/preferences/root.tsx | 29 + .../pages/preferences/start-of-week-menu.tsx | 25 + .../ui/pages/preferences/themes-menu.tsx | 37 ++ .../ui/pages/preferences/timezone-menu.tsx | 31 ++ .../core/components/power-k/ui/pages/root.tsx | 32 ++ .../ui/pages/work-item-selection-page.tsx | 163 ++++++ .../power-k/ui/renderer/command.tsx | 71 +++ .../components/power-k/ui/renderer/shared.ts | 25 + .../power-k/ui/renderer/shortcut.tsx | 109 ++++ .../components/power-k/utils/navigation.ts | 20 + .../workspace/sidebar/help-menu.tsx | 6 +- .../workspace/sidebar/help-section.tsx | 6 +- .../workspace/sidebar/help-section/root.tsx | 6 +- apps/web/core/hooks/store/use-power-k.ts | 11 + .../core/store/base-command-palette.store.ts | 40 +- apps/web/core/store/base-power-k.store.ts | 93 ++++ apps/web/core/store/root.store.ts | 5 + .../command-palette/modals/index.ts | 1 - .../{command-pallette.css => power-k.css} | 22 +- packages/i18n/src/locales/en/translations.ts | 181 ++++++ 126 files changed, 5944 insertions(+), 1784 deletions(-) delete mode 100644 apps/web/ce/components/command-palette/modals/index.ts rename apps/web/ce/components/command-palette/modals/{issue-level.tsx => work-item-level.tsx} (75%) create mode 100644 apps/web/ce/components/command-palette/power-k/constants.ts create mode 100644 apps/web/ce/components/command-palette/power-k/context-detector.ts create mode 100644 apps/web/ce/components/command-palette/power-k/hooks/use-extended-context-indicator.ts create mode 100644 apps/web/ce/components/command-palette/power-k/pages/context-based/index.ts create mode 100644 apps/web/ce/components/command-palette/power-k/pages/context-based/root.tsx create mode 100644 apps/web/ce/components/command-palette/power-k/pages/context-based/work-item/state-menu-item.tsx create mode 100644 apps/web/ce/components/command-palette/power-k/search/no-results-command.tsx create mode 100644 apps/web/ce/components/command-palette/power-k/search/search-results-map.tsx create mode 100644 apps/web/ce/components/command-palette/power-k/types.ts create mode 100644 apps/web/ce/store/power-k.store.ts delete mode 100644 apps/web/core/components/command-palette/actions/help-actions.tsx delete mode 100644 apps/web/core/components/command-palette/actions/index.ts delete mode 100644 apps/web/core/components/command-palette/actions/issue-actions/actions-list.tsx delete mode 100644 apps/web/core/components/command-palette/actions/issue-actions/change-assignee.tsx delete mode 100644 apps/web/core/components/command-palette/actions/issue-actions/change-priority.tsx delete mode 100644 apps/web/core/components/command-palette/actions/issue-actions/change-state.tsx delete mode 100644 apps/web/core/components/command-palette/actions/issue-actions/index.ts delete mode 100644 apps/web/core/components/command-palette/actions/project-actions.tsx delete mode 100644 apps/web/core/components/command-palette/actions/search-results.tsx delete mode 100644 apps/web/core/components/command-palette/actions/theme-actions.tsx delete mode 100644 apps/web/core/components/command-palette/actions/workspace-settings-actions.tsx delete mode 100644 apps/web/core/components/command-palette/command-modal.tsx delete mode 100644 apps/web/core/components/command-palette/command-palette.tsx delete mode 100644 apps/web/core/components/command-palette/index.ts delete mode 100644 apps/web/core/components/command-palette/shortcuts-modal/commands-list.tsx delete mode 100644 apps/web/core/components/command-palette/shortcuts-modal/index.ts rename apps/web/core/components/{command-palette/actions/helper.tsx => power-k/actions/helper.ts} (100%) create mode 100644 apps/web/core/components/power-k/config/account-commands.ts create mode 100644 apps/web/core/components/power-k/config/commands.ts create mode 100644 apps/web/core/components/power-k/config/creation/command.ts create mode 100644 apps/web/core/components/power-k/config/creation/root.ts create mode 100644 apps/web/core/components/power-k/config/help-commands.ts create mode 100644 apps/web/core/components/power-k/config/miscellaneous-commands.ts create mode 100644 apps/web/core/components/power-k/config/navigation/commands.ts create mode 100644 apps/web/core/components/power-k/config/navigation/root.ts create mode 100644 apps/web/core/components/power-k/config/preferences-commands.ts create mode 100644 apps/web/core/components/power-k/core/context-detector.ts create mode 100644 apps/web/core/components/power-k/core/registry.ts create mode 100644 apps/web/core/components/power-k/core/shortcut-handler.ts create mode 100644 apps/web/core/components/power-k/core/types.ts create mode 100644 apps/web/core/components/power-k/global-shortcuts.tsx create mode 100644 apps/web/core/components/power-k/hooks/use-context-indicator.ts create mode 100644 apps/web/core/components/power-k/menus/builder.tsx create mode 100644 apps/web/core/components/power-k/menus/cycles.tsx create mode 100644 apps/web/core/components/power-k/menus/empty-state.tsx create mode 100644 apps/web/core/components/power-k/menus/labels.tsx create mode 100644 apps/web/core/components/power-k/menus/members.tsx create mode 100644 apps/web/core/components/power-k/menus/modules.tsx create mode 100644 apps/web/core/components/power-k/menus/projects.tsx create mode 100644 apps/web/core/components/power-k/menus/settings.tsx create mode 100644 apps/web/core/components/power-k/menus/views.tsx create mode 100644 apps/web/core/components/power-k/menus/workspaces.tsx create mode 100644 apps/web/core/components/power-k/projects-app-provider.tsx create mode 100644 apps/web/core/components/power-k/ui/modal/command-item-shortcut-badge.tsx create mode 100644 apps/web/core/components/power-k/ui/modal/command-item.tsx create mode 100644 apps/web/core/components/power-k/ui/modal/commands-list.tsx create mode 100644 apps/web/core/components/power-k/ui/modal/constants.ts create mode 100644 apps/web/core/components/power-k/ui/modal/context-indicator.tsx create mode 100644 apps/web/core/components/power-k/ui/modal/footer.tsx create mode 100644 apps/web/core/components/power-k/ui/modal/header.tsx create mode 100644 apps/web/core/components/power-k/ui/modal/search-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/modal/search-results-map.tsx create mode 100644 apps/web/core/components/power-k/ui/modal/search-results.tsx rename apps/web/core/components/{command-palette/shortcuts-modal/modal.tsx => power-k/ui/modal/shortcuts-root.tsx} (88%) create mode 100644 apps/web/core/components/power-k/ui/modal/wrapper.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/cycle/commands.ts create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/index.ts create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/module/commands.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/module/index.ts create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/module/root.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/module/status-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/page/commands.ts create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/root.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/work-item/commands.ts create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/work-item/cycles-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/work-item/estimates-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/work-item/index.ts create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/work-item/labels-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/work-item/modules-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/work-item/priorities-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/work-item/root.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/context-based/work-item/states-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/default.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/index.ts create mode 100644 apps/web/core/components/power-k/ui/pages/open-entity/project-cycles-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/open-entity/project-modules-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/open-entity/project-settings-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/open-entity/project-views-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/open-entity/projects-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/open-entity/root.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/open-entity/shared.ts create mode 100644 apps/web/core/components/power-k/ui/pages/open-entity/workspace-settings-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/open-entity/workspaces-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/preferences/index.ts create mode 100644 apps/web/core/components/power-k/ui/pages/preferences/languages-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/preferences/root.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/preferences/start-of-week-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/preferences/themes-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/preferences/timezone-menu.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/root.tsx create mode 100644 apps/web/core/components/power-k/ui/pages/work-item-selection-page.tsx create mode 100644 apps/web/core/components/power-k/ui/renderer/command.tsx create mode 100644 apps/web/core/components/power-k/ui/renderer/shared.ts create mode 100644 apps/web/core/components/power-k/ui/renderer/shortcut.tsx create mode 100644 apps/web/core/components/power-k/utils/navigation.ts create mode 100644 apps/web/core/hooks/store/use-power-k.ts create mode 100644 apps/web/core/store/base-power-k.store.ts delete mode 100644 apps/web/ee/components/command-palette/modals/index.ts rename apps/web/styles/{command-pallette.css => power-k.css} (69%) diff --git a/apps/api/plane/app/views/search/base.py b/apps/api/plane/app/views/search/base.py index 3942b0a44..5309bff55 100644 --- a/apps/api/plane/app/views/search/base.py +++ b/apps/api/plane/app/views/search/base.py @@ -43,22 +43,25 @@ class GlobalSearchEndpoint(BaseAPIView): also show related workspace if found """ - def filter_workspaces(self, query, slug, project_id, workspace_search): + def filter_workspaces(self, query, _slug, _project_id, _workspace_search): fields = ["name"] q = Q() - for field in fields: - q |= Q(**{f"{field}__icontains": query}) + if query: + for field in fields: + q |= Q(**{f"{field}__icontains": query}) return ( Workspace.objects.filter(q, workspace_member__member=self.request.user) + .order_by("-created_at") .distinct() .values("name", "id", "slug") ) - def filter_projects(self, query, slug, project_id, workspace_search): + def filter_projects(self, query, slug, _project_id, _workspace_search): fields = ["name", "identifier"] q = Q() - for field in fields: - q |= Q(**{f"{field}__icontains": query}) + if query: + for field in fields: + q |= Q(**{f"{field}__icontains": query}) return ( Project.objects.filter( q, @@ -67,6 +70,7 @@ class GlobalSearchEndpoint(BaseAPIView): archived_at__isnull=True, workspace__slug=slug, ) + .order_by("-created_at") .distinct() .values("name", "id", "identifier", "workspace__slug") ) @@ -74,14 +78,15 @@ class GlobalSearchEndpoint(BaseAPIView): def filter_issues(self, query, slug, project_id, workspace_search): fields = ["name", "sequence_id", "project__identifier"] q = Q() - for field in fields: - if field == "sequence_id": - # Match whole integers only (exclude decimal numbers) - sequences = re.findall(r"\b\d+\b", query) - for sequence_id in sequences: - q |= Q(**{"sequence_id": sequence_id}) - else: - q |= Q(**{f"{field}__icontains": query}) + if query: + for field in fields: + if field == "sequence_id": + # Match whole integers only (exclude decimal numbers) + sequences = re.findall(r"\b\d+\b", query) + for sequence_id in sequences: + q |= Q(**{"sequence_id": sequence_id}) + else: + q |= Q(**{f"{field}__icontains": query}) issues = Issue.issue_objects.filter( q, @@ -106,8 +111,9 @@ class GlobalSearchEndpoint(BaseAPIView): def filter_cycles(self, query, slug, project_id, workspace_search): fields = ["name"] q = Q() - for field in fields: - q |= Q(**{f"{field}__icontains": query}) + if query: + for field in fields: + q |= Q(**{f"{field}__icontains": query}) cycles = Cycle.objects.filter( q, @@ -120,13 +126,20 @@ class GlobalSearchEndpoint(BaseAPIView): if workspace_search == "false" and project_id: cycles = cycles.filter(project_id=project_id) - return cycles.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug") + return ( + cycles.order_by("-created_at") + .distinct() + .values( + "name", "id", "project_id", "project__identifier", "workspace__slug" + ) + ) def filter_modules(self, query, slug, project_id, workspace_search): fields = ["name"] q = Q() - for field in fields: - q |= Q(**{f"{field}__icontains": query}) + if query: + for field in fields: + q |= Q(**{f"{field}__icontains": query}) modules = Module.objects.filter( q, @@ -139,13 +152,20 @@ class GlobalSearchEndpoint(BaseAPIView): if workspace_search == "false" and project_id: modules = modules.filter(project_id=project_id) - return modules.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug") + return ( + modules.order_by("-created_at") + .distinct() + .values( + "name", "id", "project_id", "project__identifier", "workspace__slug" + ) + ) def filter_pages(self, query, slug, project_id, workspace_search): fields = ["name"] q = Q() - for field in fields: - q |= Q(**{f"{field}__icontains": query}) + if query: + for field in fields: + q |= Q(**{f"{field}__icontains": query}) pages = ( Page.objects.filter( @@ -157,7 +177,9 @@ class GlobalSearchEndpoint(BaseAPIView): ) .annotate( project_ids=Coalesce( - ArrayAgg("projects__id", distinct=True, filter=~Q(projects__id=True)), + ArrayAgg( + "projects__id", distinct=True, filter=~Q(projects__id=True) + ), Value([], output_field=ArrayField(UUIDField())), ) ) @@ -174,19 +196,28 @@ class GlobalSearchEndpoint(BaseAPIView): ) if workspace_search == "false" and project_id: - project_subquery = ProjectPage.objects.filter(page_id=OuterRef("id"), project_id=project_id).values_list( - "project_id", flat=True - )[:1] + project_subquery = ProjectPage.objects.filter( + page_id=OuterRef("id"), project_id=project_id + ).values_list("project_id", flat=True)[:1] - pages = pages.annotate(project_id=Subquery(project_subquery)).filter(project_id=project_id) + pages = pages.annotate(project_id=Subquery(project_subquery)).filter( + project_id=project_id + ) - return pages.distinct().values("name", "id", "project_ids", "project_identifiers", "workspace__slug") + return ( + pages.order_by("-created_at") + .distinct() + .values( + "name", "id", "project_ids", "project_identifiers", "workspace__slug" + ) + ) def filter_views(self, query, slug, project_id, workspace_search): fields = ["name"] q = Q() - for field in fields: - q |= Q(**{f"{field}__icontains": query}) + if query: + for field in fields: + q |= Q(**{f"{field}__icontains": query}) issue_views = IssueView.objects.filter( q, @@ -199,29 +230,57 @@ class GlobalSearchEndpoint(BaseAPIView): if workspace_search == "false" and project_id: issue_views = issue_views.filter(project_id=project_id) - return issue_views.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug") + return ( + issue_views.order_by("-created_at") + .distinct() + .values( + "name", "id", "project_id", "project__identifier", "workspace__slug" + ) + ) + + def filter_intakes(self, query, slug, project_id, workspace_search): + fields = ["name", "sequence_id", "project__identifier"] + q = Q() + if query: + for field in fields: + if field == "sequence_id": + # Match whole integers only (exclude decimal numbers) + sequences = re.findall(r"\b\d+\b", query) + for sequence_id in sequences: + q |= Q(**{"sequence_id": sequence_id}) + else: + q |= Q(**{f"{field}__icontains": query}) + + issues = Issue.objects.filter( + q, + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + project__archived_at__isnull=True, + workspace__slug=slug, + ).filter(models.Q(issue_intake__status=0) | models.Q(issue_intake__status=-2)) + + if workspace_search == "false" and project_id: + issues = issues.filter(project_id=project_id) + + return ( + issues.order_by("-created_at") + .distinct() + .values( + "name", + "id", + "sequence_id", + "project__identifier", + "project_id", + "workspace__slug", + )[:100] + ) def get(self, request, slug): query = request.query_params.get("search", False) + entities_param = request.query_params.get("entities") workspace_search = request.query_params.get("workspace_search", "false") project_id = request.query_params.get("project_id", False) - if not query: - return Response( - { - "results": { - "workspace": [], - "project": [], - "issue": [], - "cycle": [], - "module": [], - "issue_view": [], - "page": [], - } - }, - status=status.HTTP_200_OK, - ) - MODELS_MAPPER = { "workspace": self.filter_workspaces, "project": self.filter_projects, @@ -230,13 +289,27 @@ class GlobalSearchEndpoint(BaseAPIView): "module": self.filter_modules, "issue_view": self.filter_views, "page": self.filter_pages, + "intake": self.filter_intakes, } + # Determine which entities to search + if entities_param: + requested_entities = [ + e.strip() for e in entities_param.split(",") if e.strip() + ] + requested_entities = [e for e in requested_entities if e in MODELS_MAPPER] + else: + requested_entities = list(MODELS_MAPPER.keys()) + results = {} - for model in MODELS_MAPPER.keys(): - func = MODELS_MAPPER.get(model, None) - results[model] = func(query, slug, project_id, workspace_search) + for entity in requested_entities: + func = MODELS_MAPPER.get(entity) + if func: + results[entity] = func( + query or None, slug, project_id, workspace_search + ) + return Response({"results": results}, status=status.HTTP_200_OK) @@ -316,12 +389,15 @@ class SearchEndpoint(BaseAPIView): projects = ( Project.objects.filter( q, - Q(project_projectmember__member=self.request.user) | Q(network=2), + Q(project_projectmember__member=self.request.user) + | Q(network=2), workspace__slug=slug, ) .order_by("-created_at") .distinct() - .values("name", "id", "identifier", "logo_props", "workspace__slug")[:count] + .values( + "name", "id", "identifier", "logo_props", "workspace__slug" + )[:count] ) response_data["project"] = list(projects) @@ -380,16 +456,20 @@ class SearchEndpoint(BaseAPIView): .annotate( status=Case( When( - Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()), + Q(start_date__lte=timezone.now()) + & Q(end_date__gte=timezone.now()), then=Value("CURRENT"), ), When( start_date__gt=timezone.now(), then=Value("UPCOMING"), ), - When(end_date__lt=timezone.now(), then=Value("COMPLETED")), When( - Q(start_date__isnull=True) & Q(end_date__isnull=True), + end_date__lt=timezone.now(), then=Value("COMPLETED") + ), + When( + Q(start_date__isnull=True) + & Q(end_date__isnull=True), then=Value("DRAFT"), ), default=Value("DRAFT"), @@ -507,7 +587,9 @@ class SearchEndpoint(BaseAPIView): ) ) .order_by("-created_at") - .values("member__avatar_url", "member__display_name", "member__id")[:count] + .values( + "member__avatar_url", "member__display_name", "member__id" + )[:count] ) response_data["user_mention"] = list(users) @@ -521,12 +603,15 @@ class SearchEndpoint(BaseAPIView): projects = ( Project.objects.filter( q, - Q(project_projectmember__member=self.request.user) | Q(network=2), + Q(project_projectmember__member=self.request.user) + | Q(network=2), workspace__slug=slug, ) .order_by("-created_at") .distinct() - .values("name", "id", "identifier", "logo_props", "workspace__slug")[:count] + .values( + "name", "id", "identifier", "logo_props", "workspace__slug" + )[:count] ) response_data["project"] = list(projects) @@ -583,16 +668,20 @@ class SearchEndpoint(BaseAPIView): .annotate( status=Case( When( - Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()), + Q(start_date__lte=timezone.now()) + & Q(end_date__gte=timezone.now()), then=Value("CURRENT"), ), When( start_date__gt=timezone.now(), then=Value("UPCOMING"), ), - When(end_date__lt=timezone.now(), then=Value("COMPLETED")), When( - Q(start_date__isnull=True) & Q(end_date__isnull=True), + end_date__lt=timezone.now(), then=Value("COMPLETED") + ), + When( + Q(start_date__isnull=True) + & Q(end_date__isnull=True), then=Value("DRAFT"), ), default=Value("DRAFT"), diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/layout.tsx index 3b4866849..9e381c903 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/layout.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/layout.tsx @@ -1,26 +1,33 @@ "use client"; -import { CommandPalette } from "@/components/command-palette"; +import { observer } from "mobx-react"; +import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider"; import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; // plane web components import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper"; import { ProjectAppSidebar } from "./_sidebar"; +const WorkspaceLayoutContent = observer(({ children }: { children: React.ReactNode }) => ( + <> + + +
+
+
+ +
+ {children} +
+
+
+ + +)); + export default function WorkspaceLayout({ children }: { children: React.ReactNode }) { return ( - - -
-
-
- -
- {children} -
-
-
- + {children} ); } diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx index a87d4d267..a42ae9cf8 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx @@ -1,7 +1,7 @@ "use client"; -import { CommandPalette } from "@/components/command-palette"; import { ContentWrapper } from "@/components/core/content-wrapper"; +import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider"; import { SettingsHeader } from "@/components/settings/header"; import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper"; @@ -10,7 +10,7 @@ export default function SettingsLayout({ children }: { children: React.ReactNode return ( - +
{/* Header */} diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/sidebar.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/sidebar.tsx index bda42ccc3..bcf088dd2 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/sidebar.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/sidebar.tsx @@ -12,7 +12,7 @@ import { SettingsSidebar } from "@/components/settings/sidebar"; import { useUserPermissions } from "@/hooks/store/user"; import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper"; -const ICONS = { +export const WORKSPACE_SETTINGS_ICONS = { general: Building, members: Users, export: ArrowUpToLine, @@ -30,7 +30,7 @@ export const WorkspaceActionIcons = ({ className?: string; }) => { if (type === undefined) return null; - const Icon = ICONS[type as keyof typeof ICONS]; + const Icon = WORKSPACE_SETTINGS_ICONS[type as keyof typeof WORKSPACE_SETTINGS_ICONS]; if (!Icon) return null; return ; }; diff --git a/apps/web/app/(all)/layout.tsx b/apps/web/app/(all)/layout.tsx index 2775b1b33..ee6c5750d 100644 --- a/apps/web/app/(all)/layout.tsx +++ b/apps/web/app/(all)/layout.tsx @@ -3,7 +3,7 @@ import type { Metadata, Viewport } from "next"; import { PreloadResources } from "./layout.preload"; // styles -import "@/styles/command-pallette.css"; +import "@/styles/power-k.css"; import "@/styles/emoji.css"; import "@plane/propel/styles/react-day-picker"; diff --git a/apps/web/app/(all)/profile/layout.tsx b/apps/web/app/(all)/profile/layout.tsx index fdc086765..aee7779a0 100644 --- a/apps/web/app/(all)/profile/layout.tsx +++ b/apps/web/app/(all)/profile/layout.tsx @@ -1,9 +1,8 @@ "use client"; import type { ReactNode } from "react"; -// components -import { CommandPalette } from "@/components/command-palette"; // wrappers +import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider"; import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; // layout import { ProfileLayoutSidebar } from "./sidebar"; @@ -17,7 +16,7 @@ export default function ProfileSettingsLayout(props: Props) { return ( <> - +
diff --git a/apps/web/ce/components/command-palette/helpers.tsx b/apps/web/ce/components/command-palette/helpers.tsx index 865aa9e53..1ad7e6f49 100644 --- a/apps/web/ce/components/command-palette/helpers.tsx +++ b/apps/web/ce/components/command-palette/helpers.tsx @@ -93,7 +93,7 @@ export const commandGroups: TCommandGroups = { if (!!projectId && page?.project_ids?.includes(projectId)) redirectProjectId = projectId; return redirectProjectId ? `/${page?.workspace__slug}/projects/${redirectProjectId}/pages/${page?.id}` - : `/${page?.workspace__slug}/pages/${page?.id}`; + : `/${page?.workspace__slug}/wiki/${page?.id}`; }, title: "Pages", }, diff --git a/apps/web/ce/components/command-palette/index.ts b/apps/web/ce/components/command-palette/index.ts index 62404249d..cb220b2bd 100644 --- a/apps/web/ce/components/command-palette/index.ts +++ b/apps/web/ce/components/command-palette/index.ts @@ -1,3 +1,2 @@ export * from "./actions"; -export * from "./modals"; export * from "./helpers"; diff --git a/apps/web/ce/components/command-palette/modals/index.ts b/apps/web/ce/components/command-palette/modals/index.ts deleted file mode 100644 index a4fac4b91..000000000 --- a/apps/web/ce/components/command-palette/modals/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./workspace-level"; -export * from "./project-level"; -export * from "./issue-level"; diff --git a/apps/web/ce/components/command-palette/modals/issue-level.tsx b/apps/web/ce/components/command-palette/modals/work-item-level.tsx similarity index 75% rename from apps/web/ce/components/command-palette/modals/issue-level.tsx rename to apps/web/ce/components/command-palette/modals/work-item-level.tsx index f720e38ea..d06602d7e 100644 --- a/apps/web/ce/components/command-palette/modals/issue-level.tsx +++ b/apps/web/ce/components/command-palette/modals/work-item-level.tsx @@ -15,21 +15,23 @@ import { useUser } from "@/hooks/store/user"; import { useAppRouter } from "@/hooks/use-app-router"; import { useIssuesActions } from "@/hooks/use-issues-actions"; -export type TIssueLevelModalsProps = { - projectId: string | undefined; - issueId: string | undefined; +export type TWorkItemLevelModalsProps = { + workItemIdentifier: string | undefined; }; -export const IssueLevelModals: FC = observer((props) => { - const { projectId, issueId } = props; +export const WorkItemLevelModals: FC = observer((props) => { + const { workItemIdentifier } = props; // router const { workspaceSlug, cycleId, moduleId } = useParams(); const router = useAppRouter(); // store hooks const { data: currentUser } = useUser(); const { - issue: { getIssueById }, + issue: { getIssueById, getIssueIdByIdentifier }, } = useIssueDetail(); + // derived values + const workItemId = workItemIdentifier ? getIssueIdByIdentifier(workItemIdentifier) : undefined; + const workItemDetails = workItemId ? getIssueById(workItemId) : undefined; const { removeIssue: removeEpic } = useIssuesActions(EIssuesStoreType.EPIC); const { removeIssue: removeWorkItem } = useIssuesActions(EIssuesStoreType.PROJECT); @@ -44,13 +46,12 @@ export const IssueLevelModals: FC = observer((props) => createWorkItemAllowedProjectIds, } = useCommandPalette(); // derived values - const issueDetails = issueId ? getIssueById(issueId) : undefined; const { fetchSubIssues: fetchSubWorkItems } = useIssueDetail(); const { fetchSubIssues: fetchEpicSubWorkItems } = useIssueDetail(EIssueServiceType.EPICS); const handleDeleteIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { try { - const isEpic = issueDetails?.is_epic; + const isEpic = workItemDetails?.is_epic; const deleteAction = isEpic ? removeEpic : removeWorkItem; const redirectPath = `/${workspaceSlug}/projects/${projectId}/${isEpic ? "epics" : "issues"}`; @@ -62,10 +63,10 @@ export const IssueLevelModals: FC = observer((props) => }; const handleCreateIssueSubmit = async (newIssue: TIssue) => { - if (!workspaceSlug || !newIssue.project_id || !newIssue.id || newIssue.parent_id !== issueDetails?.id) return; + if (!workspaceSlug || !newIssue.project_id || !newIssue.id || newIssue.parent_id !== workItemDetails?.id) return; - const fetchAction = issueDetails?.is_epic ? fetchEpicSubWorkItems : fetchSubWorkItems; - await fetchAction(workspaceSlug?.toString(), newIssue.project_id, issueDetails.id); + const fetchAction = workItemDetails?.is_epic ? fetchEpicSubWorkItems : fetchSubWorkItems; + await fetchAction(workspaceSlug?.toString(), newIssue.project_id, workItemDetails.id); }; const getCreateIssueModalData = () => { @@ -83,13 +84,15 @@ export const IssueLevelModals: FC = observer((props) => onSubmit={handleCreateIssueSubmit} allowedProjectIds={createWorkItemAllowedProjectIds} /> - {workspaceSlug && projectId && issueId && issueDetails && ( + {workspaceSlug && workItemId && workItemDetails && workItemDetails.project_id && ( toggleDeleteIssueModal(false)} isOpen={isDeleteIssueModalOpen} - data={issueDetails} - onSubmit={() => handleDeleteIssue(workspaceSlug.toString(), projectId?.toString(), issueId?.toString())} - isEpic={issueDetails?.is_epic} + data={workItemDetails} + onSubmit={() => + handleDeleteIssue(workspaceSlug.toString(), workItemDetails.project_id!, workItemId?.toString()) + } + isEpic={workItemDetails?.is_epic} /> )} = {}; diff --git a/apps/web/ce/components/command-palette/power-k/context-detector.ts b/apps/web/ce/components/command-palette/power-k/context-detector.ts new file mode 100644 index 000000000..acc803bdc --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/context-detector.ts @@ -0,0 +1,5 @@ +import type { Params } from "next/dist/shared/lib/router/utils/route-matcher"; +// local imports +import type { TPowerKContextTypeExtended } from "./types"; + +export const detectExtendedContextFromURL = (_params: Params): TPowerKContextTypeExtended | null => null; diff --git a/apps/web/ce/components/command-palette/power-k/hooks/use-extended-context-indicator.ts b/apps/web/ce/components/command-palette/power-k/hooks/use-extended-context-indicator.ts new file mode 100644 index 000000000..ad5f43860 --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/hooks/use-extended-context-indicator.ts @@ -0,0 +1,8 @@ +// local imports +import type { TPowerKContextTypeExtended } from "../types"; + +type TArgs = { + activeContext: TPowerKContextTypeExtended | null; +}; + +export const useExtendedContextIndicator = (_args: TArgs): string | null => null; diff --git a/apps/web/ce/components/command-palette/power-k/pages/context-based/index.ts b/apps/web/ce/components/command-palette/power-k/pages/context-based/index.ts new file mode 100644 index 000000000..1efe34c51 --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/pages/context-based/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/apps/web/ce/components/command-palette/power-k/pages/context-based/root.tsx b/apps/web/ce/components/command-palette/power-k/pages/context-based/root.tsx new file mode 100644 index 000000000..2c6c0e891 --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/pages/context-based/root.tsx @@ -0,0 +1,11 @@ +// components +import type { TPowerKCommandConfig } from "@/components/power-k/core/types"; +import type { ContextBasedActionsProps, TContextEntityMap } from "@/components/power-k/ui/pages/context-based"; +// local imports +import type { TPowerKContextTypeExtended } from "../../types"; + +export const CONTEXT_ENTITY_MAP_EXTENDED: Record = {}; + +export const PowerKContextBasedActionsExtended: React.FC = () => null; + +export const usePowerKContextBasedExtendedActions = (): TPowerKCommandConfig[] => []; diff --git a/apps/web/ce/components/command-palette/power-k/pages/context-based/work-item/state-menu-item.tsx b/apps/web/ce/components/command-palette/power-k/pages/context-based/work-item/state-menu-item.tsx new file mode 100644 index 000000000..5fbc91edf --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/pages/context-based/work-item/state-menu-item.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { observer } from "mobx-react"; +// plane types +import { StateGroupIcon } from "@plane/propel/icons"; +import type { IState } from "@plane/types"; +// components +import { PowerKModalCommandItem } from "@/components/power-k/ui/modal/command-item"; + +export type TPowerKProjectStatesMenuItemsProps = { + handleSelect: (stateId: string) => void; + projectId: string | undefined; + selectedStateId: string | undefined; + states: IState[]; + workspaceSlug: string; +}; + +export const PowerKProjectStatesMenuItems: React.FC = observer((props) => { + const { handleSelect, selectedStateId, states } = props; + + return ( + <> + {states.map((state) => ( + } + label={state.name} + isSelected={state.id === selectedStateId} + onSelect={() => handleSelect(state.id)} + /> + ))} + + ); +}); diff --git a/apps/web/ce/components/command-palette/power-k/search/no-results-command.tsx b/apps/web/ce/components/command-palette/power-k/search/no-results-command.tsx new file mode 100644 index 000000000..cc8ca10d5 --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/search/no-results-command.tsx @@ -0,0 +1,36 @@ +import { Command } from "cmdk"; +import { Search } from "lucide-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// components +import type { TPowerKContext } from "@/components/power-k/core/types"; +// plane web imports +import { PowerKModalCommandItem } from "@/components/power-k/ui/modal/command-item"; + +export type TPowerKModalNoSearchResultsCommandProps = { + context: TPowerKContext; + searchTerm: string; + updateSearchTerm: (value: string) => void; +}; + +export const PowerKModalNoSearchResultsCommand: React.FC = (props) => { + const { updateSearchTerm } = props; + // translation + const { t } = useTranslation(); + + return ( + + + {t("power_k.search_menu.no_results")}{" "} + {t("power_k.search_menu.clear_search")} +

+ } + onSelect={() => updateSearchTerm("")} + /> +
+ ); +}; diff --git a/apps/web/ce/components/command-palette/power-k/search/search-results-map.tsx b/apps/web/ce/components/command-palette/power-k/search/search-results-map.tsx new file mode 100644 index 000000000..c09dd41a1 --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/search/search-results-map.tsx @@ -0,0 +1,10 @@ +"use client"; + +// components +import type { TPowerKSearchResultGroupDetails } from "@/components/power-k/ui/modal/search-results-map"; +// local imports +import type { TPowerKSearchResultsKeysExtended } from "../types"; + +type TSearchResultsGroupsMapExtended = Record; + +export const SEARCH_RESULTS_GROUPS_MAP_EXTENDED: TSearchResultsGroupsMapExtended = {}; diff --git a/apps/web/ce/components/command-palette/power-k/types.ts b/apps/web/ce/components/command-palette/power-k/types.ts new file mode 100644 index 000000000..4e497f8b8 --- /dev/null +++ b/apps/web/ce/components/command-palette/power-k/types.ts @@ -0,0 +1,5 @@ +export type TPowerKContextTypeExtended = never; + +export type TPowerKPageTypeExtended = never; + +export type TPowerKSearchResultsKeysExtended = never; diff --git a/apps/web/ce/components/workspace/sidebar/app-search.tsx b/apps/web/ce/components/workspace/sidebar/app-search.tsx index 9e0f4cd95..89d2607cf 100644 --- a/apps/web/ce/components/workspace/sidebar/app-search.tsx +++ b/apps/web/ce/components/workspace/sidebar/app-search.tsx @@ -1,20 +1,21 @@ import { observer } from "mobx-react"; // plane imports import { useTranslation } from "@plane/i18n"; -// hooks +// components import { SidebarSearchButton } from "@/components/sidebar/search-button"; -import { useCommandPalette } from "@/hooks/store/use-command-palette"; +// hooks +import { usePowerK } from "@/hooks/store/use-power-k"; export const AppSearch = observer(() => { // store hooks - const { toggleCommandPaletteModal } = useCommandPalette(); + const { togglePowerKModal } = usePowerK(); // translation const { t } = useTranslation(); return ( +
+
+ ); +}; diff --git a/apps/web/core/components/power-k/ui/modal/footer.tsx b/apps/web/core/components/power-k/ui/modal/footer.tsx new file mode 100644 index 000000000..0b2dcf4fc --- /dev/null +++ b/apps/web/core/components/power-k/ui/modal/footer.tsx @@ -0,0 +1,34 @@ +"use client"; + +import type React from "react"; +import { observer } from "mobx-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { ToggleSwitch } from "@plane/ui"; + +type Props = { + isWorkspaceLevel: boolean; + projectId: string | undefined; + onWorkspaceLevelChange: (value: boolean) => void; +}; + +export const PowerKModalFooter: React.FC = observer((props) => { + const { isWorkspaceLevel, projectId, onWorkspaceLevelChange } = props; + // translation + const { t } = useTranslation(); + + return ( +
+
+
+ {t("power_k.footer.workspace_level")} + onWorkspaceLevelChange(!isWorkspaceLevel)} + disabled={!projectId} + size="sm" + /> +
+
+ ); +}); diff --git a/apps/web/core/components/power-k/ui/modal/header.tsx b/apps/web/core/components/power-k/ui/modal/header.tsx new file mode 100644 index 000000000..b1354ad71 --- /dev/null +++ b/apps/web/core/components/power-k/ui/modal/header.tsx @@ -0,0 +1,60 @@ +"use client"; + +import React from "react"; +import { Command } from "cmdk"; +import { X, Search } from "lucide-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// local imports +import type { TPowerKContext, TPowerKPageType } from "../../core/types"; +import { POWER_K_MODAL_PAGE_DETAILS } from "./constants"; +import { PowerKModalContextIndicator } from "./context-indicator"; + +type Props = { + activePage: TPowerKPageType | null; + context: TPowerKContext; + onSearchChange: (value: string) => void; + searchTerm: string; +}; + +export const PowerKModalHeader: React.FC = (props) => { + const { context, searchTerm, onSearchChange, activePage } = props; + // translation + const { t } = useTranslation(); + // derived values + const placeholder = activePage + ? t(POWER_K_MODAL_PAGE_DETAILS[activePage].i18n_placeholder) + : t("power_k.page_placeholders.default"); + + return ( +
+ {/* Context Indicator */} + {context.shouldShowContextBasedActions && !activePage && ( + context.setShouldShowContextBasedActions(false)} + /> + )} + + {/* Search Input */} +
+ + + {searchTerm && ( + + )} +
+
+ ); +}; diff --git a/apps/web/core/components/power-k/ui/modal/search-menu.tsx b/apps/web/core/components/power-k/ui/modal/search-menu.tsx new file mode 100644 index 000000000..33d4e034b --- /dev/null +++ b/apps/web/core/components/power-k/ui/modal/search-menu.tsx @@ -0,0 +1,107 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { useParams } from "next/navigation"; +// plane imports +import { WORKSPACE_DEFAULT_SEARCH_RESULT } from "@plane/constants"; +import type { IWorkspaceSearchResults } from "@plane/types"; +import { cn } from "@plane/utils"; +// hooks +import { usePowerK } from "@/hooks/store/use-power-k"; +import useDebounce from "@/hooks/use-debounce"; +// plane web imports +import { PowerKModalNoSearchResultsCommand } from "@/plane-web/components/command-palette/power-k/search/no-results-command"; +import { WorkspaceService } from "@/plane-web/services"; +// local imports +import type { TPowerKContext, TPowerKPageType } from "../../core/types"; +import { PowerKModalSearchResults } from "./search-results"; +// services init +const workspaceService = new WorkspaceService(); + +type Props = { + activePage: TPowerKPageType | null; + context: TPowerKContext; + isWorkspaceLevel: boolean; + searchTerm: string; + updateSearchTerm: (value: string) => void; +}; + +export const PowerKModalSearchMenu: React.FC = (props) => { + const { activePage, context, isWorkspaceLevel, searchTerm, updateSearchTerm } = props; + // states + const [resultsCount, setResultsCount] = useState(0); + const [isSearching, setIsSearching] = useState(false); + const [results, setResults] = useState(WORKSPACE_DEFAULT_SEARCH_RESULT); + const debouncedSearchTerm = useDebounce(searchTerm, 500); + // navigation + const { workspaceSlug, projectId } = useParams(); + // store hooks + const { togglePowerKModal } = usePowerK(); + + useEffect(() => { + if (activePage || !workspaceSlug) return; + setIsSearching(true); + + if (debouncedSearchTerm) { + workspaceService + .searchWorkspace(workspaceSlug.toString(), { + ...(projectId ? { project_id: projectId.toString() } : {}), + search: debouncedSearchTerm, + workspace_search: !projectId ? true : isWorkspaceLevel, + }) + .then((results) => { + setResults(results); + const count = Object.keys(results.results).reduce( + (accumulator, key) => results.results[key as keyof typeof results.results]?.length + accumulator, + 0 + ); + setResultsCount(count); + }) + .catch(() => { + setResults(WORKSPACE_DEFAULT_SEARCH_RESULT); + setResultsCount(0); + }) + .finally(() => setIsSearching(false)); + } else { + setResults(WORKSPACE_DEFAULT_SEARCH_RESULT); + setIsSearching(false); + } + }, [debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug, activePage]); + + if (activePage) return null; + + return ( + <> + {searchTerm.trim() !== "" && ( +
+
+ Search results for{" "} + + {'"'} + {searchTerm} + {'"'} + {" "} + in {isWorkspaceLevel ? "workspace" : "project"}: +
+
+ )} + + {/* Show empty state only when not loading and no results */} + {!isSearching && resultsCount === 0 && searchTerm.trim() !== "" && debouncedSearchTerm.trim() !== "" && ( + + )} + + {searchTerm.trim() !== "" && ( + togglePowerKModal(false)} results={results} /> + )} + + ); +}; diff --git a/apps/web/core/components/power-k/ui/modal/search-results-map.tsx b/apps/web/core/components/power-k/ui/modal/search-results-map.tsx new file mode 100644 index 000000000..601a81a99 --- /dev/null +++ b/apps/web/core/components/power-k/ui/modal/search-results-map.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { Briefcase, FileText, Layers, LayoutGrid } from "lucide-react"; +// plane imports +import { ContrastIcon, DiceIcon } from "@plane/propel/icons"; +import type { + IWorkspaceDefaultSearchResult, + IWorkspaceIssueSearchResult, + IWorkspacePageSearchResult, + IWorkspaceProjectSearchResult, + IWorkspaceSearchResult, +} from "@plane/types"; +import { generateWorkItemLink } from "@plane/utils"; +// components +import type { TPowerKSearchResultsKeys } from "@/components/power-k/core/types"; +// plane web imports +import { SEARCH_RESULTS_GROUPS_MAP_EXTENDED } from "@/plane-web/components/command-palette/power-k/search/search-results-map"; +import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier"; + +export type TPowerKSearchResultGroupDetails = { + icon?: React.ComponentType<{ className?: string }>; + itemName: (item: any) => React.ReactNode; + path: (item: any, projectId: string | undefined) => string; + title: string; +}; + +export const POWER_K_SEARCH_RESULTS_GROUPS_MAP: Record = { + cycle: { + icon: ContrastIcon, + itemName: (cycle: IWorkspaceDefaultSearchResult) => ( +

+ {cycle.project__identifier} {cycle.name} +

+ ), + path: (cycle: IWorkspaceDefaultSearchResult) => + `/${cycle?.workspace__slug}/projects/${cycle?.project_id}/cycles/${cycle?.id}`, + title: "Cycles", + }, + issue: { + itemName: (workItem: IWorkspaceIssueSearchResult) => ( +
+ {" "} + {workItem.name} +
+ ), + path: (workItem: IWorkspaceIssueSearchResult) => + generateWorkItemLink({ + workspaceSlug: workItem?.workspace__slug, + projectId: workItem?.project_id, + issueId: workItem?.id, + projectIdentifier: workItem.project__identifier, + sequenceId: workItem?.sequence_id, + }), + title: "Work items", + }, + issue_view: { + icon: Layers, + itemName: (view: IWorkspaceDefaultSearchResult) => ( +

+ {view.project__identifier} {view.name} +

+ ), + path: (view: IWorkspaceDefaultSearchResult) => + `/${view?.workspace__slug}/projects/${view?.project_id}/views/${view?.id}`, + title: "Views", + }, + module: { + icon: DiceIcon, + itemName: (module: IWorkspaceDefaultSearchResult) => ( +

+ {module.project__identifier} {module.name} +

+ ), + path: (module: IWorkspaceDefaultSearchResult) => + `/${module?.workspace__slug}/projects/${module?.project_id}/modules/${module?.id}`, + title: "Modules", + }, + page: { + icon: FileText, + itemName: (page: IWorkspacePageSearchResult) => ( +

+ {page.project__identifiers?.[0]} {page.name} +

+ ), + path: (page: IWorkspacePageSearchResult, projectId: string | undefined) => { + let redirectProjectId = page?.project_ids?.[0]; + if (!!projectId && page?.project_ids?.includes(projectId)) redirectProjectId = projectId; + return redirectProjectId + ? `/${page?.workspace__slug}/projects/${redirectProjectId}/pages/${page?.id}` + : `/${page?.workspace__slug}/wiki/${page?.id}`; + }, + title: "Pages", + }, + project: { + icon: Briefcase, + itemName: (project: IWorkspaceProjectSearchResult) => project?.name, + path: (project: IWorkspaceProjectSearchResult) => `/${project?.workspace__slug}/projects/${project?.id}/issues/`, + title: "Projects", + }, + workspace: { + icon: LayoutGrid, + itemName: (workspace: IWorkspaceSearchResult) => workspace?.name, + path: (workspace: IWorkspaceSearchResult) => `/${workspace?.slug}/`, + title: "Workspaces", + }, + ...SEARCH_RESULTS_GROUPS_MAP_EXTENDED, +}; diff --git a/apps/web/core/components/power-k/ui/modal/search-results.tsx b/apps/web/core/components/power-k/ui/modal/search-results.tsx new file mode 100644 index 000000000..e195728e9 --- /dev/null +++ b/apps/web/core/components/power-k/ui/modal/search-results.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { Command } from "cmdk"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +// plane imports +import type { IWorkspaceSearchResults } from "@plane/types"; +// hooks +import { useAppRouter } from "@/hooks/use-app-router"; +// helpers +import { openProjectAndScrollToSidebar } from "../../actions/helper"; +import { PowerKModalCommandItem } from "./command-item"; +import { POWER_K_SEARCH_RESULTS_GROUPS_MAP } from "./search-results-map"; + +type Props = { + closePalette: () => void; + results: IWorkspaceSearchResults; +}; + +export const PowerKModalSearchResults: React.FC = observer((props) => { + const { closePalette, results } = props; + // router + const router = useAppRouter(); + const { projectId: routerProjectId } = useParams(); + // derived values + const projectId = routerProjectId?.toString(); + + return ( + <> + {Object.keys(results.results).map((key) => { + const section = results.results[key as keyof typeof results.results]; + const currentSection = POWER_K_SEARCH_RESULTS_GROUPS_MAP[key as keyof typeof POWER_K_SEARCH_RESULTS_GROUPS_MAP]; + + if (!currentSection) return null; + if (section.length <= 0) return null; + + return ( + + {section.map((item) => { + let value = `${key}-${item?.id}-${item.name}`; + + if ("project__identifier" in item) { + value = `${value}-${item.project__identifier}`; + } + + if ("sequence_id" in item) { + value = `${value}-${item.sequence_id}`; + } + + return ( + { + closePalette(); + router.push(currentSection.path(item, projectId)); + // const itemProjectId = + // item?.project_id || + // (Array.isArray(item?.project_ids) && item?.project_ids?.length > 0 + // ? item?.project_ids[0] + // : undefined); + // if (itemProjectId) openProjectAndScrollToSidebar(itemProjectId); + }} + value={value} + /> + ); + })} + + ); + })} + + ); +}); diff --git a/apps/web/core/components/command-palette/shortcuts-modal/modal.tsx b/apps/web/core/components/power-k/ui/modal/shortcuts-root.tsx similarity index 88% rename from apps/web/core/components/command-palette/shortcuts-modal/modal.tsx rename to apps/web/core/components/power-k/ui/modal/shortcuts-root.tsx index ffd443074..97250edcd 100644 --- a/apps/web/core/components/command-palette/shortcuts-modal/modal.tsx +++ b/apps/web/core/components/power-k/ui/modal/shortcuts-root.tsx @@ -4,10 +4,12 @@ import type { FC } from "react"; import { useState, Fragment } from "react"; import { Search, X } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; -// components +// plane imports import { Input } from "@plane/ui"; -import { ShortcutCommandsList } from "@/components/command-palette"; -// ui +// hooks +import { usePowerK } from "@/hooks/store/use-power-k"; +// local imports +import { ShortcutRenderer } from "../renderer/shortcut"; type Props = { isOpen: boolean; @@ -18,6 +20,11 @@ export const ShortcutsModal: FC = (props) => { const { isOpen, onClose } = props; // states const [query, setQuery] = useState(""); + // store hooks + const { commandRegistry } = usePowerK(); + + // Get all commands from registry + const allCommandsWithShortcuts = commandRegistry.getAllCommandsWithShortcuts(); const handleClose = () => { onClose(); @@ -72,7 +79,7 @@ export const ShortcutsModal: FC = (props) => { tabIndex={1} />
- +
diff --git a/apps/web/core/components/power-k/ui/modal/wrapper.tsx b/apps/web/core/components/power-k/ui/modal/wrapper.tsx new file mode 100644 index 000000000..a8ed9fb26 --- /dev/null +++ b/apps/web/core/components/power-k/ui/modal/wrapper.tsx @@ -0,0 +1,186 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import { Command } from "cmdk"; +import { observer } from "mobx-react"; +import { Dialog, Transition } from "@headlessui/react"; +// hooks +import { usePowerK } from "@/hooks/store/use-power-k"; +// local imports +import type { TPowerKCommandConfig, TPowerKContext } from "../../core/types"; +import type { TPowerKCommandsListProps } from "./commands-list"; +import { PowerKModalFooter } from "./footer"; +import { PowerKModalHeader } from "./header"; + +type Props = { + commandsListComponent: React.FC; + context: TPowerKContext; + hideFooter?: boolean; + isOpen: boolean; + onClose: () => void; +}; + +export const ProjectsAppPowerKModalWrapper = observer((props: Props) => { + const { commandsListComponent: CommandsListComponent, context, hideFooter = false, isOpen, onClose } = props; + // states + const [searchTerm, setSearchTerm] = useState(""); + const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); + // store hooks + const { activePage, setActivePage } = usePowerK(); + + // Handle command selection + const handleCommandSelect = useCallback( + (command: TPowerKCommandConfig) => { + if (command.type === "action") { + // Direct action - execute and potentially close + command.action(context); + if (command.closeOnSelect === true) { + context.closePalette(); + } + } else if (command.type === "change-page") { + // Opens a selection page + context.setActiveCommand(command); + setActivePage(command.page); + setSearchTerm(""); + } + }, + [context, setActivePage] + ); + + // Handle selection page item selection + const handlePageDataSelection = useCallback( + (data: unknown) => { + if (context.activeCommand?.type === "change-page") { + context.activeCommand.onSelect(data, context); + } + // Go back to main page + if (context.activeCommand?.closeOnSelect === true) { + context.closePalette(); + } + }, + [context] + ); + + // Handle keyboard navigation + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + // Cmd/Ctrl+K closes palette + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { + e.preventDefault(); + onClose(); + return; + } + + // Escape closes palette or clears search + if (e.key === "Escape") { + e.preventDefault(); + if (searchTerm) { + setSearchTerm(""); + } else { + onClose(); + } + return; + } + + // Backspace clears context or goes back from page + if (e.key === "Backspace" && !searchTerm) { + e.preventDefault(); + if (activePage) { + // Go back from selection page + setActivePage(null); + context.setActiveCommand(null); + } else { + // Hide context based actions + context.setShouldShowContextBasedActions(false); + } + return; + } + }, + [searchTerm, activePage, onClose, setActivePage, context] + ); + + // Reset state when modal closes + useEffect(() => { + if (!isOpen) { + setTimeout(() => { + setSearchTerm(""); + setActivePage(null); + context.setActiveCommand(null); + context.setShouldShowContextBasedActions(true); + }, 200); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]); + + return ( + + + {/* Backdrop */} + +
+ + {/* Modal Container */} +
+
+ + + { + if (i18nValue === "no-results") return 1; + if (i18nValue.toLowerCase().includes(search.toLowerCase())) return 1; + return 0; + }} + shouldFilter={searchTerm.length > 0} + onKeyDown={handleKeyDown} + className="w-full" + > + + + + + {/* Footer hints */} + {!hideFooter && ( + + )} + + + +
+
+
+
+ ); +}); diff --git a/apps/web/core/components/power-k/ui/pages/context-based/cycle/commands.ts b/apps/web/core/components/power-k/ui/pages/context-based/cycle/commands.ts new file mode 100644 index 000000000..bab95a2dc --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/cycle/commands.ts @@ -0,0 +1,94 @@ +import { useCallback } from "react"; +import { useParams } from "next/navigation"; +import { LinkIcon, Star, StarOff } from "lucide-react"; +// plane imports +import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { setToast, TOAST_TYPE } from "@plane/propel/toast"; +import { copyTextToClipboard } from "@plane/utils"; +// components +import type { TPowerKCommandConfig } from "@/components/power-k/core/types"; +// hooks +import { useCycle } from "@/hooks/store/use-cycle"; +import { useUser } from "@/hooks/store/user"; + +export const usePowerKCycleContextBasedActions = (): TPowerKCommandConfig[] => { + // navigation + const { workspaceSlug, cycleId } = useParams(); + // store + const { + permission: { allowPermissions }, + } = useUser(); + const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle(); + // derived values + const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : null; + const isFavorite = !!cycleDetails?.is_favorite; + // permission + const isEditingAllowed = + allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && + !cycleDetails?.archived_at; + // translation + const { t } = useTranslation(); + + const toggleFavorite = useCallback(() => { + if (!workspaceSlug || !cycleDetails || !cycleDetails.project_id) return; + try { + if (isFavorite) removeCycleFromFavorites(workspaceSlug.toString(), cycleDetails.project_id, cycleDetails.id); + else addCycleToFavorites(workspaceSlug.toString(), cycleDetails.project_id, cycleDetails.id); + } catch { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Some error occurred", + }); + } + }, [addCycleToFavorites, removeCycleFromFavorites, workspaceSlug, cycleDetails, isFavorite]); + + const copyCycleUrlToClipboard = useCallback(() => { + const url = new URL(window.location.href); + copyTextToClipboard(url.href) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("power_k.contextual_actions.cycle.copy_url_toast_success"), + }); + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: t("power_k.contextual_actions.cycle.copy_url_toast_error"), + }); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return [ + { + id: "toggle_cycle_favorite", + i18n_title: isFavorite + ? "power_k.contextual_actions.cycle.remove_from_favorites" + : "power_k.contextual_actions.cycle.add_to_favorites", + icon: isFavorite ? StarOff : Star, + group: "contextual", + contextType: "cycle", + type: "action", + action: toggleFavorite, + modifierShortcut: "shift+f", + isEnabled: () => isEditingAllowed, + isVisible: () => isEditingAllowed, + closeOnSelect: true, + }, + { + id: "copy_cycle_url", + i18n_title: "power_k.contextual_actions.cycle.copy_url", + icon: LinkIcon, + group: "contextual", + contextType: "cycle", + type: "action", + action: copyCycleUrlToClipboard, + modifierShortcut: "cmd+shift+,", + isEnabled: () => true, + isVisible: () => true, + closeOnSelect: true, + }, + ]; +}; diff --git a/apps/web/core/components/power-k/ui/pages/context-based/index.ts b/apps/web/core/components/power-k/ui/pages/context-based/index.ts new file mode 100644 index 000000000..01709be4d --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/index.ts @@ -0,0 +1,31 @@ +export * from "./root"; + +// components +import type { TPowerKContextType } from "@/components/power-k/core/types"; +// plane web imports +import { CONTEXT_ENTITY_MAP_EXTENDED } from "@/plane-web/components/command-palette/power-k/pages/context-based"; + +export type TContextEntityMap = { + i18n_title: string; + i18n_indicator: string; +}; + +export const CONTEXT_ENTITY_MAP: Record = { + "work-item": { + i18n_title: "power_k.contextual_actions.work_item.title", + i18n_indicator: "power_k.contextual_actions.work_item.indicator", + }, + page: { + i18n_title: "power_k.contextual_actions.page.title", + i18n_indicator: "power_k.contextual_actions.page.indicator", + }, + cycle: { + i18n_title: "power_k.contextual_actions.cycle.title", + i18n_indicator: "power_k.contextual_actions.cycle.indicator", + }, + module: { + i18n_title: "power_k.contextual_actions.module.title", + i18n_indicator: "power_k.contextual_actions.module.indicator", + }, + ...CONTEXT_ENTITY_MAP_EXTENDED, +}; diff --git a/apps/web/core/components/power-k/ui/pages/context-based/module/commands.tsx b/apps/web/core/components/power-k/ui/pages/context-based/module/commands.tsx new file mode 100644 index 000000000..757fa75af --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/module/commands.tsx @@ -0,0 +1,160 @@ +import { useCallback } from "react"; +import { useParams } from "next/navigation"; +import { LinkIcon, Star, StarOff, Users } from "lucide-react"; +// plane imports +import { EUserPermissionsLevel } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { ModuleStatusIcon } from "@plane/propel/icons"; +import { setToast, TOAST_TYPE } from "@plane/propel/toast"; +import type { IModule, TModuleStatus } from "@plane/types"; +import { EUserPermissions } from "@plane/types"; +import { copyTextToClipboard } from "@plane/utils"; +// components +import type { TPowerKCommandConfig } from "@/components/power-k/core/types"; +// hooks +import { useModule } from "@/hooks/store/use-module"; +import { useUser } from "@/hooks/store/user"; + +export const usePowerKModuleContextBasedActions = (): TPowerKCommandConfig[] => { + // navigation + const { workspaceSlug, projectId, moduleId } = useParams(); + // store + const { + permission: { allowPermissions }, + } = useUser(); + const { getModuleById, addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule(); + // derived values + const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : null; + const isFavorite = !!moduleDetails?.is_favorite; + // permission + const isEditingAllowed = + allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && + !moduleDetails?.archived_at; + // translation + const { t } = useTranslation(); + + const handleUpdateModule = useCallback( + async (formData: Partial) => { + if (!workspaceSlug || !projectId || !moduleDetails) return; + await updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleDetails.id, formData).catch( + () => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Module could not be updated. Please try again.", + }); + } + ); + }, + [moduleDetails, projectId, updateModuleDetails, workspaceSlug] + ); + + const handleUpdateMember = useCallback( + (memberId: string) => { + if (!moduleDetails) return; + + const updatedMembers = moduleDetails.member_ids ?? []; + if (updatedMembers.includes(memberId)) updatedMembers.splice(updatedMembers.indexOf(memberId), 1); + else updatedMembers.push(memberId); + + handleUpdateModule({ member_ids: updatedMembers }); + }, + [handleUpdateModule, moduleDetails] + ); + + const toggleFavorite = useCallback(() => { + if (!workspaceSlug || !moduleDetails || !moduleDetails.project_id) return; + try { + if (isFavorite) removeModuleFromFavorites(workspaceSlug.toString(), moduleDetails.project_id, moduleDetails.id); + else addModuleToFavorites(workspaceSlug.toString(), moduleDetails.project_id, moduleDetails.id); + } catch { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Some error occurred", + }); + } + }, [addModuleToFavorites, removeModuleFromFavorites, workspaceSlug, moduleDetails, isFavorite]); + + const copyModuleUrlToClipboard = useCallback(() => { + const url = new URL(window.location.href); + copyTextToClipboard(url.href) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("power_k.contextual_actions.module.copy_url_toast_success"), + }); + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: t("power_k.contextual_actions.module.copy_url_toast_error"), + }); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return [ + { + id: "add_remove_module_members", + i18n_title: "power_k.contextual_actions.module.add_remove_members", + icon: Users, + group: "contextual", + contextType: "module", + type: "change-page", + page: "update-module-member", + onSelect: (data) => { + const memberId = data as string; + handleUpdateMember(memberId); + }, + shortcut: "m", + isEnabled: () => isEditingAllowed, + isVisible: () => isEditingAllowed, + closeOnSelect: false, + }, + { + id: "change_module_status", + i18n_title: "power_k.contextual_actions.module.change_status", + iconNode: , + group: "contextual", + contextType: "module", + type: "change-page", + page: "update-module-status", + onSelect: (data) => { + const status = data as TModuleStatus; + handleUpdateModule({ status }); + }, + shortcut: "s", + isEnabled: () => isEditingAllowed, + isVisible: () => isEditingAllowed, + closeOnSelect: true, + }, + { + id: "toggle_module_favorite", + i18n_title: isFavorite + ? "power_k.contextual_actions.module.remove_from_favorites" + : "power_k.contextual_actions.module.add_to_favorites", + icon: isFavorite ? StarOff : Star, + group: "contextual", + contextType: "module", + type: "action", + action: toggleFavorite, + modifierShortcut: "shift+f", + isEnabled: () => isEditingAllowed, + isVisible: () => isEditingAllowed, + closeOnSelect: true, + }, + { + id: "copy_module_url", + i18n_title: "power_k.contextual_actions.module.copy_url", + icon: LinkIcon, + group: "contextual", + contextType: "module", + type: "action", + action: copyModuleUrlToClipboard, + modifierShortcut: "cmd+shift+,", + isEnabled: () => true, + isVisible: () => true, + closeOnSelect: true, + }, + ]; +}; diff --git a/apps/web/core/components/power-k/ui/pages/context-based/module/index.ts b/apps/web/core/components/power-k/ui/pages/context-based/module/index.ts new file mode 100644 index 000000000..1efe34c51 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/module/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/apps/web/core/components/power-k/ui/pages/context-based/module/root.tsx b/apps/web/core/components/power-k/ui/pages/context-based/module/root.tsx new file mode 100644 index 000000000..6c7edfb2c --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/module/root.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +// components +import type { TPowerKPageType } from "@/components/power-k/core/types"; +import { PowerKMembersMenu } from "@/components/power-k/menus/members"; +// hooks +import { useMember } from "@/hooks/store/use-member"; +import { useModule } from "@/hooks/store/use-module"; +// local imports +import { PowerKModuleStatusMenu } from "./status-menu"; + +type Props = { + activePage: TPowerKPageType | null; + handleSelection: (data: unknown) => void; +}; + +export const PowerKModuleContextBasedPages: React.FC = observer((props) => { + const { activePage, handleSelection } = props; + // navigation + const { moduleId } = useParams(); + // store hooks + const { getModuleById } = useModule(); + const { + project: { getProjectMemberIds }, + } = useMember(); + // derived values + const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : null; + const projectMemberIds = moduleDetails?.project_id ? getProjectMemberIds(moduleDetails.project_id, false) : []; + + if (!moduleDetails) return null; + + return ( + <> + {/* members menu */} + {activePage === "update-module-member" && moduleDetails && ( + + )} + {/* status menu */} + {activePage === "update-module-status" && moduleDetails?.status && ( + + )} + + ); +}); diff --git a/apps/web/core/components/power-k/ui/pages/context-based/module/status-menu.tsx b/apps/web/core/components/power-k/ui/pages/context-based/module/status-menu.tsx new file mode 100644 index 000000000..5510b5435 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/module/status-menu.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { Command } from "cmdk"; +import { observer } from "mobx-react"; +// plane imports +import { MODULE_STATUS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { ModuleStatusIcon } from "@plane/propel/icons"; +import type { TModuleStatus } from "@plane/types"; +// local imports +import { PowerKModalCommandItem } from "../../../modal/command-item"; + +type Props = { + handleSelect: (data: TModuleStatus) => void; + value: TModuleStatus; +}; + +export const PowerKModuleStatusMenu: React.FC = observer((props) => { + const { handleSelect, value } = props; + // translation + const { t } = useTranslation(); + + return ( + + {MODULE_STATUS.map((status) => ( + } + label={t(status.i18n_label)} + isSelected={status.value === value} + onSelect={() => handleSelect(status.value)} + /> + ))} + + ); +}); diff --git a/apps/web/core/components/power-k/ui/pages/context-based/page/commands.ts b/apps/web/core/components/power-k/ui/pages/context-based/page/commands.ts new file mode 100644 index 000000000..c0f48ca1c --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/page/commands.ts @@ -0,0 +1,183 @@ +import { useCallback } from "react"; +import { useParams } from "next/navigation"; +import { + ArchiveIcon, + ArchiveRestoreIcon, + Globe2, + LinkIcon, + Lock, + LockKeyhole, + LockKeyholeOpen, + Star, + StarOff, +} from "lucide-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { setToast, TOAST_TYPE } from "@plane/propel/toast"; +import { EPageAccess } from "@plane/types"; +import { copyTextToClipboard } from "@plane/utils"; +// components +import type { TPowerKCommandConfig } from "@/components/power-k/core/types"; +// plane web imports +import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store"; + +export const usePowerKPageContextBasedActions = (): TPowerKCommandConfig[] => { + // navigation + const { pageId } = useParams(); + // store hooks + const { getPageById } = usePageStore(EPageStoreType.PROJECT); + // derived values + const page = pageId ? getPageById(pageId.toString()) : null; + const { + access, + archived_at, + canCurrentUserArchivePage, + canCurrentUserChangeAccess, + canCurrentUserFavoritePage, + canCurrentUserLockPage, + addToFavorites, + removePageFromFavorites, + lock, + unlock, + makePrivate, + makePublic, + archive, + restore, + } = page ?? {}; + const isFavorite = !!page?.is_favorite; + const isLocked = !!page?.is_locked; + // translation + const { t } = useTranslation(); + + const toggleFavorite = useCallback(() => { + try { + if (isFavorite) removePageFromFavorites?.(); + else addToFavorites?.(); + } catch { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Some error occurred", + }); + } + }, [addToFavorites, removePageFromFavorites, isFavorite]); + + const copyPageUrlToClipboard = useCallback(() => { + const url = new URL(window.location.href); + copyTextToClipboard(url.href) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("power_k.contextual_actions.page.copy_url_toast_success"), + }); + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: t("power_k.contextual_actions.page.copy_url_toast_error"), + }); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return [ + { + id: "toggle_page_lock", + i18n_title: isLocked ? "power_k.contextual_actions.page.unlock" : "power_k.contextual_actions.page.lock", + icon: isLocked ? LockKeyholeOpen : LockKeyhole, + group: "contextual", + contextType: "page", + type: "action", + action: () => { + if (isLocked) + unlock?.({ + shouldSync: true, + recursive: true, + }); + else + lock?.({ + shouldSync: true, + recursive: true, + }); + }, + modifierShortcut: "shift+l", + isEnabled: () => !!canCurrentUserLockPage, + isVisible: () => !!canCurrentUserLockPage, + closeOnSelect: true, + }, + { + id: "toggle_page_access", + i18n_title: + access === EPageAccess.PUBLIC + ? "power_k.contextual_actions.page.make_private" + : "power_k.contextual_actions.page.make_public", + icon: access === EPageAccess.PUBLIC ? Lock : Globe2, + group: "contextual", + contextType: "page", + type: "action", + action: () => { + if (access === EPageAccess.PUBLIC) + makePrivate?.({ + shouldSync: true, + }); + else + makePublic?.({ + shouldSync: true, + }); + }, + modifierShortcut: "shift+a", + isEnabled: () => !!canCurrentUserChangeAccess, + isVisible: () => !!canCurrentUserChangeAccess, + closeOnSelect: true, + }, + { + id: "toggle_page_archive", + i18n_title: archived_at ? "power_k.contextual_actions.page.restore" : "power_k.contextual_actions.page.archive", + icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, + group: "contextual", + contextType: "page", + type: "action", + action: () => { + if (archived_at) + restore?.({ + shouldSync: true, + }); + else + archive?.({ + shouldSync: true, + }); + }, + modifierShortcut: "shift+r", + isEnabled: () => !!canCurrentUserArchivePage, + isVisible: () => !!canCurrentUserArchivePage, + closeOnSelect: true, + }, + { + id: "toggle_page_favorite", + i18n_title: isFavorite + ? "power_k.contextual_actions.page.remove_from_favorites" + : "power_k.contextual_actions.page.add_to_favorites", + icon: isFavorite ? StarOff : Star, + group: "contextual", + contextType: "page", + type: "action", + action: () => toggleFavorite(), + modifierShortcut: "shift+f", + isEnabled: () => !!canCurrentUserFavoritePage, + isVisible: () => !!canCurrentUserFavoritePage, + closeOnSelect: true, + }, + { + id: "copy_page_url", + i18n_title: "power_k.contextual_actions.page.copy_url", + icon: LinkIcon, + group: "contextual", + contextType: "page", + type: "action", + action: copyPageUrlToClipboard, + modifierShortcut: "cmd+shift+,", + isEnabled: () => true, + isVisible: () => true, + closeOnSelect: true, + }, + ]; +}; diff --git a/apps/web/core/components/power-k/ui/pages/context-based/root.tsx b/apps/web/core/components/power-k/ui/pages/context-based/root.tsx new file mode 100644 index 000000000..71c7828cd --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/root.tsx @@ -0,0 +1,46 @@ +// components +import type { TPowerKCommandConfig, TPowerKContextType, TPowerKPageType } from "@/components/power-k/core/types"; +// plane web imports +import { + PowerKContextBasedActionsExtended, + usePowerKContextBasedExtendedActions, +} from "@/plane-web/components/command-palette/power-k/pages/context-based"; +// local imports +import { usePowerKCycleContextBasedActions } from "./cycle/commands"; +import { PowerKModuleContextBasedPages } from "./module"; +import { usePowerKModuleContextBasedActions } from "./module/commands"; +import { usePowerKPageContextBasedActions } from "./page/commands"; +import { PowerKWorkItemContextBasedPages } from "./work-item"; +import { usePowerKWorkItemContextBasedCommands } from "./work-item/commands"; + +export type ContextBasedActionsProps = { + activePage: TPowerKPageType | null; + activeContext: TPowerKContextType | null; + handleSelection: (data: unknown) => void; +}; + +export const PowerKContextBasedPagesList: React.FC = (props) => { + const { activeContext, activePage, handleSelection } = props; + + return ( + <> + {activeContext === "work-item" && ( + + )} + {activeContext === "module" && ( + + )} + + + ); +}; + +export const usePowerKContextBasedActions = (): TPowerKCommandConfig[] => { + const workItemCommands = usePowerKWorkItemContextBasedCommands(); + const cycleCommands = usePowerKCycleContextBasedActions(); + const moduleCommands = usePowerKModuleContextBasedActions(); + const pageCommands = usePowerKPageContextBasedActions(); + const extendedCommands = usePowerKContextBasedExtendedActions(); + + return [...workItemCommands, ...cycleCommands, ...moduleCommands, ...pageCommands, ...extendedCommands]; +}; diff --git a/apps/web/core/components/power-k/ui/pages/context-based/work-item/commands.ts b/apps/web/core/components/power-k/ui/pages/context-based/work-item/commands.ts new file mode 100644 index 000000000..875ba37bc --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/work-item/commands.ts @@ -0,0 +1,453 @@ +import { useCallback } from "react"; +import { useParams } from "next/navigation"; +import { + Bell, + BellOff, + LinkIcon, + Signal, + TagIcon, + TicketCheck, + Trash2, + Triangle, + Type, + UserMinus2, + UserPlus2, + Users, +} from "lucide-react"; +// plane imports +import { EUserPermissionsLevel } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { ContrastIcon, DiceIcon, DoubleCircleIcon } from "@plane/propel/icons"; +import { setToast, TOAST_TYPE } from "@plane/propel/toast"; +import type { ICycle, IIssueLabel, IModule, TIssue, TIssuePriorities } from "@plane/types"; +import { EIssueServiceType, EUserPermissions } from "@plane/types"; +import { copyTextToClipboard } from "@plane/utils"; +// components +import type { TPowerKCommandConfig } from "@/components/power-k/core/types"; +// hooks +import { useProjectEstimates } from "@/hooks/store/estimates"; +import { useCommandPalette } from "@/hooks/store/use-command-palette"; +import { useIssueDetail } from "@/hooks/store/use-issue-detail"; +import { useProject } from "@/hooks/store/use-project"; +import { useUser } from "@/hooks/store/user"; + +export const usePowerKWorkItemContextBasedCommands = (): TPowerKCommandConfig[] => { + // params + const { workspaceSlug, workItem: entityIdentifier } = useParams(); + // store + const { + data: currentUser, + permission: { allowPermissions }, + } = useUser(); + const { toggleDeleteIssueModal } = useCommandPalette(); + const { getProjectById } = useProject(); + const { areEstimateEnabledByProjectId } = useProjectEstimates(); + const { + issue: { getIssueById, getIssueIdByIdentifier, addCycleToIssue, removeIssueFromCycle, changeModulesInIssue }, + subscription: { getSubscriptionByIssueId, createSubscription, removeSubscription }, + updateIssue, + } = useIssueDetail(EIssueServiceType.ISSUES); + const { + issue: { + addCycleToIssue: addCycleToEpic, + removeIssueFromCycle: removeEpicFromCycle, + changeModulesInIssue: changeModulesInEpic, + }, + subscription: { createSubscription: createEpicSubscription, removeSubscription: removeEpicSubscription }, + updateIssue: updateEpic, + } = useIssueDetail(EIssueServiceType.EPICS); + // derived values + const entityId = entityIdentifier ? getIssueIdByIdentifier(entityIdentifier.toString()) : null; + const entityDetails = entityId ? getIssueById(entityId) : null; + const isEpic = !!entityDetails?.is_epic; + const projectDetails = entityDetails?.project_id ? getProjectById(entityDetails?.project_id) : undefined; + const isCurrentUserAssigned = !!entityDetails?.assignee_ids?.includes(currentUser?.id ?? ""); + const isEstimateEnabled = entityDetails?.project_id + ? areEstimateEnabledByProjectId(entityDetails?.project_id) + : false; + const isSubscribed = Boolean(entityId ? getSubscriptionByIssueId(entityId) : false); + // translation + const { t } = useTranslation(); + // handlers + const updateEntity = isEpic ? updateEpic : updateIssue; + const createEntitySubscription = isEpic ? createEpicSubscription : createSubscription; + const removeEntitySubscription = isEpic ? removeEpicSubscription : removeSubscription; + // permission + const isEditingAllowed = + allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug?.toString(), + entityDetails?.project_id ?? undefined + ) && !entityDetails?.archived_at; + + const handleUpdateEntity = useCallback( + async (formData: Partial) => { + if (!workspaceSlug || !entityDetails || !entityDetails.project_id) return; + await updateEntity(workspaceSlug.toString(), entityDetails.project_id, entityDetails.id, formData).catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: `${isEpic ? "Epic" : "Work item"} could not be updated. Please try again.`, + }); + }); + }, + [entityDetails, isEpic, updateEntity, workspaceSlug] + ); + + const handleUpdateAssignee = useCallback( + (assigneeId: string) => { + if (!entityDetails) return; + + const updatedAssignees = [...(entityDetails.assignee_ids ?? [])]; + if (updatedAssignees.includes(assigneeId)) updatedAssignees.splice(updatedAssignees.indexOf(assigneeId), 1); + else updatedAssignees.push(assigneeId); + + handleUpdateEntity({ assignee_ids: updatedAssignees }); + }, + [entityDetails, handleUpdateEntity] + ); + + const handleSubscription = useCallback(async () => { + if (!workspaceSlug || !entityDetails || !entityDetails.project_id) return; + + try { + if (isSubscribed) { + await removeEntitySubscription(workspaceSlug.toString(), entityDetails.project_id, entityDetails.id); + } else { + await createEntitySubscription(workspaceSlug.toString(), entityDetails.project_id, entityDetails.id); + } + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("toast.success"), + message: isSubscribed + ? t("issue.subscription.actions.unsubscribed") + : t("issue.subscription.actions.subscribed"), + }); + } catch { + setToast({ + type: TOAST_TYPE.ERROR, + title: t("toast.error"), + message: t("common.error.message"), + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [createEntitySubscription, entityDetails, isSubscribed, removeEntitySubscription, workspaceSlug]); + + const handleDeleteWorkItem = useCallback(() => { + toggleDeleteIssueModal(true); + }, [toggleDeleteIssueModal]); + + const copyWorkItemIdToClipboard = useCallback(() => { + const id = `${projectDetails?.identifier}-${entityDetails?.sequence_id}`; + copyTextToClipboard(id) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("power_k.contextual_actions.work_item.copy_id_toast_success"), + }); + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: t("power_k.contextual_actions.work_item.copy_id_toast_error"), + }); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [entityDetails?.sequence_id, projectDetails?.identifier]); + + const copyWorkItemTitleToClipboard = useCallback(() => { + copyTextToClipboard(entityDetails?.name ?? "") + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("power_k.contextual_actions.work_item.copy_title_toast_success"), + }); + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: t("power_k.contextual_actions.work_item.copy_title_toast_error"), + }); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [entityDetails?.name]); + + const copyWorkItemUrlToClipboard = useCallback(() => { + const url = new URL(window.location.href); + copyTextToClipboard(url.href) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("power_k.contextual_actions.work_item.copy_url_toast_success"), + }); + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: t("power_k.contextual_actions.work_item.copy_url_toast_error"), + }); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return [ + { + id: "change_work_item_state", + i18n_title: "power_k.contextual_actions.work_item.change_state", + icon: DoubleCircleIcon, + group: "contextual", + contextType: "work-item", + type: "change-page", + page: "update-work-item-state", + onSelect: (data) => { + const stateId = data as string; + if (entityDetails?.state_id === stateId) return; + handleUpdateEntity({ + state_id: stateId, + }); + }, + shortcut: "s", + isEnabled: () => isEditingAllowed, + isVisible: () => isEditingAllowed, + closeOnSelect: true, + }, + { + id: "change_work_item_priority", + i18n_title: "power_k.contextual_actions.work_item.change_priority", + icon: Signal, + group: "contextual", + contextType: "work-item", + type: "change-page", + page: "update-work-item-priority", + onSelect: (data) => { + const priority = data as TIssuePriorities; + if (entityDetails?.priority === priority) return; + handleUpdateEntity({ + priority, + }); + }, + shortcut: "p", + isEnabled: () => isEditingAllowed, + isVisible: () => isEditingAllowed, + closeOnSelect: true, + }, + { + id: "change_work_item_assignees", + i18n_title: "power_k.contextual_actions.work_item.change_assignees", + icon: Users, + group: "contextual", + contextType: "work-item", + type: "change-page", + page: "update-work-item-assignee", + onSelect: (data) => { + const assigneeId = data as string; + handleUpdateAssignee(assigneeId); + }, + shortcut: "a", + isEnabled: () => isEditingAllowed, + isVisible: () => isEditingAllowed, + closeOnSelect: false, + }, + { + id: "assign_work_item_to_me", + i18n_title: isCurrentUserAssigned + ? "power_k.contextual_actions.work_item.unassign_from_me" + : "power_k.contextual_actions.work_item.assign_to_me", + icon: isCurrentUserAssigned ? UserMinus2 : UserPlus2, + group: "contextual", + contextType: "work-item", + type: "action", + action: () => { + if (!currentUser) return; + handleUpdateAssignee(currentUser.id); + }, + shortcut: "i", + isEnabled: () => isEditingAllowed, + isVisible: () => isEditingAllowed, + closeOnSelect: true, + }, + { + id: "change_work_item_estimate", + i18n_title: "power_k.contextual_actions.work_item.change_estimate", + icon: Triangle, + group: "contextual", + contextType: "work-item", + type: "change-page", + page: "update-work-item-estimate", + onSelect: (data) => { + const estimatePointId = data as string | null; + if (entityDetails?.estimate_point === estimatePointId) return; + handleUpdateEntity({ + estimate_point: estimatePointId, + }); + }, + modifierShortcut: "shift+e", + isEnabled: () => isEstimateEnabled && isEditingAllowed, + isVisible: () => isEstimateEnabled && isEditingAllowed, + closeOnSelect: true, + }, + { + id: "add_work_item_to_cycle", + i18n_title: "power_k.contextual_actions.work_item.add_to_cycle", + icon: ContrastIcon, + group: "contextual", + contextType: "work-item", + type: "change-page", + page: "update-work-item-cycle", + onSelect: (data) => { + const cycleId = (data as ICycle)?.id; + if (!workspaceSlug || !entityDetails || !entityDetails.project_id) return; + if (entityDetails.cycle_id === cycleId) return; + // handlers + const addCycleToEntity = entityDetails.is_epic ? addCycleToEpic : addCycleToIssue; + const removeCycleFromEntity = entityDetails.is_epic ? removeEpicFromCycle : removeIssueFromCycle; + + try { + if (cycleId) { + addCycleToEntity(workspaceSlug.toString(), entityDetails.project_id, cycleId, entityDetails.id); + } else { + removeCycleFromEntity( + workspaceSlug.toString(), + entityDetails.project_id, + entityDetails.cycle_id ?? "", + entityDetails.id + ); + } + } catch { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: `${entityDetails.is_epic ? "Epic" : "Work item"} could not be updated. Please try again.`, + }); + } + }, + modifierShortcut: "shift+c", + isEnabled: () => Boolean(projectDetails?.cycle_view && isEditingAllowed), + isVisible: () => Boolean(projectDetails?.cycle_view && isEditingAllowed), + closeOnSelect: true, + }, + { + id: "add_work_item_to_modules", + i18n_title: "power_k.contextual_actions.work_item.add_to_modules", + icon: DiceIcon, + group: "contextual", + contextType: "work-item", + type: "change-page", + page: "update-work-item-module", + onSelect: (data) => { + const moduleId = (data as IModule)?.id; + if (!workspaceSlug || !entityDetails || !entityDetails.project_id) return; + // handlers + const changeModulesInEntity = entityDetails.is_epic ? changeModulesInEpic : changeModulesInIssue; + try { + if (entityDetails.module_ids?.includes(moduleId)) { + changeModulesInEntity(workspaceSlug.toString(), entityDetails.project_id, entityDetails.id, [], [moduleId]); + } else { + changeModulesInEntity(workspaceSlug.toString(), entityDetails.project_id, entityDetails.id, [moduleId], []); + } + } catch { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: `${entityDetails.is_epic ? "Epic" : "Work item"} could not be updated. Please try again.`, + }); + } + }, + modifierShortcut: "shift+m", + isEnabled: () => Boolean(projectDetails?.module_view && isEditingAllowed), + isVisible: () => Boolean(projectDetails?.module_view && isEditingAllowed), + closeOnSelect: false, + }, + { + id: "add_work_item_labels", + i18n_title: "power_k.contextual_actions.work_item.add_labels", + icon: TagIcon, + group: "contextual", + contextType: "work-item", + type: "change-page", + page: "update-work-item-labels", + onSelect: (data) => { + const labelId = (data as IIssueLabel)?.id; + if (!workspaceSlug || !entityDetails || !entityDetails.project_id) return; + const updatedLabels = [...(entityDetails.label_ids ?? [])]; + if (updatedLabels.includes(labelId)) updatedLabels.splice(updatedLabels.indexOf(labelId), 1); + else updatedLabels.push(labelId); + handleUpdateEntity({ + label_ids: updatedLabels, + }); + }, + shortcut: "l", + isEnabled: () => isEditingAllowed, + isVisible: () => isEditingAllowed, + closeOnSelect: false, + }, + { + id: "subscribe_work_item", + i18n_title: isSubscribed + ? "power_k.contextual_actions.work_item.unsubscribe" + : "power_k.contextual_actions.work_item.subscribe", + icon: isSubscribed ? BellOff : Bell, + group: "contextual", + contextType: "work-item", + type: "action", + action: handleSubscription, + modifierShortcut: "shift+s", + isEnabled: () => isEditingAllowed, + isVisible: () => isEditingAllowed, + closeOnSelect: true, + }, + { + id: "delete_work_item", + i18n_title: "power_k.contextual_actions.work_item.delete", + icon: Trash2, + group: "contextual", + contextType: "work-item", + type: "action", + action: handleDeleteWorkItem, + modifierShortcut: "cmd+backspace", + isEnabled: () => isEditingAllowed, + isVisible: () => isEditingAllowed, + closeOnSelect: true, + }, + { + id: "copy_work_item_id", + i18n_title: "power_k.contextual_actions.work_item.copy_id", + icon: TicketCheck, + group: "contextual", + contextType: "work-item", + type: "action", + action: copyWorkItemIdToClipboard, + modifierShortcut: "cmd+.", + isEnabled: () => true, + isVisible: () => true, + closeOnSelect: true, + }, + { + id: "copy_work_item_title", + i18n_title: "power_k.contextual_actions.work_item.copy_title", + icon: Type, + group: "contextual", + contextType: "work-item", + type: "action", + action: copyWorkItemTitleToClipboard, + modifierShortcut: "cmd+shift+'", + isEnabled: () => true, + isVisible: () => true, + closeOnSelect: true, + }, + { + id: "copy_work_item_url", + i18n_title: "power_k.contextual_actions.work_item.copy_url", + icon: LinkIcon, + group: "contextual", + contextType: "work-item", + type: "action", + action: copyWorkItemUrlToClipboard, + modifierShortcut: "cmd+shift+,", + isEnabled: () => true, + isVisible: () => true, + closeOnSelect: true, + }, + ]; +}; diff --git a/apps/web/core/components/power-k/ui/pages/context-based/work-item/cycles-menu.tsx b/apps/web/core/components/power-k/ui/pages/context-based/work-item/cycles-menu.tsx new file mode 100644 index 000000000..39389f980 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/work-item/cycles-menu.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { observer } from "mobx-react"; +// plane types +import type { ICycle, TIssue } from "@plane/types"; +import { Spinner } from "@plane/ui"; +// components +import { PowerKCyclesMenu } from "@/components/power-k/menus/cycles"; +// hooks +import { useCycle } from "@/hooks/store/use-cycle"; + +type Props = { + handleSelect: (cycle: ICycle) => void; + workItemDetails: TIssue; +}; + +export const PowerKWorkItemCyclesMenu: React.FC = observer((props) => { + const { handleSelect, workItemDetails } = props; + // store hooks + const { getProjectCycleIds, getCycleById } = useCycle(); + // derived values + const projectCycleIds = workItemDetails.project_id ? getProjectCycleIds(workItemDetails.project_id) : undefined; + const cyclesList = projectCycleIds ? projectCycleIds.map((cycleId) => getCycleById(cycleId)) : undefined; + const filteredCyclesList = cyclesList ? cyclesList.filter((cycle) => !!cycle) : undefined; + + if (!filteredCyclesList) return ; + + return ; +}); diff --git a/apps/web/core/components/power-k/ui/pages/context-based/work-item/estimates-menu.tsx b/apps/web/core/components/power-k/ui/pages/context-based/work-item/estimates-menu.tsx new file mode 100644 index 000000000..58b3eeb2b --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/work-item/estimates-menu.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { Command } from "cmdk"; +import { observer } from "mobx-react"; +import { Triangle } from "lucide-react"; +// plane types +import { useTranslation } from "@plane/i18n"; +import { EEstimateSystem } from "@plane/types"; +import type { TIssue } from "@plane/types"; +import { Spinner } from "@plane/ui"; +import { convertMinutesToHoursMinutesString } from "@plane/utils"; +// hooks +import { useEstimate, useProjectEstimates } from "@/hooks/store/estimates"; +// local imports +import { PowerKModalCommandItem } from "../../../modal/command-item"; + +type Props = { + handleSelect: (estimatePointId: string | null) => void; + workItemDetails: TIssue; +}; + +export const PowerKWorkItemEstimatesMenu: React.FC = observer((props) => { + const { handleSelect, workItemDetails } = props; + // store hooks + const { currentActiveEstimateIdByProjectId, getEstimateById } = useProjectEstimates(); + const currentActiveEstimateId = workItemDetails.project_id + ? currentActiveEstimateIdByProjectId(workItemDetails.project_id) + : undefined; + const { estimatePointIds, estimatePointById } = useEstimate(currentActiveEstimateId); + // derived values + const currentActiveEstimate = currentActiveEstimateId ? getEstimateById(currentActiveEstimateId) : undefined; + // translation + const { t } = useTranslation(); + + if (!estimatePointIds) return ; + + return ( + + handleSelect(null)} + /> + {estimatePointIds.length > 0 ? ( + estimatePointIds.map((estimatePointId) => { + const estimatePoint = estimatePointById(estimatePointId); + if (!estimatePoint) return null; + + return ( + handleSelect(estimatePoint.id ?? null)} + /> + ); + }) + ) : ( +
No estimate found
+ )} +
+ ); +}); diff --git a/apps/web/core/components/power-k/ui/pages/context-based/work-item/index.ts b/apps/web/core/components/power-k/ui/pages/context-based/work-item/index.ts new file mode 100644 index 000000000..1efe34c51 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/work-item/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/apps/web/core/components/power-k/ui/pages/context-based/work-item/labels-menu.tsx b/apps/web/core/components/power-k/ui/pages/context-based/work-item/labels-menu.tsx new file mode 100644 index 000000000..c8e51991f --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/work-item/labels-menu.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { observer } from "mobx-react"; +// plane types +import type { IIssueLabel, TIssue } from "@plane/types"; +import { Spinner } from "@plane/ui"; +// components +import { PowerKLabelsMenu } from "@/components/power-k/menus/labels"; +// hooks +import { useLabel } from "@/hooks/store/use-label"; + +type Props = { + handleSelect: (label: IIssueLabel) => void; + workItemDetails: TIssue; +}; + +export const PowerKWorkItemLabelsMenu: React.FC = observer((props) => { + const { handleSelect, workItemDetails } = props; + // store hooks + const { getProjectLabelIds, getLabelById } = useLabel(); + // derived values + const projectLabelIds = workItemDetails.project_id ? getProjectLabelIds(workItemDetails.project_id) : undefined; + const labelsList = projectLabelIds ? projectLabelIds.map((labelId) => getLabelById(labelId)) : undefined; + const filteredLabelsList = labelsList ? labelsList.filter((label) => !!label) : undefined; + + if (!filteredLabelsList) return ; + + return ; +}); diff --git a/apps/web/core/components/power-k/ui/pages/context-based/work-item/modules-menu.tsx b/apps/web/core/components/power-k/ui/pages/context-based/work-item/modules-menu.tsx new file mode 100644 index 000000000..5c46463e5 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/work-item/modules-menu.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { observer } from "mobx-react"; +// plane types +import type { IModule, TIssue } from "@plane/types"; +import { Spinner } from "@plane/ui"; +// components +import { PowerKModulesMenu } from "@/components/power-k/menus/modules"; +// hooks +import { useModule } from "@/hooks/store/use-module"; + +type Props = { + handleSelect: (module: IModule) => void; + workItemDetails: TIssue; +}; + +export const PowerKWorkItemModulesMenu: React.FC = observer((props) => { + const { handleSelect, workItemDetails } = props; + // store hooks + const { getProjectModuleIds, getModuleById } = useModule(); + // derived values + const projectModuleIds = workItemDetails.project_id ? getProjectModuleIds(workItemDetails.project_id) : undefined; + const modulesList = projectModuleIds ? projectModuleIds.map((moduleId) => getModuleById(moduleId)) : undefined; + const filteredModulesList = modulesList ? modulesList.filter((module) => !!module) : undefined; + + if (!filteredModulesList) return ; + + return ( + + ); +}); diff --git a/apps/web/core/components/power-k/ui/pages/context-based/work-item/priorities-menu.tsx b/apps/web/core/components/power-k/ui/pages/context-based/work-item/priorities-menu.tsx new file mode 100644 index 000000000..ff33f0b59 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/work-item/priorities-menu.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { Command } from "cmdk"; +import { observer } from "mobx-react"; +// plane imports +import { ISSUE_PRIORITIES } from "@plane/constants"; +import { PriorityIcon } from "@plane/propel/icons"; +import type { TIssue, TIssuePriorities } from "@plane/types"; +// local imports +import { PowerKModalCommandItem } from "../../../modal/command-item"; + +type Props = { + handleSelect: (priority: TIssuePriorities) => void; + workItemDetails: TIssue; +}; + +export const PowerKWorkItemPrioritiesMenu: React.FC = observer((props) => { + const { handleSelect, workItemDetails } = props; + + return ( + + {ISSUE_PRIORITIES.map((priority) => ( + } + label={priority.title} + isSelected={priority.key === workItemDetails.priority} + onSelect={() => handleSelect(priority.key)} + /> + ))} + + ); +}); diff --git a/apps/web/core/components/power-k/ui/pages/context-based/work-item/root.tsx b/apps/web/core/components/power-k/ui/pages/context-based/work-item/root.tsx new file mode 100644 index 000000000..ad8149036 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/work-item/root.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +// plane imports +import { EIssueServiceType } from "@plane/types"; +// components +import type { TPowerKPageType } from "@/components/power-k/core/types"; +// hooks +import { useIssueDetail } from "@/hooks/store/use-issue-detail"; +import { useMember } from "@/hooks/store/use-member"; +// local imports +import { PowerKMembersMenu } from "../../../../menus/members"; +import { PowerKWorkItemCyclesMenu } from "./cycles-menu"; +import { PowerKWorkItemEstimatesMenu } from "./estimates-menu"; +import { PowerKWorkItemLabelsMenu } from "./labels-menu"; +import { PowerKWorkItemModulesMenu } from "./modules-menu"; +import { PowerKWorkItemPrioritiesMenu } from "./priorities-menu"; +import { PowerKProjectStatesMenu } from "./states-menu"; + +type Props = { + activePage: TPowerKPageType | null; + handleSelection: (data: unknown) => void; +}; + +export const PowerKWorkItemContextBasedPages: React.FC = observer((props) => { + const { activePage, handleSelection } = props; + // navigation + const { workItem: entityIdentifier } = useParams(); + // store hooks + const { + issue: { getIssueById, getIssueIdByIdentifier }, + } = useIssueDetail(EIssueServiceType.ISSUES); + const { + project: { getProjectMemberIds }, + } = useMember(); + // derived values + const entityId = entityIdentifier ? getIssueIdByIdentifier(entityIdentifier.toString()) : null; + const entityDetails = entityId ? getIssueById(entityId) : null; + const projectMemberIds = entityDetails?.project_id ? getProjectMemberIds(entityDetails.project_id, false) : []; + + if (!entityDetails) return null; + + return ( + <> + {/* states menu */} + {activePage === "update-work-item-state" && ( + + )} + {/* priority menu */} + {activePage === "update-work-item-priority" && ( + + )} + {/* members menu */} + {activePage === "update-work-item-assignee" && ( + + )} + {/* estimates menu */} + {activePage === "update-work-item-estimate" && ( + + )} + {/* cycles menu */} + {activePage === "update-work-item-cycle" && ( + + )} + {/* modules menu */} + {activePage === "update-work-item-module" && ( + + )} + {/* labels menu */} + {activePage === "update-work-item-labels" && ( + + )} + + ); +}); diff --git a/apps/web/core/components/power-k/ui/pages/context-based/work-item/states-menu.tsx b/apps/web/core/components/power-k/ui/pages/context-based/work-item/states-menu.tsx new file mode 100644 index 000000000..835cae3ab --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/context-based/work-item/states-menu.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { Command } from "cmdk"; +import { observer } from "mobx-react"; +// plane types +import { useParams } from "next/navigation"; +import type { TIssue } from "@plane/types"; +import { Spinner } from "@plane/ui"; +// hooks +import { useProjectState } from "@/hooks/store/use-project-state"; +// local imports +import { PowerKProjectStatesMenuItems } from "@/plane-web/components/command-palette/power-k/pages/context-based/work-item/state-menu-item"; + +type Props = { + handleSelect: (stateId: string) => void; + workItemDetails: TIssue; +}; + +export const PowerKProjectStatesMenu: React.FC = observer((props) => { + const { workItemDetails } = props; + // router + const { workspaceSlug } = useParams(); + // store hooks + const { getProjectStateIds, getStateById } = useProjectState(); + // derived values + const projectStateIds = workItemDetails.project_id ? getProjectStateIds(workItemDetails.project_id) : undefined; + const projectStates = projectStateIds ? projectStateIds.map((stateId) => getStateById(stateId)) : undefined; + const filteredProjectStates = projectStates ? projectStates.filter((state) => !!state) : undefined; + + if (!filteredProjectStates) return ; + + return ( + + + + ); +}); diff --git a/apps/web/core/components/power-k/ui/pages/default.tsx b/apps/web/core/components/power-k/ui/pages/default.tsx new file mode 100644 index 000000000..eebfe22c8 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/default.tsx @@ -0,0 +1,23 @@ +"use client"; + +import React from "react"; +// hooks +import { usePowerK } from "@/hooks/store/use-power-k"; +// local imports +import type { TPowerKCommandConfig, TPowerKContext } from "../../core/types"; +import { CommandRenderer } from "../renderer/command"; + +type Props = { + context: TPowerKContext; + onCommandSelect: (command: TPowerKCommandConfig) => void; +}; + +export const PowerKModalDefaultPage: React.FC = (props) => { + const { context, onCommandSelect } = props; + // store hooks + const { commandRegistry } = usePowerK(); + // Get commands to display + const commands = commandRegistry.getVisibleCommands(context); + + return ; +}; diff --git a/apps/web/core/components/power-k/ui/pages/index.ts b/apps/web/core/components/power-k/ui/pages/index.ts new file mode 100644 index 000000000..1efe34c51 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/apps/web/core/components/power-k/ui/pages/open-entity/project-cycles-menu.tsx b/apps/web/core/components/power-k/ui/pages/open-entity/project-cycles-menu.tsx new file mode 100644 index 000000000..a8c17a176 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/open-entity/project-cycles-menu.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { observer } from "mobx-react"; +// plane types +import type { ICycle } from "@plane/types"; +import { Spinner } from "@plane/ui"; +// components +import type { TPowerKContext } from "@/components/power-k/core/types"; +import { PowerKCyclesMenu } from "@/components/power-k/menus/cycles"; +// hooks +import { useCycle } from "@/hooks/store/use-cycle"; + +type Props = { + context: TPowerKContext; + handleSelect: (cycle: ICycle) => void; +}; + +export const PowerKOpenProjectCyclesMenu: React.FC = observer((props) => { + const { context, handleSelect } = props; + // store hooks + const { fetchedMap, getProjectCycleIds, getCycleById } = useCycle(); + // derived values + const projectId = context.params.projectId?.toString(); + const isFetched = projectId ? fetchedMap[projectId] : false; + const projectCycleIds = projectId ? getProjectCycleIds(projectId) : undefined; + const cyclesList = projectCycleIds + ? projectCycleIds.map((cycleId) => getCycleById(cycleId)).filter((cycle) => !!cycle) + : []; + + if (!isFetched) return ; + + return ; +}); diff --git a/apps/web/core/components/power-k/ui/pages/open-entity/project-modules-menu.tsx b/apps/web/core/components/power-k/ui/pages/open-entity/project-modules-menu.tsx new file mode 100644 index 000000000..af3ce8020 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/open-entity/project-modules-menu.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { observer } from "mobx-react"; +// plane types +import type { IModule } from "@plane/types"; +import { Spinner } from "@plane/ui"; +// components +import type { TPowerKContext } from "@/components/power-k/core/types"; +import { PowerKModulesMenu } from "@/components/power-k/menus/modules"; +// hooks +import { useModule } from "@/hooks/store/use-module"; + +type Props = { + context: TPowerKContext; + handleSelect: (module: IModule) => void; +}; + +export const PowerKOpenProjectModulesMenu: React.FC = observer((props) => { + const { context, handleSelect } = props; + // store hooks + const { fetchedMap, getProjectModuleIds, getModuleById } = useModule(); + // derived values + const projectId = context.params.projectId?.toString(); + const isFetched = projectId ? fetchedMap[projectId] : false; + const projectModuleIds = projectId ? getProjectModuleIds(projectId) : undefined; + const modulesList = projectModuleIds + ? projectModuleIds.map((moduleId) => getModuleById(moduleId)).filter((module) => !!module) + : []; + + if (!isFetched) return ; + + return ; +}); diff --git a/apps/web/core/components/power-k/ui/pages/open-entity/project-settings-menu.tsx b/apps/web/core/components/power-k/ui/pages/open-entity/project-settings-menu.tsx new file mode 100644 index 000000000..e7ce54035 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/open-entity/project-settings-menu.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { observer } from "mobx-react"; +// plane types +import { EUserPermissionsLevel } from "@plane/constants"; +// components +import { useTranslation } from "@plane/i18n"; +import type { TPowerKContext } from "@/components/power-k/core/types"; +import { PowerKSettingsMenu } from "@/components/power-k/menus/settings"; +// hooks +import { useUserPermissions } from "@/hooks/store/user"; +import { PROJECT_SETTINGS } from "@/plane-web/constants/project"; + +type Props = { + context: TPowerKContext; + handleSelect: (href: string) => void; +}; + +export const PowerKOpenProjectSettingsMenu: React.FC = observer((props) => { + const { context, handleSelect } = props; + // plane hooks + const { t } = useTranslation(); + // store hooks + const { allowPermissions } = useUserPermissions(); + // derived values + const settingsList = Object.values(PROJECT_SETTINGS).filter( + (setting) => + context.params.workspaceSlug && + context.params.projectId && + allowPermissions( + setting.access, + EUserPermissionsLevel.PROJECT, + context.params.workspaceSlug?.toString(), + context.params.projectId?.toString() + ) + ); + const settingsListWithIcons = settingsList.map((setting) => ({ + ...setting, + label: t(setting.i18n_label), + icon: setting.Icon, + })); + + return handleSelect(setting.href)} />; +}); diff --git a/apps/web/core/components/power-k/ui/pages/open-entity/project-views-menu.tsx b/apps/web/core/components/power-k/ui/pages/open-entity/project-views-menu.tsx new file mode 100644 index 000000000..a60fb316e --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/open-entity/project-views-menu.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { observer } from "mobx-react"; +// plane types +import type { IProjectView } from "@plane/types"; +import { Spinner } from "@plane/ui"; +// components +import type { TPowerKContext } from "@/components/power-k/core/types"; +// hooks +import { PowerKViewsMenu } from "@/components/power-k/menus/views"; +import { useProjectView } from "@/hooks/store/use-project-view"; + +type Props = { + context: TPowerKContext; + handleSelect: (view: IProjectView) => void; +}; + +export const PowerKOpenProjectViewsMenu: React.FC = observer((props) => { + const { context, handleSelect } = props; + // store hooks + const { fetchedMap, getProjectViews } = useProjectView(); + // derived values + const projectId = context.params.projectId?.toString(); + const isFetched = projectId ? fetchedMap[projectId] : false; + const viewsList = projectId ? (getProjectViews(projectId)?.filter((view) => !!view) ?? []) : []; + + if (!isFetched) return ; + + return ; +}); diff --git a/apps/web/core/components/power-k/ui/pages/open-entity/projects-menu.tsx b/apps/web/core/components/power-k/ui/pages/open-entity/projects-menu.tsx new file mode 100644 index 000000000..e11aac6f1 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/open-entity/projects-menu.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { observer } from "mobx-react"; +// plane types +import type { IPartialProject } from "@plane/types"; +import { Spinner } from "@plane/ui"; +// components +import { PowerKProjectsMenu } from "@/components/power-k/menus/projects"; +// hooks +import { useProject } from "@/hooks/store/use-project"; + +type Props = { + handleSelect: (project: IPartialProject) => void; +}; + +export const PowerKOpenProjectMenu: React.FC = observer((props) => { + const { handleSelect } = props; + // store hooks + const { loader, joinedProjectIds, getPartialProjectById } = useProject(); + // derived values + const projectsList = joinedProjectIds + ? joinedProjectIds.map((id) => getPartialProjectById(id)).filter((project) => project !== undefined) + : []; + + if (loader === "init-loader") return ; + + return ; +}); diff --git a/apps/web/core/components/power-k/ui/pages/open-entity/root.tsx b/apps/web/core/components/power-k/ui/pages/open-entity/root.tsx new file mode 100644 index 000000000..a823d34ee --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/open-entity/root.tsx @@ -0,0 +1,35 @@ +// local imports +import { PowerKOpenProjectCyclesMenu } from "./project-cycles-menu"; +import { PowerKOpenProjectModulesMenu } from "./project-modules-menu"; +import { PowerKOpenProjectSettingsMenu } from "./project-settings-menu"; +import { PowerKOpenProjectViewsMenu } from "./project-views-menu"; +import { PowerKOpenProjectMenu } from "./projects-menu"; +import type { TPowerKOpenEntityActionsProps } from "./shared"; +import { PowerKOpenWorkspaceSettingsMenu } from "./workspace-settings-menu"; +import { PowerKOpenWorkspaceMenu } from "./workspaces-menu"; + +export const PowerKOpenEntityPages: React.FC = (props) => { + const { activePage, context, handleSelection } = props; + + return ( + <> + {activePage === "open-workspace" && } + {activePage === "open-project" && } + {activePage === "open-workspace-setting" && ( + + )} + {activePage === "open-project-setting" && ( + + )} + {activePage === "open-project-cycle" && ( + + )} + {activePage === "open-project-module" && ( + + )} + {activePage === "open-project-view" && ( + + )} + + ); +}; diff --git a/apps/web/core/components/power-k/ui/pages/open-entity/shared.ts b/apps/web/core/components/power-k/ui/pages/open-entity/shared.ts new file mode 100644 index 000000000..93fb40614 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/open-entity/shared.ts @@ -0,0 +1,8 @@ +// local imports +import type { TPowerKContext, TPowerKPageType } from "@/components/power-k/core/types"; + +export type TPowerKOpenEntityActionsProps = { + activePage: TPowerKPageType | null; + context: TPowerKContext; + handleSelection: (data: unknown) => void; +}; diff --git a/apps/web/core/components/power-k/ui/pages/open-entity/workspace-settings-menu.tsx b/apps/web/core/components/power-k/ui/pages/open-entity/workspace-settings-menu.tsx new file mode 100644 index 000000000..666336e5a --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/open-entity/workspace-settings-menu.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { WORKSPACE_SETTINGS_ICONS } from "app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/sidebar"; +import { observer } from "mobx-react"; +// plane types +import { EUserPermissionsLevel, WORKSPACE_SETTINGS } from "@plane/constants"; +// components +import { useTranslation } from "@plane/i18n"; +import type { TPowerKContext } from "@/components/power-k/core/types"; +import { PowerKSettingsMenu } from "@/components/power-k/menus/settings"; +// hooks +import { useUserPermissions } from "@/hooks/store/user"; +import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper"; + +type Props = { + context: TPowerKContext; + handleSelect: (href: string) => void; +}; + +export const PowerKOpenWorkspaceSettingsMenu: React.FC = observer((props) => { + const { context, handleSelect } = props; + // plane hooks + const { t } = useTranslation(); + // store hooks + const { allowPermissions } = useUserPermissions(); + // derived values + const settingsList = Object.values(WORKSPACE_SETTINGS).filter( + (setting) => + context.params.workspaceSlug && + shouldRenderSettingLink(context.params.workspaceSlug?.toString(), setting.key) && + allowPermissions(setting.access, EUserPermissionsLevel.WORKSPACE, context.params.workspaceSlug?.toString()) + ); + const settingsListWithIcons = settingsList.map((setting) => ({ + ...setting, + label: t(setting.i18n_label), + icon: WORKSPACE_SETTINGS_ICONS[setting.key as keyof typeof WORKSPACE_SETTINGS_ICONS], + })); + + return handleSelect(setting.href)} />; +}); diff --git a/apps/web/core/components/power-k/ui/pages/open-entity/workspaces-menu.tsx b/apps/web/core/components/power-k/ui/pages/open-entity/workspaces-menu.tsx new file mode 100644 index 000000000..a5b315254 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/open-entity/workspaces-menu.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { observer } from "mobx-react"; +// plane types +import type { IWorkspace } from "@plane/types"; +import { Spinner } from "@plane/ui"; +// components +import { PowerKWorkspacesMenu } from "@/components/power-k/menus/workspaces"; +// hooks +import { useWorkspace } from "@/hooks/store/use-workspace"; + +type Props = { + handleSelect: (workspace: IWorkspace) => void; +}; + +export const PowerKOpenWorkspaceMenu: React.FC = observer((props) => { + const { handleSelect } = props; + // store hooks + const { loader, workspaces } = useWorkspace(); + // derived values + const workspacesList = workspaces ? Object.values(workspaces) : []; + + if (loader) return ; + + return ; +}); diff --git a/apps/web/core/components/power-k/ui/pages/preferences/index.ts b/apps/web/core/components/power-k/ui/pages/preferences/index.ts new file mode 100644 index 000000000..1efe34c51 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/preferences/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/apps/web/core/components/power-k/ui/pages/preferences/languages-menu.tsx b/apps/web/core/components/power-k/ui/pages/preferences/languages-menu.tsx new file mode 100644 index 000000000..d5ca6556e --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/preferences/languages-menu.tsx @@ -0,0 +1,25 @@ +"use client"; + +import React from "react"; +import { Command } from "cmdk"; +import { observer } from "mobx-react"; +// plane imports +import { SUPPORTED_LANGUAGES } from "@plane/i18n"; +// local imports +import { PowerKModalCommandItem } from "../../modal/command-item"; + +type Props = { + onSelect: (language: string) => void; +}; + +export const PowerKPreferencesLanguagesMenu: React.FC = observer((props) => { + const { onSelect } = props; + + return ( + + {SUPPORTED_LANGUAGES.map((language) => ( + onSelect(language.value)} label={language.label} /> + ))} + + ); +}); diff --git a/apps/web/core/components/power-k/ui/pages/preferences/root.tsx b/apps/web/core/components/power-k/ui/pages/preferences/root.tsx new file mode 100644 index 000000000..e183b2109 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/preferences/root.tsx @@ -0,0 +1,29 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react"; +// components +import type { TPowerKPageType } from "@/components/power-k/core/types"; +// local imports +import { PowerKPreferencesLanguagesMenu } from "./languages-menu"; +import { PowerKPreferencesStartOfWeekMenu } from "./start-of-week-menu"; +import { PowerKPreferencesThemesMenu } from "./themes-menu"; +import { PowerKPreferencesTimezonesMenu } from "./timezone-menu"; + +type Props = { + activePage: TPowerKPageType | null; + handleSelection: (data: unknown) => void; +}; + +export const PowerKAccountPreferencesPages: React.FC = observer((props) => { + const { activePage, handleSelection } = props; + + return ( + <> + {activePage === "update-theme" && } + {activePage === "update-timezone" && } + {activePage === "update-start-of-week" && } + {activePage === "update-language" && } + + ); +}); diff --git a/apps/web/core/components/power-k/ui/pages/preferences/start-of-week-menu.tsx b/apps/web/core/components/power-k/ui/pages/preferences/start-of-week-menu.tsx new file mode 100644 index 000000000..40b349458 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/preferences/start-of-week-menu.tsx @@ -0,0 +1,25 @@ +"use client"; + +import React from "react"; +import { Command } from "cmdk"; +// plane imports +import { START_OF_THE_WEEK_OPTIONS } from "@plane/constants"; +import type { EStartOfTheWeek } from "@plane/types"; +// local imports +import { PowerKModalCommandItem } from "../../modal/command-item"; + +type Props = { + onSelect: (day: EStartOfTheWeek) => void; +}; + +export const PowerKPreferencesStartOfWeekMenu: React.FC = (props) => { + const { onSelect } = props; + + return ( + + {START_OF_THE_WEEK_OPTIONS.map((day) => ( + onSelect(day.value)} label={day.label} /> + ))} + + ); +}; diff --git a/apps/web/core/components/power-k/ui/pages/preferences/themes-menu.tsx b/apps/web/core/components/power-k/ui/pages/preferences/themes-menu.tsx new file mode 100644 index 000000000..1519c337d --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/preferences/themes-menu.tsx @@ -0,0 +1,37 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { Command } from "cmdk"; +import { observer } from "mobx-react"; +// plane imports +import { THEME_OPTIONS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +// local imports +import { PowerKModalCommandItem } from "../../modal/command-item"; + +type Props = { + onSelect: (theme: string) => void; +}; + +export const PowerKPreferencesThemesMenu: React.FC = observer((props) => { + const { onSelect } = props; + // hooks + const { t } = useTranslation(); + // states + const [mounted, setMounted] = useState(false); + + // useEffect only runs on the client, so now we can safely show the UI + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + return ( + + {THEME_OPTIONS.map((theme) => ( + onSelect(theme.value)} label={t(theme.i18n_label)} /> + ))} + + ); +}); diff --git a/apps/web/core/components/power-k/ui/pages/preferences/timezone-menu.tsx b/apps/web/core/components/power-k/ui/pages/preferences/timezone-menu.tsx new file mode 100644 index 000000000..53b068d51 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/preferences/timezone-menu.tsx @@ -0,0 +1,31 @@ +"use client"; + +import React from "react"; +import { Command } from "cmdk"; +import { observer } from "mobx-react"; +// hooks +import useTimezone from "@/hooks/use-timezone"; +// local imports +import { PowerKModalCommandItem } from "../../modal/command-item"; + +type Props = { + onSelect: (timezone: string) => void; +}; + +export const PowerKPreferencesTimezonesMenu: React.FC = observer((props) => { + const { onSelect } = props; + // timezones + const { timezones } = useTimezone(); + + return ( + + {timezones.map((timezone) => ( + onSelect(timezone.value)} + label={timezone.content} + /> + ))} + + ); +}); diff --git a/apps/web/core/components/power-k/ui/pages/root.tsx b/apps/web/core/components/power-k/ui/pages/root.tsx new file mode 100644 index 000000000..66bb97bb0 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/root.tsx @@ -0,0 +1,32 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react"; +// local imports +import type { TPowerKCommandConfig, TPowerKContext, TPowerKPageType } from "../../core/types"; +import { PowerKModalDefaultPage } from "./default"; +import { PowerKOpenEntityPages } from "./open-entity/root"; +import { PowerKAccountPreferencesPages } from "./preferences"; + +type Props = { + activePage: TPowerKPageType | null; + context: TPowerKContext; + onCommandSelect: (command: TPowerKCommandConfig) => void; + onPageDataSelect: (value: unknown) => void; +}; + +export const PowerKModalPagesList: React.FC = observer((props) => { + const { activePage, context, onCommandSelect, onPageDataSelect } = props; + + // Main page content (no specific page) + if (!activePage) { + return ; + } + + return ( + <> + + + + ); +}); diff --git a/apps/web/core/components/power-k/ui/pages/work-item-selection-page.tsx b/apps/web/core/components/power-k/ui/pages/work-item-selection-page.tsx new file mode 100644 index 000000000..6b3715e39 --- /dev/null +++ b/apps/web/core/components/power-k/ui/pages/work-item-selection-page.tsx @@ -0,0 +1,163 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +// plane imports +// import { useTranslation } from "@plane/i18n"; +import type { TIssueEntityData, TIssueSearchResponse, TActivityEntityData } from "@plane/types"; +// import { generateWorkItemLink } from "@plane/utils"; +// components +// import { CommandPaletteEntityList } from "@/components/command-palette"; +// import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root"; +// hooks +// import { useCommandPalette } from "@/hooks/store/use-command-palette"; +// import { usePowerK } from "@/hooks/store/use-power-k"; +// import { useAppRouter } from "@/hooks/use-app-router"; +// plane web imports +// import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier"; +import { WorkspaceService } from "@/plane-web/services"; + +const workspaceService = new WorkspaceService(); + +type Props = { + workspaceSlug: string | undefined; + projectId: string | undefined; + searchTerm: string; + debouncedSearchTerm: string; + isLoading: boolean; + isSearching: boolean; + resolvedPath: string; + isWorkspaceLevel?: boolean; +}; + +export const WorkItemSelectionPage: React.FC = (props) => { + const { workspaceSlug, projectId, debouncedSearchTerm, isWorkspaceLevel = false } = props; + // router + // const router = useAppRouter(); + // plane hooks + // const { t } = useTranslation(); + // store hooks + // const { togglePowerKModal } = usePowerK(); + // states + const [_recentIssues, setRecentIssues] = useState([]); + const [_issueResults, setIssueResults] = useState([]); + + // Load recent issues when component mounts + useEffect(() => { + if (!workspaceSlug) return; + + workspaceService + .fetchWorkspaceRecents(workspaceSlug.toString(), "issue") + .then((res) => + setRecentIssues(res.map((r: TActivityEntityData) => r.entity_data as TIssueEntityData).slice(0, 10)) + ) + .catch(() => setRecentIssues([])); + }, [workspaceSlug]); + + // Search issues based on search term + useEffect(() => { + if (!workspaceSlug || !debouncedSearchTerm) { + setIssueResults([]); + return; + } + + workspaceService + .searchEntity(workspaceSlug.toString(), { + count: 10, + query: debouncedSearchTerm, + query_type: ["issue"], + ...(!isWorkspaceLevel && projectId ? { project_id: projectId.toString() } : {}), + }) + .then((res) => { + setIssueResults(res.issue || []); + }) + .catch(() => setIssueResults([])); + }, [debouncedSearchTerm, workspaceSlug, projectId, isWorkspaceLevel]); + + if (!workspaceSlug) return null; + + return ( + <> + {/* {searchTerm === "" ? ( + recentIssues.length > 0 ? ( + issue.id} + getLabel={(issue) => `${issue.project_identifier}-${issue.sequence_id} ${issue.name}`} + renderItem={(issue) => ( +
+ {issue.project_id && ( + + )} + {issue.name} +
+ )} + onSelect={(issue) => { + if (!issue.project_id) return; + togglePowerKModal(false); + router.push( + generateWorkItemLink({ + workspaceSlug: workspaceSlug.toString(), + projectId: issue.project_id, + issueId: issue.id, + projectIdentifier: issue.project_identifier, + sequenceId: issue.sequence_id, + isEpic: issue.is_epic, + }) + ); + }} + emptyText="Search for issue id or issue title" + /> + ) : ( +
Search for issue id or issue title
+ ) + ) : issueResults.length > 0 ? ( + issue.id} + getLabel={(issue) => `${issue.project__identifier}-${issue.sequence_id} ${issue.name}`} + renderItem={(issue) => ( +
+ {issue.project_id && issue.project__identifier && issue.sequence_id && ( + + )} + {issue.name} +
+ )} + onSelect={(issue) => { + if (!issue.project_id) return; + togglePowerKModal(false); + router.push( + generateWorkItemLink({ + workspaceSlug: workspaceSlug.toString(), + projectId: issue.project_id, + issueId: issue.id, + projectIdentifier: issue.project__identifier, + sequenceId: issue.sequence_id, + }) + ); + }} + emptyText={t("command_k.empty_state.search.title") as string} + /> + ) : ( + !isLoading && + !isSearching && ( +
+ +
+ ) + )} */} + + ); +}; diff --git a/apps/web/core/components/power-k/ui/renderer/command.tsx b/apps/web/core/components/power-k/ui/renderer/command.tsx new file mode 100644 index 000000000..af153fcd4 --- /dev/null +++ b/apps/web/core/components/power-k/ui/renderer/command.tsx @@ -0,0 +1,71 @@ +"use client"; + +import React from "react"; +import { Command } from "cmdk"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// local imports +import type { TPowerKCommandConfig, TPowerKCommandGroup, TPowerKContext } from "../../core/types"; +import { PowerKModalCommandItem } from "../modal/command-item"; +import { CONTEXT_ENTITY_MAP } from "../pages/context-based"; +import { POWER_K_GROUP_PRIORITY, POWER_K_GROUP_I18N_TITLES } from "./shared"; + +type Props = { + commands: TPowerKCommandConfig[]; + context: TPowerKContext; + onCommandSelect: (command: TPowerKCommandConfig) => void; +}; + +export const CommandRenderer: React.FC = (props) => { + const { commands, context, onCommandSelect } = props; + // derived values + const { activeContext } = context; + // translation + const { t } = useTranslation(); + + const commandsByGroup = commands.reduce( + (acc, command) => { + const group = command.group || "general"; + if (!acc[group]) acc[group] = []; + acc[group].push(command); + return acc; + }, + {} as Record + ); + + const sortedGroups = Object.keys(commandsByGroup).sort((a, b) => { + const aPriority = POWER_K_GROUP_PRIORITY[a as TPowerKCommandGroup]; + const bPriority = POWER_K_GROUP_PRIORITY[b as TPowerKCommandGroup]; + return aPriority - bPriority; + }) as TPowerKCommandGroup[]; + + return ( + <> + {sortedGroups.map((groupKey) => { + const groupCommands = commandsByGroup[groupKey]; + if (!groupCommands || groupCommands.length === 0) return null; + + const title = + groupKey === "contextual" && activeContext + ? t(CONTEXT_ENTITY_MAP[activeContext].i18n_title) + : t(POWER_K_GROUP_I18N_TITLES[groupKey]); + + return ( + + {groupCommands.map((command) => ( + onCommandSelect(command)} + /> + ))} + + ); + })} + + ); +}; diff --git a/apps/web/core/components/power-k/ui/renderer/shared.ts b/apps/web/core/components/power-k/ui/renderer/shared.ts new file mode 100644 index 000000000..782752c43 --- /dev/null +++ b/apps/web/core/components/power-k/ui/renderer/shared.ts @@ -0,0 +1,25 @@ +import type { TPowerKCommandGroup } from "../../core/types"; + +export const POWER_K_GROUP_PRIORITY: Record = { + contextual: 1, + create: 2, + navigation: 3, + general: 7, + settings: 8, + account: 9, + miscellaneous: 10, + preferences: 11, + help: 12, +}; + +export const POWER_K_GROUP_I18N_TITLES: Record = { + contextual: "power_k.group_titles.contextual", + navigation: "power_k.group_titles.navigation", + create: "power_k.group_titles.create", + general: "power_k.group_titles.general", + settings: "power_k.group_titles.settings", + help: "power_k.group_titles.help", + account: "power_k.group_titles.account", + miscellaneous: "power_k.group_titles.miscellaneous", + preferences: "power_k.group_titles.preferences", +}; diff --git a/apps/web/core/components/power-k/ui/renderer/shortcut.tsx b/apps/web/core/components/power-k/ui/renderer/shortcut.tsx new file mode 100644 index 000000000..49caef90b --- /dev/null +++ b/apps/web/core/components/power-k/ui/renderer/shortcut.tsx @@ -0,0 +1,109 @@ +// plane imports +import { useTranslation } from "@plane/i18n"; +import { substringMatch } from "@plane/utils"; +// components +import type { TPowerKCommandConfig, TPowerKCommandGroup } from "@/components/power-k/core/types"; +import { KeySequenceBadge, ShortcutBadge } from "@/components/power-k/ui/modal/command-item-shortcut-badge"; +// types +import { CONTEXT_ENTITY_MAP } from "@/components/power-k/ui/pages/context-based"; +// local imports +import { POWER_K_GROUP_I18N_TITLES, POWER_K_GROUP_PRIORITY } from "./shared"; + +type Props = { + searchQuery: string; + commands: TPowerKCommandConfig[]; +}; + +export const ShortcutRenderer: React.FC = (props) => { + const { searchQuery, commands } = props; + // translation + const { t } = useTranslation(); + + // Apply search filter + const filteredCommands = commands.filter((command) => substringMatch(t(command.i18n_title), searchQuery)); + + // Group commands - separate contextual by context type, others by group + type GroupedCommands = { + key: string; + title: string; + priority: number; + commands: TPowerKCommandConfig[]; + }; + + const groupedCommands: GroupedCommands[] = []; + + filteredCommands.forEach((command) => { + if (command.group === "contextual") { + // For contextual commands, group by context type + const contextKey = `contextual-${command.contextType}`; + let group = groupedCommands.find((g) => g.key === contextKey); + + if (!group) { + group = { + key: contextKey, + title: t(CONTEXT_ENTITY_MAP[command.contextType].i18n_title), + priority: POWER_K_GROUP_PRIORITY.contextual, + commands: [], + }; + groupedCommands.push(group); + } + group.commands.push(command); + } else { + // For other commands, group by command group + const groupKey = command.group || "general"; + let group = groupedCommands.find((g) => g.key === groupKey); + + if (!group) { + group = { + key: groupKey, + title: t(POWER_K_GROUP_I18N_TITLES[groupKey as TPowerKCommandGroup]), + priority: POWER_K_GROUP_PRIORITY[groupKey as TPowerKCommandGroup], + commands: [], + }; + groupedCommands.push(group); + } + group.commands.push(command); + } + }); + + // Sort groups by priority + groupedCommands.sort((a, b) => a.priority - b.priority); + + const isShortcutsEmpty = groupedCommands.length === 0; + + return ( +
+ {!isShortcutsEmpty ? ( + groupedCommands.map((group) => ( +
+
{group.title}
+
+ {group.commands.map((command) => ( +
+
+

{t(command.i18n_title)}

+
+ {command.keySequence && } + {(command.shortcut || command.modifierShortcut) && ( + + )} +
+
+
+ ))} +
+
+ )) + ) : ( +

+ No shortcuts found for{" "} + + {`"`} + {searchQuery} + {`"`} + +

+ )} +
+ ); +}; diff --git a/apps/web/core/components/power-k/utils/navigation.ts b/apps/web/core/components/power-k/utils/navigation.ts new file mode 100644 index 000000000..2c88823e3 --- /dev/null +++ b/apps/web/core/components/power-k/utils/navigation.ts @@ -0,0 +1,20 @@ +// plane imports +import { joinUrlPath } from "@plane/utils"; +// local imports +import type { TPowerKContext } from "../core/types"; + +export const handlePowerKNavigate = (context: TPowerKContext, routerSegments: (string | undefined)[]) => { + const validRouterSegments = routerSegments.filter((segment) => segment !== undefined); + + if (validRouterSegments.length === 0) { + console.warn("No valid router segments provided", routerSegments); + return; + } + + if (validRouterSegments.length !== routerSegments.length) { + console.warn("Some of the router segments are undefined", routerSegments); + } + + const route = joinUrlPath(...validRouterSegments); + context.router.push(route); +}; diff --git a/apps/web/core/components/workspace/sidebar/help-menu.tsx b/apps/web/core/components/workspace/sidebar/help-menu.tsx index eddcd8fc0..d2bb71506 100644 --- a/apps/web/core/components/workspace/sidebar/help-menu.tsx +++ b/apps/web/core/components/workspace/sidebar/help-menu.tsx @@ -13,8 +13,8 @@ import { cn } from "@plane/utils"; import { ProductUpdatesModal } from "@/components/global"; // helpers // hooks -import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useInstance } from "@/hooks/store/use-instance"; +import { usePowerK } from "@/hooks/store/use-power-k"; import { useTransient } from "@/hooks/store/use-transient"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components @@ -27,7 +27,7 @@ export interface WorkspaceHelpSectionProps { export const HelpMenu: React.FC = observer(() => { // store hooks const { t } = useTranslation(); - const { toggleShortcutModal } = useCommandPalette(); + const { toggleShortcutsListModal } = usePowerK(); const { isMobile } = usePlatformOS(); const { config } = useInstance(); const { isIntercomToggle, toggleIntercom } = useTransient(); @@ -95,7 +95,7 @@ export const HelpMenu: React.FC = observer(() => {
); diff --git a/apps/space/core/components/account/auth-forms/auth-banner.tsx b/apps/space/core/components/account/auth-forms/auth-banner.tsx index 30cd6e093..feda960e7 100644 --- a/apps/space/core/components/account/auth-forms/auth-banner.tsx +++ b/apps/space/core/components/account/auth-forms/auth-banner.tsx @@ -1,7 +1,8 @@ "use client"; import type { FC } from "react"; -import { Info, X } from "lucide-react"; +import { Info } from "lucide-react"; +import { CloseIcon } from "@plane/propel/icons"; // helpers import type { TAuthErrorInfo } from "@/helpers/authentication.helper"; @@ -24,7 +25,7 @@ export const AuthBanner: FC = (props) => { className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/80" onClick={() => handleBannerData && handleBannerData(undefined)} > - +
); diff --git a/apps/space/core/components/issues/filters/applied-filters/filters-list.tsx b/apps/space/core/components/issues/filters/applied-filters/filters-list.tsx index cb542face..bffc7c553 100644 --- a/apps/space/core/components/issues/filters/applied-filters/filters-list.tsx +++ b/apps/space/core/components/issues/filters/applied-filters/filters-list.tsx @@ -1,9 +1,9 @@ "use client"; import { observer } from "mobx-react"; -import { X } from "lucide-react"; -// types import { useTranslation } from "@plane/i18n"; +import { CloseIcon } from "@plane/propel/icons"; +// types import type { TFilters } from "@/types/issue"; // components import { AppliedPriorityFilters } from "./priority"; @@ -55,7 +55,7 @@ export const AppliedFiltersList: React.FC = observer((props) => { className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" onClick={() => handleRemoveFilter(filterKey, null)} > - +
@@ -67,7 +67,7 @@ export const AppliedFiltersList: React.FC = observer((props) => { className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200" > {t("common.clear_all")} - +
); diff --git a/apps/space/core/components/issues/filters/applied-filters/label.tsx b/apps/space/core/components/issues/filters/applied-filters/label.tsx index 5abbd54ba..2b842c152 100644 --- a/apps/space/core/components/issues/filters/applied-filters/label.tsx +++ b/apps/space/core/components/issues/filters/applied-filters/label.tsx @@ -1,6 +1,6 @@ "use client"; -import { X } from "lucide-react"; +import { CloseIcon } from "@plane/propel/icons"; // types import type { IIssueLabel } from "@/types/issue"; @@ -34,7 +34,7 @@ export const AppliedLabelsFilters: React.FC = (props) => { className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" onClick={() => handleRemove(labelId)} > - +
); diff --git a/apps/space/core/components/issues/filters/applied-filters/priority.tsx b/apps/space/core/components/issues/filters/applied-filters/priority.tsx index a687cb67c..da151873f 100644 --- a/apps/space/core/components/issues/filters/applied-filters/priority.tsx +++ b/apps/space/core/components/issues/filters/applied-filters/priority.tsx @@ -1,7 +1,6 @@ "use client"; -import { X } from "lucide-react"; -import { PriorityIcon } from "@plane/propel/icons"; +import { CloseIcon, PriorityIcon } from "@plane/propel/icons"; import type { TIssuePriorities } from "@plane/propel/icons"; type Props = { @@ -25,7 +24,7 @@ export const AppliedPriorityFilters: React.FC = (props) => { className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" onClick={() => handleRemove(priority)} > - +
))} diff --git a/apps/space/core/components/issues/filters/applied-filters/state.tsx b/apps/space/core/components/issues/filters/applied-filters/state.tsx index c80c8688a..1836c86a7 100644 --- a/apps/space/core/components/issues/filters/applied-filters/state.tsx +++ b/apps/space/core/components/issues/filters/applied-filters/state.tsx @@ -1,10 +1,9 @@ "use client"; import { observer } from "mobx-react"; -import { X } from "lucide-react"; // plane imports import { EIconSize } from "@plane/constants"; -import { StateGroupIcon } from "@plane/propel/icons"; +import { CloseIcon, StateGroupIcon } from "@plane/propel/icons"; // hooks import { useStates } from "@/hooks/store/use-state"; @@ -34,7 +33,7 @@ export const AppliedStateFilters: React.FC = observer((props) => { className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" onClick={() => handleRemove(stateId)} > - +
); diff --git a/apps/space/core/components/issues/filters/helpers/filter-header.tsx b/apps/space/core/components/issues/filters/helpers/filter-header.tsx index 52d766516..4505efbdb 100644 --- a/apps/space/core/components/issues/filters/helpers/filter-header.tsx +++ b/apps/space/core/components/issues/filters/helpers/filter-header.tsx @@ -1,8 +1,7 @@ "use client"; - import React from "react"; -// lucide icons -import { ChevronDown, ChevronUp } from "lucide-react"; +// icons +import { ChevronDownIcon, ChevronUpIcon } from "@plane/propel/icons"; interface IFilterHeader { title: string; @@ -18,7 +17,7 @@ export const FilterHeader = ({ title, isPreviewEnabled, handleIsPreviewEnabled } className="grid h-5 w-5 flex-shrink-0 place-items-center rounded hover:bg-custom-background-80" onClick={handleIsPreviewEnabled} > - {isPreviewEnabled ? : } + {isPreviewEnabled ? : }
); diff --git a/apps/space/core/components/issues/filters/selection.tsx b/apps/space/core/components/issues/filters/selection.tsx index 3042a419d..221e9df8c 100644 --- a/apps/space/core/components/issues/filters/selection.tsx +++ b/apps/space/core/components/issues/filters/selection.tsx @@ -2,7 +2,8 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; -import { Search, X } from "lucide-react"; +import { Search } from "lucide-react"; +import { CloseIcon } from "@plane/propel/icons"; // types import type { IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue"; // local imports @@ -37,7 +38,7 @@ export const FilterSelection: React.FC = observer((props) => { /> {filtersSearchQuery !== "" && ( )}
diff --git a/apps/space/core/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx b/apps/space/core/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx index fd7ba5f0d..e656ee254 100644 --- a/apps/space/core/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx +++ b/apps/space/core/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx @@ -1,7 +1,8 @@ import type { FC } from "react"; import React from "react"; import { observer } from "mobx-react"; -import { Circle, ChevronDown, ChevronUp } from "lucide-react"; +import { Circle } from "lucide-react"; +import { ChevronDownIcon, ChevronUpIcon } from "@plane/propel/icons"; // mobx interface IHeaderSubGroupByCard { @@ -20,7 +21,7 @@ export const HeaderSubGroupByCard: FC = observer((props) onClick={() => toggleExpanded()} >
- {isExpanded ? : } + {isExpanded ? : }
diff --git a/apps/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx b/apps/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx index 6e97ed6ea..c33c1789b 100644 --- a/apps/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/apps/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -1,10 +1,11 @@ import React, { useRef, useState } from "react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; -import { Check, MessageSquare, MoreVertical, X } from "lucide-react"; +import { Check, MessageSquare, MoreVertical } from "lucide-react"; import { Menu, Transition } from "@headlessui/react"; // plane imports import type { EditorRefApi } from "@plane/editor"; +import { CloseIcon } from "@plane/propel/icons"; import type { TIssuePublicComment } from "@plane/types"; import { getFileURL } from "@plane/utils"; // components @@ -136,7 +137,7 @@ export const CommentCard: React.FC = observer((props) => { className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500" onClick={() => setIsEditing(false)} > - +
diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx index ef49c8ffb..cee770d80 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx @@ -4,10 +4,10 @@ import type { FC } from "react"; import { observer } from "mobx-react"; import { useParams, useRouter } from "next/navigation"; -import { ChevronDown, PanelRight } from "lucide-react"; +import { PanelRight } from "lucide-react"; import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { YourWorkIcon } from "@plane/propel/icons"; +import { YourWorkIcon, ChevronDownIcon } from "@plane/propel/icons"; import type { IUserProfileProjectSegregation } from "@plane/types"; import { Breadcrumbs, Header, CustomMenu } from "@plane/ui"; import { cn } from "@plane/utils"; @@ -75,7 +75,7 @@ export const UserProfileHeader: FC = observer((props) => { customButton={
{type} - +
} customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx index 5b59b39d4..c6d4e3410 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx @@ -3,12 +3,12 @@ import { useCallback } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// icons -import { ChevronDown } from "lucide-react"; // plane constants import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; // plane i18n import { useTranslation } from "@plane/i18n"; +// icons +import { ChevronDownIcon } from "@plane/propel/icons"; // types import type { IIssueDisplayFilterOptions, @@ -88,7 +88,7 @@ export const ProfileIssuesMobileHeader = observer(() => { customButton={
{t("common.layout")} - +
} customButtonClassName="flex flex-center text-custom-text-200 text-sm" @@ -117,7 +117,7 @@ export const ProfileIssuesMobileHeader = observer(() => { menuButton={
{t("common.display")} - +
} > diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx index 747e65c16..75c0a9114 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx @@ -3,12 +3,10 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// icons -import { ChevronDown } from "lucide-react"; // plane imports import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { CalendarLayoutIcon, BoardLayoutIcon, ListLayoutIcon } from "@plane/propel/icons"; +import { CalendarLayoutIcon, BoardLayoutIcon, ListLayoutIcon, ChevronDownIcon } from "@plane/propel/icons"; import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types"; import { EIssuesStoreType } from "@plane/types"; import { CustomMenu } from "@plane/ui"; @@ -125,7 +123,7 @@ export const CycleIssuesMobileHeader = observer(() => { menuButton={ {t("common.display")} - + } > diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx index ed827135a..c714cfaf1 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx @@ -3,10 +3,10 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import { ChevronDown } from "lucide-react"; // plane imports import { EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { ChevronDownIcon } from "@plane/propel/icons"; import type { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types"; // components @@ -79,7 +79,7 @@ export const ProjectIssuesMobileHeader = observer(() => { menuButton={ {t("common.display")} - + } > diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx index ae450f5de..6be0088d6 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx @@ -3,12 +3,10 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// icons -import { ChevronDown } from "lucide-react"; // plane imports import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { CalendarLayoutIcon, BoardLayoutIcon, ListLayoutIcon } from "@plane/propel/icons"; +import { CalendarLayoutIcon, BoardLayoutIcon, ListLayoutIcon, ChevronDownIcon } from "@plane/propel/icons"; import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types"; import { EIssuesStoreType } from "@plane/types"; import { CustomMenu } from "@plane/ui"; @@ -109,7 +107,7 @@ export const ModuleIssuesMobileHeader = observer(() => { menuButton={ Display - + } > diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx index 138867e4f..94b190457 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx @@ -1,9 +1,9 @@ "use client"; import { observer } from "mobx-react"; -import { ChevronDown } from "lucide-react"; import { MODULE_VIEW_LAYOUTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { ChevronDownIcon } from "@plane/propel/icons"; import { CustomMenu, Row } from "@plane/ui"; import { ModuleLayoutIcon } from "@/components/modules"; import { useModuleFilter } from "@/hooks/store/use-module-filter"; @@ -22,7 +22,7 @@ export const ModulesListMobileHeader = observer(() => { // placement="bottom-start" customButton={ - Layout + Layout } customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm" diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/mobile-header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/mobile-header.tsx index e9ef19d1f..3980e977f 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/mobile-header.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/mobile-header.tsx @@ -2,7 +2,8 @@ import { observer } from "mobx-react"; // icons -import { ChevronDown, ListFilter } from "lucide-react"; +import { ListFilter } from "lucide-react"; +import { ChevronDownIcon } from "@plane/propel/icons"; // components import { Row } from "@plane/ui"; import { FiltersDropdown } from "@/components/issues/issue-layouts/filters"; @@ -42,7 +43,7 @@ export const ViewMobileHeader = observer(() => { menuButton={ Filters - + } > diff --git a/apps/web/app/(all)/profile/sidebar.tsx b/apps/web/app/(all)/profile/sidebar.tsx index acf25bb18..4619b186f 100644 --- a/apps/web/app/(all)/profile/sidebar.tsx +++ b/apps/web/app/(all)/profile/sidebar.tsx @@ -5,22 +5,12 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; // icons -import { - ChevronLeft, - LogOut, - MoveLeft, - Activity, - Bell, - CircleUser, - KeyRound, - Settings2, - CirclePlus, - Mails, -} from "lucide-react"; +import { LogOut, MoveLeft, Activity, Bell, CircleUser, KeyRound, Settings2, CirclePlus, Mails } from "lucide-react"; // plane imports import { PROFILE_ACTION_LINKS } from "@plane/constants"; import { useOutsideClickDetector } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; +import { ChevronLeftIcon } from "@plane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { Tooltip } from "@plane/propel/tooltip"; import { cn, getFileURL } from "@plane/utils"; @@ -141,7 +131,7 @@ export const ProfileLayoutSidebar = observer(() => { }`} > - + {!sidebarCollapsed && (

{t("profile_settings")}

diff --git a/apps/web/app/(all)/workspace-invitations/page.tsx b/apps/web/app/(all)/workspace-invitations/page.tsx index 6f9d78d56..7883b08c6 100644 --- a/apps/web/app/(all)/workspace-invitations/page.tsx +++ b/apps/web/app/(all)/workspace-invitations/page.tsx @@ -4,7 +4,8 @@ import React from "react"; import { observer } from "mobx-react"; import { useSearchParams } from "next/navigation"; import useSWR from "swr"; -import { Boxes, Check, Share2, Star, User2, X } from "lucide-react"; +import { Boxes, Check, Share2, Star, User2 } from "lucide-react"; +import { CloseIcon } from "@plane/propel/icons"; // components import { LogoSpinner } from "@/components/common/logo-spinner"; import { EmptySpace, EmptySpaceItem } from "@/components/ui/empty-space"; @@ -85,7 +86,7 @@ const WorkspaceInvitationPage = observer(() => { description="Your workspace is where you'll create projects, collaborate on your work items, and organize different streams of work in your Plane account." > - + ) ) : error || invitationDetail?.responded_at ? ( diff --git a/apps/web/ce/components/pages/editor/ai/menu.tsx b/apps/web/ce/components/pages/editor/ai/menu.tsx index 109af7974..033c8ff88 100644 --- a/apps/web/ce/components/pages/editor/ai/menu.tsx +++ b/apps/web/ce/components/pages/editor/ai/menu.tsx @@ -2,9 +2,10 @@ import React, { useEffect, useRef, useState } from "react"; import type { LucideIcon } from "lucide-react"; -import { ChevronRight, CornerDownRight, RefreshCcw, Sparkles, TriangleAlert } from "lucide-react"; +import { CornerDownRight, RefreshCcw, Sparkles, TriangleAlert } from "lucide-react"; // plane editor import type { EditorRefApi } from "@plane/editor"; +import { ChevronRightIcon } from "@plane/propel/icons"; // plane ui import { Tooltip } from "@plane/propel/tooltip"; // components @@ -174,7 +175,7 @@ export const EditorAIMenu: React.FC = (props) => { {item.label} - {
{t("common.filters")} - +
} isFiltersApplied={isFiltersApplied} diff --git a/apps/web/ce/components/workspace/delete-workspace-section.tsx b/apps/web/ce/components/workspace/delete-workspace-section.tsx index aa72fdc32..a9975b8f5 100644 --- a/apps/web/ce/components/workspace/delete-workspace-section.tsx +++ b/apps/web/ce/components/workspace/delete-workspace-section.tsx @@ -1,11 +1,11 @@ import type { FC } from "react"; import { useState } from "react"; import { observer } from "mobx-react"; -import { ChevronDown, ChevronUp } from "lucide-react"; // types import { WORKSPACE_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; +import { ChevronDownIcon, ChevronUpIcon } from "@plane/propel/icons"; import type { IWorkspace } from "@plane/types"; // ui import { Collapsible } from "@plane/ui"; @@ -42,7 +42,7 @@ export const DeleteWorkspaceSection: FC = observer((props) => {t("workspace_settings.settings.general.delete_workspace")} - {isOpen ? : } + {isOpen ? : } } > diff --git a/apps/web/core/components/account/auth-forms/auth-banner.tsx b/apps/web/core/components/account/auth-forms/auth-banner.tsx index 95022dcb2..555d179af 100644 --- a/apps/web/core/components/account/auth-forms/auth-banner.tsx +++ b/apps/web/core/components/account/auth-forms/auth-banner.tsx @@ -1,7 +1,8 @@ import type { FC } from "react"; -import { Info, X } from "lucide-react"; -// plane imports +import { Info } from "lucide-react"; import { useTranslation } from "@plane/i18n"; +import { CloseIcon } from "@plane/propel/icons"; +// plane imports // helpers import type { TAuthErrorInfo } from "@/helpers/authentication.helper"; @@ -32,7 +33,7 @@ export const AuthBanner: FC = (props) => { onClick={() => handleBannerData?.(undefined)} aria-label={t("aria_labels.auth_forms.close_alert")} > - +
); diff --git a/apps/web/core/components/account/auth-forms/forgot-password-popover.tsx b/apps/web/core/components/account/auth-forms/forgot-password-popover.tsx index 3b243b894..3e47dbe1c 100644 --- a/apps/web/core/components/account/auth-forms/forgot-password-popover.tsx +++ b/apps/web/core/components/account/auth-forms/forgot-password-popover.tsx @@ -1,9 +1,9 @@ import { Fragment, useState } from "react"; import { usePopper } from "react-popper"; -import { X } from "lucide-react"; import { Popover } from "@headlessui/react"; // plane imports import { useTranslation } from "@plane/i18n"; +import { CloseIcon } from "@plane/propel/icons"; export const ForgotPasswordPopover = () => { // popper-js refs @@ -51,7 +51,7 @@ export const ForgotPasswordPopover = () => { onClick={() => close()} aria-label={t("aria_labels.auth_forms.close_popover")} > - +
)} diff --git a/apps/web/core/components/account/auth-forms/password.tsx b/apps/web/core/components/account/auth-forms/password.tsx index 52d32dc5d..5fb6a4276 100644 --- a/apps/web/core/components/account/auth-forms/password.tsx +++ b/apps/web/core/components/account/auth-forms/password.tsx @@ -4,11 +4,12 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; // icons -import { Eye, EyeOff, Info, X, XCircle } from "lucide-react"; +import { Eye, EyeOff, Info, XCircle } from "lucide-react"; // plane imports import { API_BASE_URL, E_PASSWORD_STRENGTH, AUTH_TRACKER_EVENTS, AUTH_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; +import { CloseIcon } from "@plane/propel/icons"; import { Input, PasswordStrengthIndicator, Spinner } from "@plane/ui"; import { getPasswordStrength } from "@plane/utils"; // components @@ -134,7 +135,7 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-red-500/20 text-custom-primary-100/80" onClick={() => setBannerMessage(false)} > - +
)} diff --git a/apps/web/core/components/analytics/insight-table/data-table.tsx b/apps/web/core/components/analytics/insight-table/data-table.tsx index 83892ceb4..8c6468793 100644 --- a/apps/web/core/components/analytics/insight-table/data-table.tsx +++ b/apps/web/core/components/analytics/insight-table/data-table.tsx @@ -17,10 +17,11 @@ import { getSortedRowModel, useReactTable, } from "@tanstack/react-table"; -import { Search, X } from "lucide-react"; -// plane package imports +import { Search } from "lucide-react"; import { useTranslation } from "@plane/i18n"; import { EmptyStateCompact } from "@plane/propel/empty-state"; +import { CloseIcon } from "@plane/propel/icons"; +// plane package imports import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@plane/propel/table"; import { cn } from "@plane/utils"; // plane web components @@ -116,7 +117,7 @@ export function DataTable({ columns, data, searchPlaceholder, act setIsSearchOpen(false); }} > - + )}
diff --git a/apps/web/core/components/analytics/work-items/modal/header.tsx b/apps/web/core/components/analytics/work-items/modal/header.tsx index 734eebbf8..22f1a14c3 100644 --- a/apps/web/core/components/analytics/work-items/modal/header.tsx +++ b/apps/web/core/components/analytics/work-items/modal/header.tsx @@ -1,6 +1,7 @@ import { observer } from "mobx-react"; // plane package imports -import { Expand, Shrink, X } from "lucide-react"; +import { Expand, Shrink } from "lucide-react"; +import { CloseIcon } from "@plane/propel/icons"; import type { ICycle, IModule } from "@plane/types"; // icons @@ -34,7 +35,7 @@ export const WorkItemsModalHeader: React.FC = observer((props) => { className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100" onClick={handleClose} > - +
diff --git a/apps/web/core/components/comments/card/edit-form.tsx b/apps/web/core/components/comments/card/edit-form.tsx index b370db0a2..fcaa6b352 100644 --- a/apps/web/core/components/comments/card/edit-form.tsx +++ b/apps/web/core/components/comments/card/edit-form.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useRef } from "react"; import { observer } from "mobx-react"; import { useForm } from "react-hook-form"; -import { Check, X } from "lucide-react"; -// plane imports +import { Check } from "lucide-react"; import type { EditorRefApi } from "@plane/editor"; +import { CloseIcon } from "@plane/propel/icons"; +// plane imports import type { TCommentsOperations, TIssueComment } from "@plane/types"; import { isCommentEmpty } from "@plane/utils"; // components @@ -123,7 +124,7 @@ export const CommentCardEditForm: React.FC = observer((props) => { editorRef.current?.setEditorValue(comment.comment_html ?? "

"); }} > - +
diff --git a/apps/web/core/components/common/applied-filters/date.tsx b/apps/web/core/components/common/applied-filters/date.tsx index 3ecf8a540..3d8d94cf8 100644 --- a/apps/web/core/components/common/applied-filters/date.tsx +++ b/apps/web/core/components/common/applied-filters/date.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react"; // icons -import { X } from "lucide-react"; -// plane constants import { DATE_BEFORE_FILTER_OPTIONS } from "@plane/constants"; +import { CloseIcon } from "@plane/propel/icons"; +// plane constants import { renderFormattedDate, capitalizeFirstLetter } from "@plane/utils"; // helpers type Props = { @@ -44,7 +44,7 @@ export const AppliedDateFilters: React.FC = observer((props) => { className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" onClick={() => handleRemove(date)} > - + )}
diff --git a/apps/web/core/components/common/applied-filters/members.tsx b/apps/web/core/components/common/applied-filters/members.tsx index 49b0f1a30..a1823132f 100644 --- a/apps/web/core/components/common/applied-filters/members.tsx +++ b/apps/web/core/components/common/applied-filters/members.tsx @@ -1,7 +1,7 @@ "use client"; import { observer } from "mobx-react"; -import { X } from "lucide-react"; +import { CloseIcon } from "@plane/propel/icons"; // plane ui import { Avatar } from "@plane/ui"; // helpers @@ -44,7 +44,7 @@ export const AppliedMembersFilters: React.FC = observer((props) => { className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" onClick={() => handleRemove(memberId)} > - + )}
diff --git a/apps/web/core/components/core/description-versions/modal.tsx b/apps/web/core/components/core/description-versions/modal.tsx index 1dbe5e552..15e291494 100644 --- a/apps/web/core/components/core/description-versions/modal.tsx +++ b/apps/web/core/components/core/description-versions/modal.tsx @@ -1,10 +1,11 @@ import { useCallback, useRef } from "react"; import { observer } from "mobx-react"; -import { ChevronLeft, ChevronRight, Copy } from "lucide-react"; +import { Copy } from "lucide-react"; // plane imports import type { EditorRefApi } from "@plane/editor"; import { useTranslation } from "@plane/i18n"; import { Button, getButtonStyling } from "@plane/propel/button"; +import { ChevronLeftIcon, ChevronRightIcon } from "@plane/propel/icons"; import { setToast, TOAST_TYPE } from "@plane/propel/toast"; import { Tooltip } from "@plane/propel/tooltip"; import type { TDescriptionVersion } from "@plane/types"; @@ -102,7 +103,7 @@ export const DescriptionVersionsModal: React.FC = observer((props) => { )} disabled={isPrevDisabled} > - +
diff --git a/apps/web/core/components/core/filters/date-filter-modal.tsx b/apps/web/core/components/core/filters/date-filter-modal.tsx index 334d15ae4..32a407414 100644 --- a/apps/web/core/components/core/filters/date-filter-modal.tsx +++ b/apps/web/core/components/core/filters/date-filter-modal.tsx @@ -2,12 +2,12 @@ import { Fragment } from "react"; import { Controller, useForm } from "react-hook-form"; -import { X } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; import { Button } from "@plane/propel/button"; import { Calendar } from "@plane/propel/calendar"; +import { CloseIcon } from "@plane/propel/icons"; import { renderFormattedPayloadDate, renderFormattedDate, getDate } from "@plane/utils"; import { DateFilterSelect } from "./date-filter-select"; type Props = { @@ -84,7 +84,7 @@ export const DateFilterModal: React.FC = ({ title, handleClose, isOpen, o )} /> - +
= (props) => { className="group p-1" onClick={() => setSelectedIssues((prevData) => prevData.filter((i) => i.id !== issue.id))} > - +
))} diff --git a/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx b/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx index 224e1ff67..100311e78 100644 --- a/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx +++ b/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx @@ -5,10 +5,10 @@ import { useMemo } from "react"; import { isEmpty } from "lodash-es"; import { observer } from "mobx-react"; import { useSearchParams } from "next/navigation"; -import { ChevronUp, ChevronDown } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; // plane imports import { useTranslation } from "@plane/i18n"; +import { ChevronUpIcon, ChevronDownIcon } from "@plane/propel/icons"; import type { ICycle, TCyclePlotType, TProgressSnapshot } from "@plane/types"; import { EIssuesStoreType } from "@plane/types"; import { getDate } from "@plane/utils"; @@ -114,9 +114,9 @@ export const CycleAnalyticsProgress: FC = observer((pro {open ? ( -
diff --git a/apps/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx b/apps/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx index 0c84571c9..41129e9c0 100644 --- a/apps/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx +++ b/apps/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx @@ -4,7 +4,7 @@ import type { FC } from "react"; import React, { useEffect } from "react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; -import { ArrowRight, ChevronRight } from "lucide-react"; +import { ArrowRight } from "lucide-react"; // Plane Imports import { CYCLE_TRACKER_EVENTS, @@ -14,6 +14,7 @@ import { CYCLE_TRACKER_ELEMENTS, } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { ChevronRightIcon } from "@plane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { ICycle } from "@plane/types"; import { getDate, renderFormattedPayloadDate } from "@plane/utils"; @@ -160,7 +161,7 @@ export const CycleSidebarHeader: FC = observer((props) => { className="flex size-4 items-center justify-center rounded-full bg-custom-border-200" onClick={() => handleClose()} > - +
diff --git a/apps/web/core/components/cycles/applied-filters/date.tsx b/apps/web/core/components/cycles/applied-filters/date.tsx index 18a5ece58..e8156fafc 100644 --- a/apps/web/core/components/cycles/applied-filters/date.tsx +++ b/apps/web/core/components/cycles/applied-filters/date.tsx @@ -1,7 +1,7 @@ import { observer } from "mobx-react"; -import { X } from "lucide-react"; // helpers import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants"; +import { CloseIcon } from "@plane/propel/icons"; import { renderFormattedDate, capitalizeFirstLetter } from "@plane/utils"; // constants @@ -44,7 +44,7 @@ export const AppliedDateFilters: React.FC = observer((props) => { className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" onClick={() => handleRemove(date)} > - + )}
diff --git a/apps/web/core/components/cycles/applied-filters/root.tsx b/apps/web/core/components/cycles/applied-filters/root.tsx index 1ee80e738..59ebf55cb 100644 --- a/apps/web/core/components/cycles/applied-filters/root.tsx +++ b/apps/web/core/components/cycles/applied-filters/root.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react"; -import { X } from "lucide-react"; // plane imports import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { CloseIcon } from "@plane/propel/icons"; import type { TCycleFilters } from "@plane/types"; import { Tag } from "@plane/ui"; import { replaceUnderscoreIfSnakeCase } from "@plane/utils"; @@ -67,7 +67,7 @@ export const CycleAppliedFiltersList: React.FC = observer((props) => { className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" onClick={() => handleRemoveFilter(filterKey, null)} > - + )}
@@ -78,7 +78,7 @@ export const CycleAppliedFiltersList: React.FC = observer((props) => { )} diff --git a/apps/web/core/components/cycles/applied-filters/status.tsx b/apps/web/core/components/cycles/applied-filters/status.tsx index ef2d63b1a..e54edf6b7 100644 --- a/apps/web/core/components/cycles/applied-filters/status.tsx +++ b/apps/web/core/components/cycles/applied-filters/status.tsx @@ -1,7 +1,7 @@ import { observer } from "mobx-react"; -import { X } from "lucide-react"; import { CYCLE_STATUS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { CloseIcon } from "@plane/propel/icons"; import { cn } from "@plane/utils"; type Props = { @@ -34,7 +34,7 @@ export const AppliedStatusFilters: React.FC = observer((props) => { className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" onClick={() => handleRemove(status)} > - + )}
diff --git a/apps/web/core/components/cycles/archived-cycles/header.tsx b/apps/web/core/components/cycles/archived-cycles/header.tsx index dbbb93dc1..50e8fd67a 100644 --- a/apps/web/core/components/cycles/archived-cycles/header.tsx +++ b/apps/web/core/components/cycles/archived-cycles/header.tsx @@ -3,9 +3,10 @@ import { useCallback, useRef, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // icons -import { ListFilter, Search, X } from "lucide-react"; -// plane helpers +import { ListFilter, Search } from "lucide-react"; import { useOutsideClickDetector } from "@plane/hooks"; +import { CloseIcon } from "@plane/propel/icons"; +// plane helpers // types import type { TCycleFilters } from "@plane/types"; import { cn, calculateTotalFilters } from "@plane/utils"; @@ -109,7 +110,7 @@ export const ArchivedCyclesHeader: FC = observer(() => { setIsSearchOpen(false); }} > - + )}
diff --git a/apps/web/core/components/cycles/cycles-view-header.tsx b/apps/web/core/components/cycles/cycles-view-header.tsx index 4276c1ed2..1cc782f59 100644 --- a/apps/web/core/components/cycles/cycles-view-header.tsx +++ b/apps/web/core/components/cycles/cycles-view-header.tsx @@ -1,11 +1,12 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; // icons -import { ListFilter, Search, X } from "lucide-react"; +import { ListFilter, Search } from "lucide-react"; // plane helpers import { useOutsideClickDetector } from "@plane/hooks"; // types import { useTranslation } from "@plane/i18n"; +import { CloseIcon } from "@plane/propel/icons"; import type { TCycleFilters } from "@plane/types"; import { cn, calculateTotalFilters } from "@plane/utils"; // components @@ -109,7 +110,7 @@ export const CyclesViewHeader: React.FC = observer((props) => { setIsSearchOpen(false); }} > - + )}
diff --git a/apps/web/core/components/cycles/dropdowns/filters/root.tsx b/apps/web/core/components/cycles/dropdowns/filters/root.tsx index 550d7daf7..148de9946 100644 --- a/apps/web/core/components/cycles/dropdowns/filters/root.tsx +++ b/apps/web/core/components/cycles/dropdowns/filters/root.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { observer } from "mobx-react"; -import { Search, X } from "lucide-react"; +import { Search } from "lucide-react"; +import { CloseIcon } from "@plane/propel/icons"; // plane imports import type { TCycleFilters, TCycleGroups } from "@plane/types"; // hooks @@ -38,7 +39,7 @@ export const CycleFiltersSelection: React.FC = observer((props) => { /> {filtersSearchQuery !== "" && ( )}
diff --git a/apps/web/core/components/cycles/list/cycle-list-group-header.tsx b/apps/web/core/components/cycles/list/cycle-list-group-header.tsx index 5ab265dcf..074bc500c 100644 --- a/apps/web/core/components/cycles/list/cycle-list-group-header.tsx +++ b/apps/web/core/components/cycles/list/cycle-list-group-header.tsx @@ -2,9 +2,8 @@ import type { FC } from "react"; import React from "react"; -import { ChevronDown } from "lucide-react"; // types -import { CycleGroupIcon } from "@plane/propel/icons"; +import { CycleGroupIcon, ChevronDownIcon } from "@plane/propel/icons"; import type { TCycleGroups } from "@plane/types"; // icons import { Row } from "@plane/ui"; @@ -33,7 +32,7 @@ export const CycleListGroupHeader: FC = (props) => { {showCount &&
{`${count ?? "0"}`}
}
- = observer((props) => { if (!project) return null; return ( - = observer((props) => {

Transfer work items

diff --git a/apps/web/core/components/dropdowns/cycle/index.tsx b/apps/web/core/components/dropdowns/cycle/index.tsx index 15569315c..cb2df4683 100644 --- a/apps/web/core/components/dropdowns/cycle/index.tsx +++ b/apps/web/core/components/dropdowns/cycle/index.tsx @@ -3,10 +3,9 @@ import type { ReactNode } from "react"; import { useRef, useState } from "react"; import { observer } from "mobx-react"; -import { ChevronDown } from "lucide-react"; import { useTranslation } from "@plane/i18n"; // ui -import { CycleIcon } from "@plane/propel/icons"; +import { CycleIcon, ChevronDownIcon } from "@plane/propel/icons"; import { ComboDropDown } from "@plane/ui"; // helpers import { cn } from "@plane/utils"; @@ -126,7 +125,7 @@ export const CycleDropdown: React.FC = observer((props) => { {selectedName ?? placeholder} )} {dropdownArrow && ( -
@@ -210,7 +210,7 @@ const BackgroundButton = (props: ButtonProps) => { {priorityDetails?.title ?? t("common.priority") ?? placeholder} )} {dropdownArrow && ( -