Compare commits

..

26 Commits

Author SHA1 Message Date
akshat5302 2ff9f1d775 fix: sed cmd warnings fixed 2025-01-31 15:20:57 +05:30
Aaryan Khandelwal ddad1767a2 fix: table flixker on resize (#6524) 2025-01-31 02:31:17 +05:30
Aaryan Khandelwal 6a37a2ce21 fix: link without protocol (#6517) 2025-01-30 20:25:00 +05:30
Aaryan Khandelwal 01bd1bde64 fix: table resize overflow issues (#6520) 2025-01-30 19:27:12 +05:30
Abenezer Belachew 9268180aec path already defined on line 51 (#6427) 2025-01-30 13:36:16 +05:30
Aaryan Khandelwal ff778b98f5 [WEB-3095] fix: recents widget title truncate (#6512)
* fix: recents widget title truncate

* chore: revert prop changes

* chore: revert list item changes
2025-01-30 13:34:42 +05:30
Sangeetha 8f5ce6b232 feat: user preference url and sort order change (#6505)
* fix: change url

* Change order of user preference keys
2025-01-30 13:29:39 +05:30
Anmol Singh Bhatia 58a4ca9f36 chore: board layout padding improvement (#6507) 2025-01-29 16:56:27 +05:30
guru_sainath 312b077657 [WEB-3177] fix: resolve cycle creation issue for equal start_date and completed_date (#6504)
* fix: fixed cycle startdate if the the start_date and completed cyles dates are today

* chore: updated validation for date match
2025-01-29 16:35:25 +05:30
Bavisetti Narayan c65e42f807 chore: add attachment in intake issue (#6503) 2025-01-29 15:53:34 +05:30
Prateek Shourya f4af78c0fc [WEB-3218] fix: redirection for cross projects issue relations (#6457) 2025-01-29 13:00:24 +05:30
Prateek Shourya c0b6abc3d5 refactor: minor store level changes (#6500) 2025-01-29 01:04:54 +05:30
sriram veeraghanta 2f2e6626c6 fix: typo fixes 2025-01-28 21:07:59 +05:30
Sangeetha 6a8d3202b7 feat: workspace user preference api (#6497)
* feat: workspace user preference api

* feat: remove sort order calculation

* Return 404 error
2025-01-28 20:50:24 +05:30
Bavisetti Narayan 51b52a7fc3 [WEB-3249] chore: delete the user recent visits (#6496)
* chore: delete the user recent visits

* chore: hard deleted the recent visits
2025-01-28 20:50:15 +05:30
Bavisetti Narayan 23ede81737 chore: update project state (#6467) 2025-01-28 20:49:36 +05:30
Aaryan Khandelwal b698f44500 [PE-155] chore: floating toolbar for pages (#6482)
* chore: add floating toolbar to pages

* fix: locked page toolbar
2025-01-28 20:21:09 +05:30
M. Palanikannan 421839ec51 [PE-255] fix: remove drag handles from content within table cells (#6487)
* fix: remove drag handles from content within table cells

* style: table cell padding

* style: table cell padding

* fix: insert resizable tables

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2025-01-28 20:20:40 +05:30
M. Palanikannan 940b5e4e44 fix: custom color extension markdown rule added now (#6471) 2025-01-28 20:20:23 +05:30
Aaryan Khandelwal 6003c88d62 fix: disable comment submit while uploading an image (#6445) 2025-01-28 20:19:01 +05:30
Aaryan Khandelwal 74913a6659 fix: page name and recents empty state (#6491) 2025-01-28 17:13:20 +05:30
sriram veeraghanta 97578684c6 chore: lock file updates 2025-01-28 16:18:30 +05:30
Aaryan Khandelwal 88b4d32220 [WEB-3237, 3238] dev: date picker enhancements (#6470)
* [WEB-3238] dev: datepicker with month and year selection dropdowns (#6391)

* feat: react-day-picker upgrade and caption dropdowns

* style fixes

* style: css and autofocus improved

* fix: fixed weeks for datepicker to ensure static height

---------

Co-authored-by: Vineet K <55555696+vineetk13@users.noreply.github.com>
2025-01-28 16:15:18 +05:30
Aaryan Khandelwal f32635a6a8 [WEB-3203] fix: stickies height, overflow (#6484)
* fix: stickies height

* chore: remove unused drop indicators
2025-01-28 15:33:25 +05:30
Bavisetti Narayan 7fe58e0ea9 chore: added estimate point value in expand issues (#6483) 2025-01-28 15:11:33 +05:30
Prateek Shourya 7f22cd1ac1 [WEB-3229] fix: issue creation from modal (#6460) 2025-01-27 13:13:32 +05:30
335 changed files with 4730 additions and 8740 deletions
@@ -15,3 +15,4 @@ from .state import StateLiteSerializer, StateSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
from .intake import IntakeIssueSerializer
from .estimate import EstimatePointSerializer
+2
View File
@@ -72,6 +72,7 @@ class BaseSerializer(serializers.ModelSerializer):
StateLiteSerializer,
UserLiteSerializer,
WorkspaceLiteSerializer,
EstimatePointSerializer,
)
# Expansion mapper
@@ -88,6 +89,7 @@ class BaseSerializer(serializers.ModelSerializer):
"owned_by": UserLiteSerializer,
"members": UserLiteSerializer,
"parent": IssueLiteSerializer,
"estimate_point": EstimatePointSerializer,
}
# Check if field in expansion then expand the field
if expand in expansion:
@@ -0,0 +1,10 @@
# Module imports
from plane.db.models import EstimatePoint
from .base import BaseSerializer
class EstimatePointSerializer(BaseSerializer):
class Meta:
model = EstimatePoint
fields = ["id", "value"]
read_only_fields = fields
+5
View File
@@ -71,4 +71,9 @@ urlpatterns = [
IssueAttachmentEndpoint.as_view(),
name="attachment",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
IssueAttachmentEndpoint.as_view(),
name="issue-attachment",
),
]
@@ -22,6 +22,7 @@ from plane.db.models import (
ProjectMember,
WorkspaceHomePreference,
Sticky,
WorkspaceUserPreference,
)
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
@@ -258,3 +259,10 @@ class StickySerializer(BaseSerializer):
fields = "__all__"
read_only_fields = ["workspace", "owner"]
extra_kwargs = {"name": {"required": False}}
class WorkspaceUserPreferenceSerializer(BaseSerializer):
class Meta:
model = WorkspaceUserPreference
fields = ["key", "is_pinned", "sort_order"]
read_only_fields = ["workspace", "created_by", "updated_by"]
+12
View File
@@ -31,6 +31,7 @@ from plane.app.views import (
UserRecentVisitViewSet,
WorkspaceHomePreferenceViewSet,
WorkspaceStickyViewSet,
WorkspaceUserPreferenceViewSet,
)
@@ -258,4 +259,15 @@ urlpatterns = [
),
name="workspace-sticky",
),
# User Preference
path(
"workspaces/<str:slug>/sidebar-preferences/",
WorkspaceUserPreferenceViewSet.as_view(),
name="workspace-user-preference",
),
path(
"workspaces/<str:slug>/sidebar-preferences/<str:key>/",
WorkspaceUserPreferenceViewSet.as_view(),
name="workspace-user-preference",
),
]
+1
View File
@@ -48,6 +48,7 @@ from .workspace.favorite import (
WorkspaceFavoriteGroupEndpoint,
)
from .workspace.recent_visit import UserRecentVisitViewSet
from .workspace.user_preference import WorkspaceUserPreferenceViewSet
from .workspace.member import (
WorkSpaceMemberViewSet,
+8
View File
@@ -47,6 +47,7 @@ from plane.db.models import (
User,
Project,
ProjectMember,
UserRecentVisit,
)
from plane.utils.analytics_plot import burndown_plot
from plane.bgtasks.recent_visited_task import recent_visited_task
@@ -543,6 +544,13 @@ class CycleViewSet(BaseViewSet):
entity_identifier=pk,
project_id=project_id,
).delete()
# Delete the cycle from recent visits
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="cycle",
).delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)
+8
View File
@@ -44,6 +44,7 @@ from plane.db.models import (
Project,
ProjectMember,
CycleIssue,
UserRecentVisit,
)
from plane.utils.grouper import (
issue_group_values,
@@ -671,6 +672,13 @@ class IssueViewSet(BaseViewSet):
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
issue.delete()
# delete the issue from recent visits
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="issue",
).delete(soft=False)
issue_activity.delay(
type="issue.activity.deleted",
requested_data=json.dumps({"issue_id": str(pk)}),
+8
View File
@@ -54,6 +54,7 @@ from plane.db.models import (
ModuleLink,
ModuleUserProperties,
Project,
UserRecentVisit,
)
from plane.utils.analytics_plot import burndown_plot
from plane.utils.timezone_converter import user_timezone_converter
@@ -808,6 +809,13 @@ class ModuleViewSet(BaseViewSet):
entity_identifier=pk,
project_id=project_id,
).delete()
# delete the module from recent visits
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="module",
).delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)
+8
View File
@@ -33,6 +33,7 @@ from plane.db.models import (
ProjectMember,
ProjectPage,
Project,
UserRecentVisit,
)
from plane.utils.error_codes import ERROR_CODES
from ..base import BaseAPIView, BaseViewSet
@@ -387,6 +388,13 @@ class PageViewSet(BaseViewSet):
entity_identifier=pk,
entity_type="page",
).delete()
# Delete the page from recent visit
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="page",
).delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)
+17
View File
@@ -53,6 +53,23 @@ class StateViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def partial_update(self, request, slug, project_id, pk):
try:
state = State.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
serializer = StateSerializer(state, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"name": "The state name is already taken"},
status=status.HTTP_400_BAD_REQUEST,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):
+8
View File
@@ -24,6 +24,7 @@ from plane.db.models import (
ProjectMember,
Project,
CycleIssue,
UserRecentVisit,
)
from plane.utils.grouper import (
issue_group_values,
@@ -495,6 +496,13 @@ class IssueViewViewSet(BaseViewSet):
entity_identifier=pk,
entity_type="view",
).delete()
# Delete the page from recent visit
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="view",
).delete(soft=False)
else:
return Response(
{"error": "Only admin or owner can delete the view"},
@@ -0,0 +1,78 @@
# Module imports
from ..base import BaseAPIView
from plane.db.models.workspace import WorkspaceUserPreference
from plane.app.serializers.workspace import WorkspaceUserPreferenceSerializer
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import Workspace
# Third party imports
from rest_framework.response import Response
from rest_framework import status
class WorkspaceUserPreferenceViewSet(BaseAPIView):
model = WorkspaceUserPreference
def get_serializer_class(self):
return WorkspaceUserPreferenceSerializer
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
get_preference = WorkspaceUserPreference.objects.filter(
user=request.user, workspace_id=workspace.id
)
create_preference_keys = []
keys = [
key
for key, _ in WorkspaceUserPreference.UserPreferenceKeys.choices
if key not in ["projects"]
]
for preference in keys:
if preference not in get_preference.values_list("key", flat=True):
create_preference_keys.append(preference)
preference = WorkspaceUserPreference.objects.bulk_create(
[
WorkspaceUserPreference(
key=key, user=request.user, workspace=workspace
)
for key in create_preference_keys
],
batch_size=10,
ignore_conflicts=True,
)
preference = WorkspaceUserPreference.objects.filter(
user=request.user, workspace_id=workspace.id
)
return Response(
preference.values("key", "is_pinned", "sort_order"),
status=status.HTTP_200_OK,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def patch(self, request, slug, key):
preference = WorkspaceUserPreference.objects.filter(
key=key, workspace__slug=slug, user=request.user
).first()
if preference:
serializer = WorkspaceUserPreferenceSerializer(
preference, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
{"detail": "Preference not found"}, status=status.HTTP_404_NOT_FOUND
)
-1
View File
@@ -53,7 +53,6 @@ urlpatterns = [
path("magic-generate/", MagicGenerateEndpoint.as_view(), name="magic-generate"),
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
path("magic-sign-up/", MagicSignUpEndpoint.as_view(), name="magic-sign-up"),
path("get-csrf-token/", CSRFTokenEndpoint.as_view(), name="get_csrf_token"),
path(
"spaces/magic-generate/",
MagicGenerateSpaceEndpoint.as_view(),
+2 -1
View File
@@ -69,7 +69,8 @@ from .workspace import (
WorkspaceTheme,
WorkspaceUserProperties,
WorkspaceUserLink,
WorkspaceHomePreference
WorkspaceHomePreference,
WorkspaceUserPreference,
)
from .favorite import UserFavorite
+2 -3
View File
@@ -388,15 +388,14 @@ class WorkspaceHomePreference(BaseModel):
return f"{self.workspace.name} {self.user.email} {self.key}"
class WorkspaceUserPreference(BaseModel):
"""Preference for the workspace for a user"""
class UserPreferenceKeys(models.TextChoices):
PROJECTS = "projects", "Projects"
ANALYTICS = "analytics", "Analytics"
CYCLES = "cycles", "Cycles"
VIEWS = "views", "Views"
ANALYTICS = "analytics", "Analytics"
PROJECTS = "projects", "Projects"
workspace = models.ForeignKey(
"db.Workspace",
+26 -9
View File
@@ -1,8 +1,14 @@
# Python imports
import pytz
from plane.db.models import Project
from datetime import datetime, time
from datetime import timedelta
# Django imports
from django.utils import timezone
# Module imports
from plane.db.models import Project
def user_timezone_converter(queryset, datetime_fields, user_timezone):
# Create a timezone object for the user's timezone
@@ -65,16 +71,27 @@ def convert_to_utc(
if is_start_date:
localized_datetime += timedelta(minutes=0, seconds=1)
# If it's start an end date are equal, add 23 hours, 59 minutes, and 59 seconds
# to make it the end of the day
if is_start_date_end_date_equal:
localized_datetime += timedelta(hours=23, minutes=59, seconds=59)
# Convert the localized datetime to UTC
utc_datetime = localized_datetime.astimezone(pytz.utc)
# Convert the localized datetime to UTC
utc_datetime = localized_datetime.astimezone(pytz.utc)
current_datetime_in_project_tz = timezone.now().astimezone(local_tz)
current_datetime_in_utc = current_datetime_in_project_tz.astimezone(pytz.utc)
# Return the UTC datetime for storage
return utc_datetime
if utc_datetime.date() == current_datetime_in_utc.date():
return current_datetime_in_utc
return utc_datetime
else:
# If it's start an end date are equal, add 23 hours, 59 minutes, and 59 seconds
# to make it the end of the day
if is_start_date_end_date_equal:
localized_datetime += timedelta(hours=23, minutes=59, seconds=59)
# Convert the localized datetime to UTC
utc_datetime = localized_datetime.astimezone(pytz.utc)
# Return the UTC datetime for storage
return utc_datetime
def convert_utc_to_project_timezone(utc_datetime, project_id):
+8 -7
View File
@@ -131,17 +131,18 @@ function updateEnvFile() {
fi
if [ -f "$file" ]; then
# check if key exists in the file
grep -q "^$key=" "$file"
if [ $? -ne 0 ]; then
# Check if key exists in the file
if ! grep -q "^$key=" "$file"; then
echo "$key=$value" >> "$file"
return
else
else
# Escape special characters in value for sed
value=$(printf '%s\n' "$value" | sed -e 's/[\/&]/\\&/g')
if [ "$OS_NAME" == "Darwin" ]; then
value=$(echo "$value" | sed 's/|/\\|/g')
sed -i '' "s|^$key=.*|$key=$value|g" "$file"
sed -i '' "s|^$key=.*|$key=$value|" "$file"
else
sed -i "s/^$key=.*/$key=$value/g" "$file"
sed -i "s|^$key=.*|$key=$value|" "$file"
fi
fi
else
-53
View File
@@ -2,56 +2,3 @@ export enum E_SORT_ORDER {
ASC = "asc",
DESC = "desc",
}
export const DATE_AFTER_FILTER_OPTIONS = [
{
name: "1 week from now",
value: "1_weeks;after;fromnow",
},
{
name: "2 weeks from now",
value: "2_weeks;after;fromnow",
},
{
name: "1 month from now",
value: "1_months;after;fromnow",
},
{
name: "2 months from now",
value: "2_months;after;fromnow",
},
];
export const DATE_BEFORE_FILTER_OPTIONS = [
{
name: "1 week ago",
value: "1_weeks;before;fromnow",
},
{
name: "2 weeks ago",
value: "2_weeks;before;fromnow",
},
{
name: "1 month ago",
i18n_name: "date_filters.1_month_ago",
value: "1_months;before;fromnow",
},
];
export const PROJECT_CREATED_AT_FILTER_OPTIONS = [
{
name: "Today",
value: "today;custom;custom",
},
{
name: "Yesterday",
value: "yesterday;custom;custom",
},
{
name: "Last 7 days",
value: "last_7_days;custom;custom",
},
{
name: "Last 30 days",
value: "last_30_days;custom;custom",
},
];
-91
View File
@@ -1,91 +0,0 @@
import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
export enum EInboxIssueCurrentTab {
OPEN = "open",
CLOSED = "closed",
}
export enum EInboxIssueStatus {
PENDING = -2,
DECLINED = -1,
SNOOZED = 0,
ACCEPTED = 1,
DUPLICATE = 2,
}
export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
export type TInboxIssueStatus = EInboxIssueStatus;
export type TInboxIssue = {
id: string;
status: TInboxIssueStatus;
snoozed_till: Date | null;
duplicate_to: string | undefined;
source: string;
issue: TIssue;
created_by: string;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
};
export const INBOX_STATUS: {
key: string;
status: TInboxIssueStatus;
i18n_title: string;
i18n_description: () => string;
}[] = [
{
key: "pending",
i18n_title: "inbox_issue.status.pending.title",
status: EInboxIssueStatus.PENDING,
i18n_description: () => `inbox_issue.status.pending.description`,
},
{
key: "declined",
i18n_title: "inbox_issue.status.declined.title",
status: EInboxIssueStatus.DECLINED,
i18n_description: () => `inbox_issue.status.declined.description`,
},
{
key: "snoozed",
i18n_title: "inbox_issue.status.snoozed.title",
status: EInboxIssueStatus.SNOOZED,
i18n_description: () => `inbox_issue.status.snoozed.description`,
},
{
key: "accepted",
i18n_title: "inbox_issue.status.accepted.title",
status: EInboxIssueStatus.ACCEPTED,
i18n_description: () => `inbox_issue.status.accepted.description`,
},
{
key: "duplicate",
i18n_title: "inbox_issue.status.duplicate.title",
status: EInboxIssueStatus.DUPLICATE,
i18n_description: () => `inbox_issue.status.duplicate.description`,
},
];
export const INBOX_ISSUE_ORDER_BY_OPTIONS = [
{
key: "issue__created_at",
i18n_label: "inbox_issue.order_by.created_at",
},
{
key: "issue__updated_at",
i18n_label: "inbox_issue.order_by.updated_at",
},
{
key: "issue__sequence_id",
i18n_label: "inbox_issue.order_by.id",
},
];
export const INBOX_ISSUE_SORT_BY_OPTIONS = [
{
key: "asc",
i18n_label: "common.sort.asc",
},
{
key: "desc",
i18n_label: "common.sort.desc",
},
];
-6
View File
@@ -11,12 +11,6 @@ export * from "./issue";
export * from "./metadata";
export * from "./state";
export * from "./swr";
export * from "./tab-indices";
export * from "./user";
export * from "./workspace";
export * from "./stickies";
export * from "./project";
export * from "./views";
export * from "./inbox";
export * from "./profile";
export * from "./workspace-drafts";
+185
View File
@@ -0,0 +1,185 @@
import { List, Kanban } from "lucide-react";
export const ALL_ISSUES = "All Issues";
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
export type TIssueFilterKeys = "priority" | "state" | "labels";
export type TIssueLayout =
| "list"
| "kanban"
| "calendar"
| "spreadsheet"
| "gantt";
export type TIssueFilterPriorityObject = {
key: TIssuePriorities;
title: string;
className: string;
icon: string;
};
export enum EIssueGroupByToServerOptions {
"state" = "state_id",
"priority" = "priority",
"labels" = "labels__id",
"state_detail.group" = "state__group",
"assignees" = "assignees__id",
"cycle" = "cycle_id",
"module" = "issue_module__module_id",
"target_date" = "target_date",
"project" = "project_id",
"created_by" = "created_by",
"team_project" = "project_id",
}
export enum EIssueGroupBYServerToProperty {
"state_id" = "state_id",
"priority" = "priority",
"labels__id" = "label_ids",
"state__group" = "state__group",
"assignees__id" = "assignee_ids",
"cycle_id" = "cycle_id",
"issue_module__module_id" = "module_ids",
"target_date" = "target_date",
"project_id" = "project_id",
"created_by" = "created_by",
}
export enum EServerGroupByToFilterOptions {
"state_id" = "state",
"priority" = "priority",
"labels__id" = "labels",
"state__group" = "state_group",
"assignees__id" = "assignees",
"cycle_id" = "cycle",
"issue_module__module_id" = "module",
"target_date" = "target_date",
"project_id" = "project",
"created_by" = "created_by",
}
export enum EIssueServiceType {
ISSUES = "issues",
EPICS = "epics",
}
export enum EIssueLayoutTypes {
LIST = "list",
KANBAN = "kanban",
CALENDAR = "calendar",
GANTT = "gantt_chart",
SPREADSHEET = "spreadsheet",
}
export enum EIssuesStoreType {
GLOBAL = "GLOBAL",
PROFILE = "PROFILE",
TEAM = "TEAM",
PROJECT = "PROJECT",
CYCLE = "CYCLE",
MODULE = "MODULE",
TEAM_VIEW = "TEAM_VIEW",
PROJECT_VIEW = "PROJECT_VIEW",
ARCHIVED = "ARCHIVED",
DRAFT = "DRAFT",
DEFAULT = "DEFAULT",
WORKSPACE_DRAFT = "WORKSPACE_DRAFT",
EPIC = "EPIC",
}
export enum EIssueFilterType {
FILTERS = "filters",
DISPLAY_FILTERS = "display_filters",
DISPLAY_PROPERTIES = "display_properties",
KANBAN_FILTERS = "kanban_filters",
}
export enum EIssueCommentAccessSpecifier {
EXTERNAL = "EXTERNAL",
INTERNAL = "INTERNAL",
}
export enum EIssueListRow {
HEADER = "HEADER",
ISSUE = "ISSUE",
NO_ISSUES = "NO_ISSUES",
QUICK_ADD = "QUICK_ADD",
}
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>;
} = {
list: {
filters: ["priority", "state", "labels"],
},
kanban: {
filters: ["priority", "state", "labels"],
},
calendar: {
filters: ["priority", "state", "labels"],
},
spreadsheet: {
filters: ["priority", "state", "labels"],
},
gantt: {
filters: ["priority", "state", "labels"],
},
};
export const ISSUE_PRIORITIES: {
key: TIssuePriorities;
title: string;
}[] = [
{ key: "urgent", title: "Urgent" },
{ key: "high", title: "High" },
{ key: "medium", title: "Medium" },
{ key: "low", title: "Low" },
{ key: "none", title: "None" },
];
export const ISSUE_PRIORITY_FILTERS: TIssueFilterPriorityObject[] = [
{
key: "urgent",
title: "Urgent",
className: "bg-red-500 border-red-500 text-white",
icon: "error",
},
{
key: "high",
title: "High",
className: "text-orange-500 border-custom-border-300",
icon: "signal_cellular_alt",
},
{
key: "medium",
title: "Medium",
className: "text-yellow-500 border-custom-border-300",
icon: "signal_cellular_alt_2_bar",
},
{
key: "low",
title: "Low",
className: "text-green-500 border-custom-border-300",
icon: "signal_cellular_alt_1_bar",
},
{
key: "none",
title: "None",
className: "text-gray-500 border-custom-border-300",
icon: "block",
},
];
export const SITES_ISSUE_LAYOUTS: {
key: TIssueLayout;
title: string;
icon: any;
}[] = [
{ key: "list", title: "List", icon: List },
{ key: "kanban", title: "Kanban", icon: Kanban },
// { key: "calendar", title: "Calendar", icon: Calendar },
// { key: "spreadsheet", title: "Spreadsheet", icon: Sheet },
// { key: "gantt", title: "Gantt chart", icon: GanttChartSquare },
];
-217
View File
@@ -1,217 +0,0 @@
import {
TIssueGroupByOptions,
TIssueOrderByOptions,
IIssueDisplayProperties,
} from "@plane/types";
export const ALL_ISSUES = "All Issues";
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
export type TIssueFilterPriorityObject = {
key: TIssuePriorities;
titleTranslationKey: string;
className: string;
icon: string;
};
export enum EIssueGroupByToServerOptions {
"state" = "state_id",
"priority" = "priority",
"labels" = "labels__id",
"state_detail.group" = "state__group",
"assignees" = "assignees__id",
"cycle" = "cycle_id",
"module" = "issue_module__module_id",
"target_date" = "target_date",
"project" = "project_id",
"created_by" = "created_by",
"team_project" = "project_id",
}
export enum EIssueGroupBYServerToProperty {
"state_id" = "state_id",
"priority" = "priority",
"labels__id" = "label_ids",
"state__group" = "state__group",
"assignees__id" = "assignee_ids",
"cycle_id" = "cycle_id",
"issue_module__module_id" = "module_ids",
"target_date" = "target_date",
"project_id" = "project_id",
"created_by" = "created_by",
}
export enum EIssueServiceType {
ISSUES = "issues",
EPICS = "epics",
}
export enum EIssuesStoreType {
GLOBAL = "GLOBAL",
PROFILE = "PROFILE",
TEAM = "TEAM",
PROJECT = "PROJECT",
CYCLE = "CYCLE",
MODULE = "MODULE",
TEAM_VIEW = "TEAM_VIEW",
PROJECT_VIEW = "PROJECT_VIEW",
ARCHIVED = "ARCHIVED",
DRAFT = "DRAFT",
DEFAULT = "DEFAULT",
WORKSPACE_DRAFT = "WORKSPACE_DRAFT",
EPIC = "EPIC",
}
export enum EIssueCommentAccessSpecifier {
EXTERNAL = "EXTERNAL",
INTERNAL = "INTERNAL",
}
export enum EIssueListRow {
HEADER = "HEADER",
ISSUE = "ISSUE",
NO_ISSUES = "NO_ISSUES",
QUICK_ADD = "QUICK_ADD",
}
export const ISSUE_PRIORITIES: {
key: TIssuePriorities;
title: string;
}[] = [
{
key: "urgent",
title: "Urgent",
},
{
key: "high",
title: "High",
},
{
key: "medium",
title: "Medium",
},
{
key: "low",
title: "Low",
},
{
key: "none",
title: "None",
},
];
export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
"state",
"priority",
"assignees",
"labels",
"module",
"cycle",
];
export type TCreateModalStoreTypes =
| EIssuesStoreType.TEAM
| EIssuesStoreType.PROJECT
| EIssuesStoreType.TEAM_VIEW
| EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.PROFILE
| EIssuesStoreType.CYCLE
| EIssuesStoreType.MODULE
| EIssuesStoreType.EPIC;
export const ISSUE_GROUP_BY_OPTIONS: {
key: TIssueGroupByOptions;
titleTranslationKey: string;
}[] = [
{ key: "state", titleTranslationKey: "common.states" },
{ key: "state_detail.group", titleTranslationKey: "common.state_groups" },
{ key: "priority", titleTranslationKey: "common.priority" },
{ key: "team_project", titleTranslationKey: "common.team_project" }, // required this on team issues
{ key: "project", titleTranslationKey: "common.project" }, // required this on my issues
{ key: "cycle", titleTranslationKey: "common.cycle" }, // required this on my issues
{ key: "module", titleTranslationKey: "common.module" }, // required this on my issues
{ key: "labels", titleTranslationKey: "common.labels" },
{ key: "assignees", titleTranslationKey: "common.assignees" },
{ key: "created_by", titleTranslationKey: "common.created_by" },
{ key: null, titleTranslationKey: "common.none" },
];
export const ISSUE_ORDER_BY_OPTIONS: {
key: TIssueOrderByOptions;
titleTranslationKey: string;
}[] = [
{ key: "sort_order", titleTranslationKey: "common.order_by.manual" },
{ key: "-created_at", titleTranslationKey: "common.order_by.last_created" },
{ key: "-updated_at", titleTranslationKey: "common.order_by.last_updated" },
{ key: "start_date", titleTranslationKey: "common.order_by.start_date" },
{ key: "target_date", titleTranslationKey: "common.order_by.due_date" },
{ key: "-priority", titleTranslationKey: "common.priority" },
];
export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] =
[
"assignee",
"start_date",
"due_date",
"labels",
"key",
"priority",
"state",
"sub_issue_count",
"link",
"attachment_count",
"estimate",
"created_on",
"updated_on",
"modules",
"cycle",
"issue_type",
];
export const ISSUE_DISPLAY_PROPERTIES: {
key: keyof IIssueDisplayProperties;
titleTranslationKey: string;
}[] = [
{
key: "key",
titleTranslationKey: "issue.display.properties.id",
},
{
key: "issue_type",
titleTranslationKey: "issue.display.properties.issue_type",
},
{
key: "assignee",
titleTranslationKey: "common.assignee",
},
{
key: "start_date",
titleTranslationKey: "common.order_by.start_date",
},
{
key: "due_date",
titleTranslationKey: "common.order_by.due_date",
},
{ key: "labels", titleTranslationKey: "common.labels" },
{
key: "priority",
titleTranslationKey: "common.priority",
},
{ key: "state", titleTranslationKey: "common.state" },
{
key: "sub_issue_count",
titleTranslationKey: "issue.display.properties.sub_issue_count",
},
{
key: "attachment_count",
titleTranslationKey: "issue.display.properties.attachment_count",
},
{ key: "link", titleTranslationKey: "common.link" },
{
key: "estimate",
titleTranslationKey: "common.estimate",
},
{ key: "modules", titleTranslationKey: "common.module" },
{ key: "cycle", titleTranslationKey: "common.cycle" },
];
-530
View File
@@ -1,530 +0,0 @@
import {
ILayoutDisplayFiltersOptions,
TIssueActivityComment,
} from "@plane/types";
import {
TIssueFilterPriorityObject,
ISSUE_DISPLAY_PROPERTIES_KEYS,
EIssuesStoreType,
} from "./common";
import { TIssueLayout } from "./layout";
export type TIssueFilterKeys = "priority" | "state" | "labels";
export enum EServerGroupByToFilterOptions {
"state_id" = "state",
"priority" = "priority",
"labels__id" = "labels",
"state__group" = "state_group",
"assignees__id" = "assignees",
"cycle_id" = "cycle",
"issue_module__module_id" = "module",
"target_date" = "target_date",
"project_id" = "project",
"created_by" = "created_by",
}
export enum EIssueFilterType {
FILTERS = "filters",
DISPLAY_FILTERS = "display_filters",
DISPLAY_PROPERTIES = "display_properties",
KANBAN_FILTERS = "kanban_filters",
}
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>;
} = {
list: {
filters: ["priority", "state", "labels"],
},
kanban: {
filters: ["priority", "state", "labels"],
},
calendar: {
filters: ["priority", "state", "labels"],
},
spreadsheet: {
filters: ["priority", "state", "labels"],
},
gantt: {
filters: ["priority", "state", "labels"],
},
};
export const ISSUE_PRIORITY_FILTERS: TIssueFilterPriorityObject[] = [
{
key: "urgent",
titleTranslationKey: "issue.priority.urgent",
className: "bg-red-500 border-red-500 text-white",
icon: "error",
},
{
key: "high",
titleTranslationKey: "issue.priority.high",
className: "text-orange-500 border-custom-border-300",
icon: "signal_cellular_alt",
},
{
key: "medium",
titleTranslationKey: "issue.priority.medium",
className: "text-yellow-500 border-custom-border-300",
icon: "signal_cellular_alt_2_bar",
},
{
key: "low",
titleTranslationKey: "issue.priority.low",
className: "text-green-500 border-custom-border-300",
icon: "signal_cellular_alt_1_bar",
},
{
key: "none",
titleTranslationKey: "common.none",
className: "text-gray-500 border-custom-border-300",
icon: "block",
},
];
export type TFiltersByLayout = {
[layoutType: string]: ILayoutDisplayFiltersOptions;
};
export type TIssueFiltersToDisplayByPageType = {
[pageType: string]: TFiltersByLayout;
};
export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
profile_issues: {
list: {
filters: [
"priority",
"state_group",
"labels",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels", null],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
kanban: {
filters: [
"priority",
"state_group",
"labels",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels"],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
archived_issues: {
list: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"cycle",
"module",
"state_detail.group",
"priority",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
draft_issues: {
list: {
filters: [
"priority",
"state_group",
"cycle",
"module",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state_detail.group",
"cycle",
"module",
"priority",
"project",
"labels",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
kanban: {
filters: [
"priority",
"state_group",
"cycle",
"module",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state_detail.group",
"cycle",
"module",
"priority",
"project",
"labels",
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
my_issues: {
spreadsheet: {
filters: [
"priority",
"state_group",
"labels",
"assignees",
"created_by",
"subscriber",
"project",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: [],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
list: {
filters: [
"priority",
"state_group",
"labels",
"assignees",
"created_by",
"subscriber",
"project",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
type: [null, "active", "backlog"],
},
extra_options: {
access: false,
values: [],
},
},
},
issues: {
list: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
kanban: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
],
sub_group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
"target_date",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
calendar: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"issue_type",
],
display_properties: ["key", "issue_type"],
display_filters: {
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
spreadsheet: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
gantt_chart: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ["key", "issue_type"],
display_filters: {
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
},
};
export const ISSUE_STORE_TO_FILTERS_MAP: Partial<
Record<EIssuesStoreType, TFiltersByLayout>
> = {
[EIssuesStoreType.PROJECT]: ISSUE_DISPLAY_FILTERS_BY_PAGE.issues,
};
export enum EActivityFilterType {
ACTIVITY = "ACTIVITY",
COMMENT = "COMMENT",
}
export type TActivityFilters = EActivityFilterType;
export const ACTIVITY_FILTER_TYPE_OPTIONS: Record<
TActivityFilters,
{ labelTranslationKey: string }
> = {
[EActivityFilterType.ACTIVITY]: {
labelTranslationKey: "common.updates",
},
[EActivityFilterType.COMMENT]: {
labelTranslationKey: "common.comments",
},
};
export type TActivityFilterOption = {
key: TActivityFilters;
labelTranslationKey: string;
isSelected: boolean;
onClick: () => void;
};
export const defaultActivityFilters: TActivityFilters[] = [
EActivityFilterType.ACTIVITY,
EActivityFilterType.COMMENT,
];
export const filterActivityOnSelectedFilters = (
activity: TIssueActivityComment[],
filters: TActivityFilters[]
): TIssueActivityComment[] =>
activity.filter((activity) =>
filters.includes(activity.activity_type as TActivityFilters)
);
export const ENABLE_ISSUE_DEPENDENCIES = false;
-3
View File
@@ -1,3 +0,0 @@
export * from "./common";
export * from "./filter";
export * from "./layout";
-76
View File
@@ -1,76 +0,0 @@
export type TIssueLayout =
| "list"
| "kanban"
| "calendar"
| "spreadsheet"
| "gantt";
export enum EIssueLayoutTypes {
LIST = "list",
KANBAN = "kanban",
CALENDAR = "calendar",
GANTT = "gantt_chart",
SPREADSHEET = "spreadsheet",
}
export type TIssueLayoutMap = Record<
EIssueLayoutTypes,
{
key: EIssueLayoutTypes;
i18n_title: string;
i18n_label: string;
}
>;
export const SITES_ISSUE_LAYOUTS: {
key: TIssueLayout;
titleTranslationKey: string;
icon: any;
}[] = [
{
key: "list",
icon: "List",
titleTranslationKey: "issue.layouts.list",
},
{
key: "kanban",
icon: "Kanban",
titleTranslationKey: "issue.layouts.kanban",
},
// { key: "calendar", title: "Calendar", icon: Calendar },
// { key: "spreadsheet", title: "Spreadsheet", icon: Sheet },
// { key: "gantt", title: "Gantt chart", icon: GanttChartSquare },
];
export const ISSUE_LAYOUT_MAP: TIssueLayoutMap = {
[EIssueLayoutTypes.LIST]: {
key: EIssueLayoutTypes.LIST,
i18n_title: "issue.layouts.title.list",
i18n_label: "issue.layouts.list",
},
[EIssueLayoutTypes.KANBAN]: {
key: EIssueLayoutTypes.KANBAN,
i18n_title: "issue.layouts.title.kanban",
i18n_label: "issue.layouts.kanban",
},
[EIssueLayoutTypes.CALENDAR]: {
key: EIssueLayoutTypes.CALENDAR,
i18n_title: "issue.layouts.title.calendar",
i18n_label: "issue.layouts.calendar",
},
[EIssueLayoutTypes.SPREADSHEET]: {
key: EIssueLayoutTypes.SPREADSHEET,
i18n_title: "issue.layouts.title.spreadsheet",
i18n_label: "issue.layouts.spreadsheet",
},
[EIssueLayoutTypes.GANTT]: {
key: EIssueLayoutTypes.GANTT,
i18n_title: "issue.layouts.title.gantt",
i18n_label: "issue.layouts.gantt",
},
};
export const ISSUE_LAYOUTS: {
key: EIssueLayoutTypes;
i18n_title: string;
}[] = Object.values(ISSUE_LAYOUT_MAP);
-23
View File
@@ -1,23 +0,0 @@
export enum EViewAccess {
PRIVATE,
PUBLIC,
}
export const VIEW_ACCESS_SPECIFIERS: {
key: EViewAccess;
i18n_label: string;
}[] = [
{ key: EViewAccess.PUBLIC, i18n_label: "project_view.access.public" },
{ key: EViewAccess.PRIVATE, i18n_label: "project_view.access.private" },
];
export const VIEW_SORTING_KEY_OPTIONS = [
{ key: "name", i18n_label: "project_view.sort_by.name" },
{ key: "created_at", i18n_label: "project_view.sort_by.created_at" },
{ key: "updated_at", i18n_label: "project_view.sort_by.updated_at" },
];
export const VIEW_SORT_BY_OPTIONS = [
{ key: "asc", i18n_label: "common.order_by.asc" },
{ key: "desc", i18n_label: "common.order_by.desc" },
];
-182
View File
@@ -1,6 +1,3 @@
import { TStaticViewTypes } from "@plane/types";
import { EUserWorkspaceRoles } from "./user";
export const ORGANIZATION_SIZE = [
"Just myself", // TODO: translate
"2-10",
@@ -77,182 +74,3 @@ export const RESTRICTED_URLS = [
"instances",
"instance",
];
export const WORKSPACE_SETTINGS = {
general: {
key: "general",
i18n_label: "workspace_settings.settings.general.title",
href: `/settings`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) =>
pathname === `${baseUrl}/settings/`,
},
members: {
key: "members",
i18n_label: "workspace_settings.settings.members.title",
href: `/settings/members`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) =>
pathname === `${baseUrl}/settings/members/`,
},
"billing-and-plans": {
key: "billing-and-plans",
i18n_label: "workspace_settings.settings.billing_and_plans.title",
href: `/settings/billing`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) =>
pathname === `${baseUrl}/settings/billing/`,
},
export: {
key: "export",
i18n_label: "workspace_settings.settings.exports.title",
href: `/settings/exports`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) =>
pathname === `${baseUrl}/settings/exports/`,
},
webhooks: {
key: "webhooks",
i18n_label: "workspace_settings.settings.webhooks.title",
href: `/settings/webhooks`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) =>
pathname === `${baseUrl}/settings/webhooks/`,
},
"api-tokens": {
key: "api-tokens",
i18n_label: "workspace_settings.settings.webhooks.title",
href: `/settings/api-tokens`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) =>
pathname === `${baseUrl}/settings/api-tokens/`,
},
};
export const WORKSPACE_SETTINGS_LINKS: {
key: string;
i18n_label: string;
href: string;
access: EUserWorkspaceRoles[];
highlight: (pathname: string, baseUrl: string) => boolean;
}[] = [
WORKSPACE_SETTINGS["general"],
WORKSPACE_SETTINGS["members"],
WORKSPACE_SETTINGS["billing-and-plans"],
WORKSPACE_SETTINGS["export"],
WORKSPACE_SETTINGS["webhooks"],
WORKSPACE_SETTINGS["api-tokens"],
];
export const ROLE = {
[EUserWorkspaceRoles.GUEST]: "Guest",
[EUserWorkspaceRoles.MEMBER]: "Member",
[EUserWorkspaceRoles.ADMIN]: "Admin",
};
export const ROLE_DETAILS = {
[EUserWorkspaceRoles.GUEST]: {
i18n_title: "role_details.guest.title",
i18n_description: "role_details.guest.description",
},
[EUserWorkspaceRoles.MEMBER]: {
i18n_title: "role_details.member.title",
i18n_description: "role_details.member.description",
},
[EUserWorkspaceRoles.ADMIN]: {
i18n_title: "role_details.admin.title",
i18n_description: "role_details.admin.description",
},
};
export const USER_ROLES = [
{
value: "Product / Project Manager",
i18n_label: "user_roles.product_or_project_manager",
},
{
value: "Development / Engineering",
i18n_label: "user_roles.development_or_engineering",
},
{
value: "Founder / Executive",
i18n_label: "user_roles.founder_or_executive",
},
{
value: "Freelancer / Consultant",
i18n_label: "user_roles.freelancer_or_consultant",
},
{ value: "Marketing / Growth", i18n_label: "user_roles.marketing_or_growth" },
{
value: "Sales / Business Development",
i18n_label: "user_roles.sales_or_business_development",
},
{
value: "Support / Operations",
i18n_label: "user_roles.support_or_operations",
},
{
value: "Student / Professor",
i18n_label: "user_roles.student_or_professor",
},
{ value: "Human Resources", i18n_label: "user_roles.human_resources" },
{ value: "Other", i18n_label: "user_roles.other" },
];
export const IMPORTERS_LIST = [
{
provider: "github",
type: "import",
i18n_title: "importer.github.title",
i18n_description: "importer.github.description",
},
{
provider: "jira",
type: "import",
i18n_title: "importer.jira.title",
i18n_description: "importer.jira.description",
},
];
export const EXPORTERS_LIST = [
{
provider: "csv",
type: "export",
i18n_title: "exporter.csv.title",
i18n_description: "exporter.csv.description",
},
{
provider: "xlsx",
type: "export",
i18n_title: "exporter.excel.title",
i18n_description: "exporter.csv.description",
},
{
provider: "json",
type: "export",
i18n_title: "exporter.json.title",
i18n_description: "exporter.csv.description",
},
];
export const DEFAULT_GLOBAL_VIEWS_LIST: {
key: TStaticViewTypes;
i18n_label: string;
}[] = [
{
key: "all-issues",
i18n_label: "default_global_view.all_issues",
},
{
key: "assigned",
i18n_label: "default_global_view.assigned",
},
{
key: "created",
i18n_label: "default_global_view.created",
},
{
key: "subscribed",
i18n_label: "default_global_view.subscribed",
},
];
@@ -16,6 +16,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
const {
onTransaction,
aiHandler,
bubbleMenuEnabled = true,
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
@@ -75,8 +76,9 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
return (
<PageRenderer
displayConfig={displayConfig}
aiHandler={aiHandler}
bubbleMenuEnabled={bubbleMenuEnabled}
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassNames}
id={id}
@@ -15,12 +15,13 @@ import { Editor, ReactRenderer } from "@tiptap/react";
// components
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
import { LinkView, LinkViewProps } from "@/components/links";
import { AIFeaturesMenu, BlockMenu } from "@/components/menus";
import { AIFeaturesMenu, BlockMenu, EditorBubbleMenu } from "@/components/menus";
// types
import { TAIHandler, TDisplayConfig } from "@/types";
type IPageRenderer = {
aiHandler?: TAIHandler;
bubbleMenuEnabled: boolean;
displayConfig: TDisplayConfig;
editor: Editor;
editorContainerClassName: string;
@@ -29,7 +30,7 @@ type IPageRenderer = {
};
export const PageRenderer = (props: IPageRenderer) => {
const { aiHandler, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
const { aiHandler, bubbleMenuEnabled, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
// states
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
const [isOpen, setIsOpen] = useState(false);
@@ -141,6 +142,7 @@ export const PageRenderer = (props: IPageRenderer) => {
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
{editor.isEditable && (
<div>
{bubbleMenuEnabled && <EditorBubbleMenu editor={editor} />}
<BlockMenu editor={editor} />
<AIFeaturesMenu menu={aiHandler?.menu} />
</div>
@@ -69,6 +69,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
return (
<PageRenderer
bubbleMenuEnabled={false}
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassName}
@@ -1,6 +1,6 @@
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react";
import { Editor } from "@tiptap/core";
import { Check, Link, Trash } from "lucide-react";
import { Check, Link, Trash2 } from "lucide-react";
import { Dispatch, FC, SetStateAction, useCallback, useRef, useState } from "react";
// plane utils
import { cn } from "@plane/utils";
// helpers
@@ -15,22 +15,26 @@ type Props = {
export const BubbleMenuLinkSelector: FC<Props> = (props) => {
const { editor, isOpen, setIsOpen } = props;
// states
const [error, setError] = useState(false);
// refs
const inputRef = useRef<HTMLInputElement>(null);
const onLinkSubmit = useCallback(() => {
const handleLinkSubmit = useCallback(() => {
const input = inputRef.current;
const url = input?.value;
if (url && isValidHttpUrl(url)) {
if (!input) return;
let url = input.value;
if (!url) return;
if (!url.startsWith("http")) url = `http://${url}`;
if (isValidHttpUrl(url)) {
setLinkEditor(editor, url);
setIsOpen(false);
setError(false);
} else {
setError(true);
}
}, [editor, inputRef, setIsOpen]);
useEffect(() => {
inputRef.current && inputRef.current?.focus();
});
return (
<div className="relative h-full">
<button
@@ -47,52 +51,62 @@ export const BubbleMenuLinkSelector: FC<Props> = (props) => {
e.stopPropagation();
}}
>
<span>Link</span>
Link
<Link className="flex-shrink-0 size-3" />
</button>
{isOpen && (
<div
className="dow-xl fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 animate-in fade-in slide-in-from-top-1"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onLinkSubmit();
}
}}
>
<input
ref={inputRef}
type="url"
placeholder="Paste a link"
onClick={(e) => {
e.stopPropagation();
}}
className="flex-1 border-r border-custom-border-300 bg-custom-background-100 p-1 text-sm outline-none placeholder:text-custom-text-400"
defaultValue={editor.getAttributes("link").href || ""}
/>
{editor.getAttributes("link").href ? (
<button
type="button"
className="flex items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800"
onClick={(e) => {
unsetLinkEditor(editor);
setIsOpen(false);
e.stopPropagation();
<div className="fixed top-full z-[99999] mt-1 w-60 animate-in fade-in slide-in-from-top-1 rounded bg-custom-background-100 shadow-custom-shadow-rg">
<div
className={cn("flex rounded border border-custom-border-300 transition-colors", {
"border-red-500": error,
})}
>
<input
ref={inputRef}
type="url"
placeholder="Enter or paste a link"
onClick={(e) => e.stopPropagation()}
className="flex-1 border-r border-custom-border-300 bg-custom-background-100 py-2 px-1.5 text-xs outline-none placeholder:text-custom-text-400 rounded"
defaultValue={editor.getAttributes("link").href || ""}
onKeyDown={(e) => {
setError(false);
if (e.key === "Enter") {
e.preventDefault();
handleLinkSubmit();
}
}}
>
<Trash className="h-4 w-4" />
</button>
) : (
<button
className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90"
type="button"
onClick={(e) => {
onLinkSubmit();
e.stopPropagation();
}}
>
<Check className="h-4 w-4" />
</button>
onFocus={() => setError(false)}
autoFocus
/>
{editor.getAttributes("link").href ? (
<button
type="button"
className="grid place-items-center rounded-sm p-1 text-red-500 hover:bg-red-500/20 transition-all"
onClick={(e) => {
unsetLinkEditor(editor);
setIsOpen(false);
e.stopPropagation();
}}
>
<Trash2 className="size-4" />
</button>
) : (
<button
type="button"
className="h-full aspect-square grid place-items-center p-1 rounded-sm text-custom-text-300 hover:bg-custom-background-80 transition-all"
onClick={(e) => {
e.stopPropagation();
handleLinkSubmit();
}}
>
<Check className="size-4" />
</button>
)}
</div>
{error && (
<p className="text-xs text-red-500 my-1 px-2 pointer-events-none animate-in fade-in slide-in-from-top-0">
Please enter a valid URL
</p>
)}
</div>
)}
@@ -93,6 +93,19 @@ export const CustomColorExtension = Mark.create({
};
},
addStorage() {
return {
markdown: {
serialize: {
open: "",
close: "",
mixable: true,
expelEnclosingWhitespace: true,
},
},
};
},
parseHTML() {
return [
{
@@ -134,10 +134,6 @@ const SideMenu = (options: SideMenuPluginProps) => {
rect.left -= 8;
}
if (node.parentElement?.matches("td") || node.parentElement?.matches("th")) {
rect.left += 8;
}
rect.width = options.dragHandleWidth;
if (!editorSideMenu) return;
@@ -39,7 +39,12 @@ export interface TableOptions {
declare module "@tiptap/core" {
interface Commands<ReturnType> {
table: {
insertTable: (options?: { rows?: number; cols?: number; withHeaderRow?: boolean }) => ReturnType;
insertTable: (options?: {
rows?: number;
cols?: number;
withHeaderRow?: boolean;
columnWidth?: number;
}) => ReturnType;
addColumnBefore: () => ReturnType;
addColumnAfter: () => ReturnType;
deleteColumn: () => ReturnType;
@@ -108,9 +113,9 @@ export const Table = Node.create({
addCommands() {
return {
insertTable:
({ rows = 3, cols = 3, withHeaderRow = false } = {}) =>
({ rows = 3, cols = 3, withHeaderRow = false, columnWidth = 150 } = {}) =>
({ tr, dispatch, editor }) => {
const node = createTable(editor.schema, rows, cols, withHeaderRow);
const node = createTable(editor.schema, rows, cols, withHeaderRow, undefined, columnWidth);
if (dispatch) {
const offset = tr.selection.anchor + 1;
@@ -2,11 +2,12 @@ import { Fragment, Node as ProsemirrorNode, NodeType } from "@tiptap/pm/model";
export function createCell(
cellType: NodeType,
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
attrs?: Record<string, any>
): ProsemirrorNode | null | undefined {
if (cellContent) {
return cellType.createChecked(null, cellContent);
return cellType.createChecked(attrs, cellContent);
}
return cellType.createAndFill();
return cellType.createAndFill(attrs);
}
@@ -8,21 +8,22 @@ export function createTable(
rowsCount: number,
colsCount: number,
withHeaderRow: boolean,
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
columnWidth: number = 100
): ProsemirrorNode {
const types = getTableNodeTypes(schema);
const headerCells: ProsemirrorNode[] = [];
const cells: ProsemirrorNode[] = [];
for (let index = 0; index < colsCount; index += 1) {
const cell = createCell(types.cell, cellContent);
const cell = createCell(types.cell, cellContent, { colwidth: [columnWidth] });
if (cell) {
cells.push(cell);
}
if (withHeaderRow) {
const headerCell = createCell(types.header_cell, cellContent);
const headerCell = createCell(types.header_cell, cellContent, { colwidth: [columnWidth] });
if (headerCell) {
headerCells.push(headerCell);
@@ -138,8 +138,9 @@ export const insertTableCommand = (editor: Editor, range?: Range) => {
}
}
}
if (range) editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3 }).run();
else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3 }).run();
if (range)
editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3, columnWidth: 150 }).run();
else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3, columnWidth: 150 }).run();
};
export const insertImage = ({
@@ -88,16 +88,18 @@ export const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
const elements = document.elementsFromPoint(coords.x, coords.y);
for (const elem of elements) {
// Check for table wrapper first
if (elem.matches(".table-wrapper")) {
return elem;
}
if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) {
return elem;
}
// if the element is a <p> tag that is the first child of a td or th
if (
(elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) &&
elem?.textContent?.trim() !== ""
) {
return elem; // Return only if p tag is not empty in td or th
// Skip table cells
if (elem.closest(".table-wrapper")) {
continue;
}
// apply general selector
+2 -1
View File
@@ -138,8 +138,9 @@ export interface IRichTextEditor extends IEditorProps {
export interface ICollaborativeDocumentEditor
extends Omit<IEditorProps, "initialValue" | "onChange" | "onEnterKeyPress" | "value"> {
editable: boolean;
aiHandler?: TAIHandler;
bubbleMenuEnabled?: boolean;
editable: boolean;
embedHandler: TEmbedConfig;
handleEditorReady?: (value: boolean) => void;
id: string;
+7 -5
View File
@@ -408,12 +408,14 @@ p.editor-paragraph-block {
padding-top: 4px;
}
&:last-child {
padding-bottom: 4px;
}
&:not(td p.editor-paragraph-block, th p.editor-paragraph-block) {
&:last-child {
padding-bottom: 4px;
}
&:not(:last-child) {
padding-bottom: 8px;
&:not(:last-child) {
padding-bottom: 8px;
}
}
font-size: var(--font-size-regular);
+2 -2
View File
@@ -16,7 +16,7 @@
.table-wrapper table th {
min-width: 1em;
border: 1px solid rgba(var(--color-border-200));
padding: 10px 20px;
padding: 7px 10px;
vertical-align: top;
box-sizing: border-box;
position: relative;
@@ -48,7 +48,7 @@
/* table dropdown */
.table-wrapper table .column-resize-handle {
position: absolute;
right: -2px;
right: 0;
top: 0;
width: 2px;
height: 100%;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -966
View File
@@ -18,7 +18,6 @@
"add_members": "Ajouter des membres",
"remove_members": "Supprimer des membres",
"add": "Ajouter",
"adding": "Ajout",
"remove": "Supprimer",
"add_new": "Ajouter un nouveau",
"remove_selected": "Supprimer la sélection",
@@ -39,6 +38,7 @@
"deactivate_account_description": "Lors de la désactivation d'un compte, toutes les données et ressources de ce compte seront définitivement supprimées et ne pourront pas être récupérées.",
"profile_settings": "Paramètres du profil",
"your_account": "Votre compte",
"profile": "Profil",
"security": " Sécurité",
"activity": "Activité",
"appearance": "Apparence",
@@ -154,9 +154,11 @@
"stay_ahead_of_blockers_description": "Repérez les défis d'un projet à l'autre et identifiez les dépendances entre cycles qui ne sont pas évidentes depuis d'autres vues.",
"analytics": "Analyse",
"workspace_invites": "Invitations de l'espace de travail",
"workspace_settings": "Paramètres de l'espace de travail",
"enter_god_mode": "Entrer en mode dieu",
"workspace_logo": "Logo de l'espace de travail",
"new_issue": "Nouveau problème",
"home": "Accueil",
"your_work": "Votre travail",
"drafts": "Brouillons",
"projects": "Projets",
@@ -314,969 +316,5 @@
"remove_parent_issue": "Supprimer le problème parent",
"add_parent": "Ajouter un parent",
"loading_members": "Chargement des membres...",
"view_link_copied_to_clipboard": "Lien de vue copié dans le presse-papiers.",
"required": "Requis",
"optional": "Optionnel",
"Cancel": "Annuler",
"edit": "Modifier",
"open_in_new_tab": "Ouvrir dans un nouvel onglet",
"delete": "Supprimer",
"good": "Bonjour",
"morning": "matin",
"afternoon": "après-midi",
"evening": "soir",
"show_all": "Tout afficher",
"show_less": "Afficher moins",
"no_data_yet": "Pas encore de données",
"connections": "Connexions",
"project_view": {
"access": {
"public": "Public",
"private": "Privé"
},
"sort_by": {
"created_at": "Date de création",
"updated_at": "Date de modification",
"name": "Nom"
}
},
"toast": {
"success": "Succès !",
"error": "Erreur !"
},
"home": {
"empty": {
"create_project": {
"title": "Créer un projet",
"description": "La plupart des choses commencent par un projet dans Plane.",
"cta": "Commencer"
},
"invite_team": {
"title": "Inviter votre équipe",
"description": "Construisez, déployez et gérez avec vos collègues.",
"cta": "Les faire entrer"
},
"configure_workspace": {
"title": "Configurez votre espace de travail",
"description": "Activez ou désactivez des fonctionnalités ou allez plus loin.",
"cta": "Configurer cet espace"
},
"personalize_account": {
"title": "Faites de Plane le vôtre",
"description": "Choisissez votre photo, vos couleurs et plus encore.",
"cta": "Personnaliser maintenant"
},
"widgets": {
"title": "C'est calme sans widgets, activez-les",
"description": "Il semble que tous vos widgets soient désactivés. Activez-les maintenant pour améliorer votre expérience !",
"primary_button": {
"text": "Gérer les widgets"
}
}
},
"quick_links": {
"empty": "Enregistrez des liens vers des éléments de travail que vous souhaitez avoir à portée de main.",
"add": "Ajouter un lien rapide",
"title": "Lien rapide",
"title_plural": "Liens rapides",
"toasts": {
"created": {
"title": "Lien créé",
"message": "Le lien a été créé avec succès"
},
"not_created": {
"title": "Lien non créé",
"message": "Le lien n'a pas pu être créé"
},
"updated": {
"title": "Lien mis à jour",
"message": "Le lien a été mis à jour avec succès"
},
"not_updated": {
"title": "Lien non mis à jour",
"message": "Le lien n'a pas pu être mis à jour"
},
"removed": {
"title": "Lien supprimé",
"message": "Le lien a été supprimé avec succès"
},
"not_removed": {
"title": "Lien non supprimé",
"message": "Le lien n'a pas pu être supprimé"
}
}
},
"recents": {
"title": "Récents",
"empty": {
"project": "Vos projets récents apparaîtront ici une fois que vous en aurez visité un.",
"page": "Vos pages récentes apparaîtront ici une fois que vous en aurez visité une.",
"issue": "Vos problèmes récents apparaîtront ici une fois que vous en aurez visité un.",
"default": "Vous n'avez pas encore d'éléments récents."
},
"filters": {
"all": "Tous les éléments",
"projects": "Projets",
"pages": "Pages",
"issues": "Problèmes"
}
},
"my_stickies": {
"title": "Vos notes"
},
"new_at_plane": {
"title": "Nouveau chez Plane"
},
"quick_tutorial": {
"title": "Tutoriel rapide"
},
"widget": {
"reordered_successfully": "Widget réorganisé avec succès.",
"reordering_failed": "Une erreur s'est produite lors de la réorganisation du widget."
},
"manage_widgets": "Gérer les widgets",
"title": "Accueil",
"star_us_on_github": "Étoilez-nous sur GitHub"
},
"link": {
"modal": {
"url": {
"text": "URL",
"required": "L'URL n'est pas valide",
"placeholder": "Tapez ou collez une URL"
},
"title": {
"text": "Titre d'affichage",
"placeholder": "Comment souhaitez-vous voir ce lien"
}
}
},
"common": {
"all": "Tout",
"states": "États",
"state": "État",
"state_groups": "Groupes d'états",
"priority": "Priorité",
"team_project": "Projet d'équipe",
"project": "Projet",
"cycle": "Cycle",
"cycles": "Cycles",
"module": "Module",
"modules": "Modules",
"labels": "Étiquettes",
"assignees": "Assignés",
"assignee": "Assigné",
"created_by": "Créé par",
"none": "Aucun",
"link": "Lien",
"estimate": "Estimation",
"layout": "Disposition",
"filters": "Filtres",
"display": "Affichage",
"load_more": "Charger plus",
"no_matches_found": "Aucun résultat trouvé",
"activity": "Activité",
"analytics": "Analyses",
"success": "Succès",
"error": "Erreur",
"group_by": "Grouper par",
"search": "Rechercher",
"epic": "Épopée",
"issue": "Problème",
"warning": "Avertissement",
"updating": "Mise à jour",
"update": "Mettre à jour",
"creating": "Création",
"cancel": "Annuler",
"description": "Description",
"title": "Titre",
"order_by": {
"label": "Trier par",
"manual": "Manuel",
"last_created": "Dernière création",
"last_updated": "Dernière mise à jour",
"start_date": "Date de début",
"due_date": "Date d'échéance",
"asc": "Croissant",
"desc": "Décroissant"
},
"sort": {
"asc": "Croissant",
"desc": "Décroissant"
},
"comments": "Commentaires",
"updates": "Mises à jour"
},
"form": {
"title": {
"required": "Le titre est requis",
"max_length": "Le titre ne doit pas dépasser {length} caractères"
}
},
"entity": {
"grouping_title": "Groupement de {entity}",
"priority": "{entity}",
"all": "Tous les {entity}",
"drop_here_to_move": "Déposer ici pour déplacer {entity}"
},
"epic": {
"all": "Toutes les épopées",
"label": "{count, plural, one {Épopée} other {Épopées}}"
},
"issue": {
"label": "{count, plural, one {Problème} other {Problèmes}}",
"all": "Tous les problèmes",
"add": "Ajouter un problème",
"priority": {
"urgent": "Urgent",
"high": "Haute",
"medium": "Moyenne",
"low": "Basse"
},
"display": {
"properties": {
"label": "Propriétés d'affichage",
"id": "ID",
"issue_type": "Type de problème",
"sub_issue_count": "Nombre de sous-problèmes",
"attachment_count": "Nombre de pièces jointes"
},
"extra": {
"show_sub_issues": "Afficher les sous-problèmes",
"show_empty_groups": "Afficher les groupes vides"
}
},
"layouts": {
"ordered_by_label": "Cette disposition est triée par",
"list": "Liste",
"kanban": "Tableau",
"calendar": "Calendrier",
"spreadsheet": "Feuille de calcul",
"gantt": "Chronologie",
"title": {
"list": "Disposition en liste",
"kanban": "Disposition en tableau",
"calendar": "Disposition en calendrier",
"spreadsheet": "Disposition en feuille de calcul",
"gantt": "Disposition en chronologie"
}
},
"states": {
"active": "Actif",
"backlog": "Backlog"
},
"comments": {
"create": {
"success": "Commentaire créé avec succès",
"error": "Échec de la création du commentaire. Veuillez réessayer plus tard."
},
"update": {
"success": "Commentaire mis à jour avec succès",
"error": "Échec de la mise à jour du commentaire. Veuillez réessayer plus tard."
},
"remove": {
"success": "Commentaire supprimé avec succès",
"error": "Échec de la suppression du commentaire. Veuillez réessayer plus tard."
},
"upload": {
"error": "Échec du téléchargement du fichier. Veuillez réessayer plus tard."
}
}
},
"view": {
"create": {
"label": "Créer une vue"
},
"update": {
"label": "Mettre à jour la vue"
}
},
"inbox_issue": {
"status": {
"pending": {
"title": "En attente",
"description": "En attente"
},
"declined": {
"title": "Refusé",
"description": "Refusé"
},
"snoozed": {
"title": "Reporté",
"description": "{days, plural, one{# jour} other{# jours}} restants"
},
"accepted": {
"title": "Accepté",
"description": "Accepté"
},
"duplicate": {
"title": "Doublon",
"description": "Doublon"
}
},
"source": {
"in-app": "dans l'application"
},
"order_by": {
"created_at": "Créé le",
"updated_at": "Mis à jour le",
"id": "Identifiant"
}
},
"workspace_dashboard": {
"empty_state": {
"general": {
"title": "Vue d'ensemble de vos projets, activités et métriques",
"description": "Bienvenue sur Plane, nous sommes ravis de vous accueillir. Créez votre premier projet et suivez vos tâches, et cette page se transformera en un espace qui vous aidera à progresser. Les administrateurs verront également des éléments pour aider leur équipe à progresser.",
"primary_button": {
"text": "Créez votre premier projet",
"comic": {
"title": "Tout commence par un projet sur Plane",
"description": "Un projet peut être une feuille de route produit, une campagne marketing ou le lancement d'une nouvelle voiture."
}
}
}
}
},
"workspace_analytics": {
"empty_state": {
"general": {
"title": "Suivez les progrès, les charges de travail et les allocations. Repérez les tendances, éliminez les obstacles et accélérez le travail",
"description": "Visualisez l'étendue par rapport à la demande, les estimations et l'expansion des objectifs. Obtenez des performances par membre et par équipe, et assurez-vous que votre projet respecte les délais.",
"primary_button": {
"text": "Commencez votre premier projet",
"comic": {
"title": "Les analyses fonctionnent mieux avec Cycles + Modules",
"description": "D'abord, cadrez vos tâches dans des Cycles et, si possible, regroupez les tâches qui s'étendent sur plus d'un cycle dans des Modules. Consultez-les dans la navigation à gauche."
}
}
}
}
},
"workspace_projects": {
"network": {
"private": {
"title": "Privé",
"description": "Accessible uniquement sur invitation"
},
"public": {
"title": "Public",
"description": "Tout le monde dans l'espace de travail sauf les invités peut rejoindre"
}
},
"error": {
"permission": "Vous n'avez pas la permission d'effectuer cette action.",
"cycle_delete": "Échec de la suppression du cycle",
"module_delete": "Échec de la suppression du module",
"issue_delete": "Échec de la suppression du problème"
},
"state": {
"backlog": "Backlog",
"unstarted": "Non démarré",
"started": "Démarré",
"completed": "Terminé",
"cancelled": "Annulé"
},
"sort": {
"manual": "Manuel",
"name": "Nom",
"created_at": "Date de création",
"members_length": "Nombre de membres"
},
"scope": {
"my_projects": "Mes projets",
"archived_projects": "Archivés"
},
"common": {
"months_count": "{months, plural, one{# mois} other{# mois}}"
},
"empty_state": {
"general": {
"title": "Aucun projet actif",
"description": "Considérez chaque projet comme le parent des travaux orientés vers un objectif. Les projets sont l'endroit où vivent les tâches, les Cycles et les Modules, et avec vos collègues, ils vous aident à atteindre cet objectif. Créez un nouveau projet ou filtrez les projets archivés.",
"primary_button": {
"text": "Commencez votre premier projet",
"comic": {
"title": "Tout commence par un projet sur Plane",
"description": "Un projet peut être une feuille de route produit, une campagne marketing ou le lancement d'une nouvelle voiture."
}
}
},
"no_projects": {
"title": "Aucun projet",
"description": "Pour créer des tâches ou gérer votre travail, vous devez créer un projet ou en faire partie.",
"primary_button": {
"text": "Commencez votre premier projet",
"comic": {
"title": "Tout commence par un projet sur Plane",
"description": "Un projet peut être une feuille de route produit, une campagne marketing ou le lancement d'une nouvelle voiture."
}
}
},
"filter": {
"title": "Aucun projet correspondant",
"description": "Aucun projet détecté avec les critères correspondants. \n Créez un nouveau projet à la place."
}
}
},
"workspace_issues": {
"empty_state": {
"all-issues": {
"title": "Aucune tâche dans le projet",
"description": "Premier projet terminé ! Maintenant, divisez votre travail en morceaux traçables avec des tâches. Allons-y !",
"primary_button": {
"text": "Créer une nouvelle tâche"
}
},
"assigned": {
"title": "Aucune tâche assignée",
"description": "Les tâches qui vous sont assignées peuvent être suivies ici.",
"primary_button": {
"text": "Créer une nouvelle tâche"
}
},
"created": {
"title": "Aucune tâche créée",
"description": "Toutes les tâches que vous avez créées se trouvent ici. Suivez-les directement ici.",
"primary_button": {
"text": "Créer une nouvelle tâche"
}
},
"subscribed": {
"title": "Aucune tâche suivie",
"description": "Abonnez-vous aux tâches qui vous intéressent, suivez-les toutes ici."
},
"custom-view": {
"title": "Aucune tâche trouvée",
"description": "Les tâches correspondant aux filtres sont affichées ici. Suivez-les toutes ici."
}
}
},
"workspace_settings": {
"label": "Paramètres de l'espace de travail",
"settings": {
"general": {
"title": "Général"
},
"members": {
"title": "Membres"
},
"billing-and-plans": {
"title": "Facturation et plans"
},
"exports": {
"title": "Exportations"
},
"webhooks": {
"title": "Webhooks"
},
"api-tokens": {
"title": "Jetons API"
}
},
"empty_state": {
"api_tokens": {
"title": "Aucun jeton API créé",
"description": "Les API Plane peuvent être utilisées pour intégrer vos données dans Plane avec n'importe quel système externe. Créez un jeton pour commencer."
},
"webhooks": {
"title": "Aucun webhook ajouté",
"description": "Créez des webhooks pour recevoir des mises à jour en temps réel et automatiser des actions."
},
"exports": {
"title": "Aucune exportation pour le moment",
"description": "Chaque fois que vous exportez, une copie sera également disponible ici pour référence."
},
"imports": {
"title": "Aucune importation pour le moment",
"description": "Retrouvez toutes vos importations précédentes ici et téléchargez-les."
}
}
},
"profile": {
"label": "Profil",
"page_label": "Votre travail",
"work": "Travail",
"details": {
"joined_on": "Inscrit le",
"time_zone": "Fuseau horaire"
},
"stats": {
"workload": "Charge de travail",
"overview": "Aperçu",
"created": "Problèmes créés",
"assigned": "Problèmes assignés",
"subscribed": "Problèmes suivis",
"state_distribution": {
"title": "Problèmes par état",
"empty": "Créez des problèmes pour les visualiser par état dans le graphique pour une meilleure analyse."
},
"priority_distribution": {
"title": "Problèmes par priorité",
"empty": "Créez des problèmes pour les visualiser par priorité dans le graphique pour une meilleure analyse."
},
"recent_activity": {
"title": "Activité récente",
"empty": "Nous n'avons pas trouvé de données. Veuillez consulter vos entrées"
}
},
"actions": {
"profile": "Profil",
"security": "Sécurité",
"activity": "Activité",
"appearance": "Apparence",
"notifications": "Notifications"
},
"tabs": {
"summary": "Résumé",
"assigned": "Assignés",
"created": "Créés",
"subscribed": "Suivis",
"activity": "Activité"
},
"empty_state": {
"activity": {
"title": "Aucune activité pour le moment",
"description": "Commencez par créer une nouvelle tâche ! Ajoutez des détails et des propriétés à celle-ci. Explorez davantage Plane pour voir votre activité."
},
"assigned": {
"title": "Aucune tâche assignée",
"description": "Les tâches qui vous sont assignées peuvent être suivies ici."
},
"created": {
"title": "Aucune tâche créée",
"description": "Toutes les tâches que vous avez créées se trouvent ici. Suivez-les directement ici."
},
"subscribed": {
"title": "Aucune tâche suivie",
"description": "Abonnez-vous aux tâches qui vous intéressent, suivez-les toutes ici."
}
}
},
"project_settings": {
"empty_state": {
"labels": {
"title": "Aucune étiquette pour le moment",
"description": "Créez des étiquettes pour organiser et filtrer les tâches de votre projet."
}
}
},
"project_cycles": {
"empty_state": {
"general": {
"title": "Groupez et cadrez votre travail en cycles.",
"description": "Décomposez le travail en périodes définies, travaillez à rebours à partir de votre date limite pour fixer des échéances, et réalisez des progrès tangibles en équipe.",
"primary_button": {
"text": "Définir votre premier cycle",
"comic": {
"title": "Les cycles sont des périodes répétitives.",
"description": "Un sprint, une itération ou tout autre terme que vous utilisez pour le suivi hebdomadaire ou bi-hebdomadaire du travail est un cycle."
}
}
},
"no_issues": {
"title": "Aucune tâche ajoutée au cycle",
"description": "Ajoutez ou créez des tâches que vous souhaitez cadencer et livrer dans ce cycle",
"primary_button": {
"text": "Créer une nouvelle tâche"
},
"secondary_button": {
"text": "Ajouter une tâche existante"
}
},
"completed_no_issues": {
"title": "Aucun problème dans le cycle",
"description": "Aucun problème dans le cycle. Les problèmes ont été transférés ou sont cachés. Pour voir les problèmes cachés, le cas échéant, mettez à jour les propriétés d'affichage en conséquence."
},
"active": {
"title": "Aucun cycle actif",
"description": "Un cycle actif inclut toute période englobant la date d'aujourd'hui dans sa plage. Retrouvez ici les progrès et les détails du cycle actif."
},
"archived": {
"title": "Aucun cycle archivé pour le moment",
"description": "Pour organiser votre projet, archivez les cycles terminés. Retrouvez-les ici une fois archivés."
}
}
},
"project_issues": {
"empty_state": {
"no_issues": {
"title": "Créez un problème et assignez-le à quelqu'un, même à vous-même",
"description": "Considérez les problèmes comme des emplois, des tâches, des actions ou JTBD (travaux à accomplir). Nous aimons ça. Un problème et ses sous-problèmes sont généralement des actions basées sur le temps assignées aux membres de votre équipe. Votre équipe crée, assigne et termine des problèmes pour faire avancer votre projet vers son objectif.",
"primary_button": {
"text": "Créez votre premier problème",
"comic": {
"title": "Les problèmes sont les éléments constitutifs de Plane.",
"description": "Redessiner l'interface utilisateur de Plane, Rebrander l'entreprise ou Lancer le nouveau système d'injection de carburant sont des exemples de problèmes qui comportent probablement des sous-problèmes."
}
}
},
"no_archived_issues": {
"title": "Aucun problème archivé pour l'instant",
"description": "Manuellement ou via une automatisation, vous pouvez archiver les problèmes terminés ou annulés. Retrouvez-les ici une fois archivés.",
"primary_button": {
"text": "Configurer l'automatisation"
}
},
"issues_empty_filter": {
"title": "Aucun problème trouvé correspondant aux filtres appliqués",
"secondary_button": {
"text": "Effacer tous les filtres"
}
}
}
},
"project_module": {
"empty_state": {
"no_issues": {
"title": "Aucun problème dans le module",
"description": "Créez ou ajoutez des problèmes que vous souhaitez réaliser dans le cadre de ce module",
"primary_button": {
"text": "Créer un nouveau problème"
},
"secondary_button": {
"text": "Ajouter un problème existant"
}
},
"general": {
"title": "Associez les jalons de votre projet aux modules et suivez facilement le travail global.",
"description": "Un groupe de problèmes appartenant à un parent logique et hiérarchique forme un module. Pensez-y comme un moyen de suivre le travail par jalons de projet. Ils ont leurs propres périodes, échéances et analyses pour vous aider à voir votre progression vers un jalon.",
"primary_button": {
"text": "Construisez votre premier module",
"comic": {
"title": "Les modules aident à regrouper le travail par hiérarchie.",
"description": "Un module de panier, un module de châssis et un module d'entrepôt sont de bons exemples de ce regroupement."
}
}
},
"archived": {
"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."
}
}
},
"project_views": {
"empty_state": {
"general": {
"title": "Enregistrez des vues filtrées pour votre projet. Créez-en autant que nécessaire",
"description": "Les vues sont un ensemble de filtres enregistrés que vous utilisez fréquemment ou auxquels vous souhaitez accéder facilement. Tous vos collègues dans un projet peuvent voir les vues de chacun et choisir celle qui correspond le mieux à leurs besoins.",
"primary_button": {
"text": "Créez votre première vue",
"comic": {
"title": "Les vues fonctionnent au-dessus des propriétés des problèmes.",
"description": "Vous pouvez créer une vue à partir d'ici avec autant de propriétés ou de filtres que vous le souhaitez."
}
}
},
"filter": {
"title": "Aucune vue correspondante",
"description": "Aucune vue ne correspond aux critères de recherche. \n Créez une nouvelle vue à la place."
}
}
},
"project_page": {
"empty_state": {
"general": {
"title": "Écrivez une note, un document ou une base de connaissances complète. Laissez Galileo, l'assistant IA de Plane, vous aider à démarrer",
"description": "Les pages sont un espace pour capturer des idées dans Plane. Prenez des notes de réunion, formatez-les facilement, intégrez des problèmes, organisez-les en utilisant une bibliothèque de composants et gardez-les toutes dans le contexte de votre projet. Pour simplifier tout document, invoquez Galileo, l'IA de Plane, avec un raccourci ou en cliquant sur un bouton.",
"primary_button": {
"text": "Créez votre première page"
}
},
"private": {
"title": "Aucune page privée pour le moment",
"description": "Conservez vos pensées privées ici. Lorsque vous êtes prêt à les partager, votre équipe n'est qu'à un clic.",
"primary_button": {
"text": "Créez votre première page"
}
},
"public": {
"title": "Aucune page publique pour le moment",
"description": "Consultez ici les pages partagées avec tout le monde dans votre projet.",
"primary_button": {
"text": "Créez votre première page"
}
},
"archived": {
"title": "Aucune page archivée pour le moment",
"description": "Archivez les pages qui ne sont pas sur votre radar. Accédez-y ici en cas de besoin."
}
}
},
"command_k": {
"empty_state": {
"search": {
"title": "Aucun résultat trouvé"
}
}
},
"issue_relation": {
"empty_state": {
"search": {
"title": "Aucun problème correspondant trouvé"
},
"general": {
"title": "Aucun problème trouvé"
}
}
},
"issue_comment": {
"empty_state": {
"general": {
"title": "Pas encore de commentaires",
"description": "Les commentaires peuvent être utilisés comme un espace de discussion et de suivi pour les problèmes"
}
}
},
"notification": {
"empty_state": {
"detail": {
"title": "Sélectionnez pour voir les détails."
},
"all": {
"title": "Aucune tâche assignée",
"description": "Les mises à jour des problèmes qui vous sont assignés peuvent être \n vues ici."
},
"mentions": {
"title": "Aucune tâche assignée",
"description": "Les mises à jour des problèmes qui vous sont assignés peuvent être \n vues ici."
}
}
},
"active_cycle": {
"empty_state": {
"progress": {
"title": "Ajoutez des problèmes au cycle pour voir sa progression."
},
"chart": {
"title": "Ajoutez des problèmes au cycle pour voir le graphique d'avancement."
},
"priority_issue": {
"title": "Observez d'un coup d'œil les problèmes prioritaires traités dans le cycle."
},
"assignee": {
"title": "Ajoutez des responsables aux problèmes pour voir la répartition du travail par responsable."
},
"label": {
"title": "Ajoutez des étiquettes aux problèmes pour voir la répartition du travail par étiquette."
}
}
},
"disabled_project": {
"empty_state": {
"inbox": {
"title": "La collecte n'est pas activée pour le projet.",
"description": "La collecte vous aide à gérer les demandes entrantes pour votre projet et à les ajouter en tant que problèmes dans votre flux de travail. Activez la collecte dans les paramètres du projet pour gérer les demandes.",
"primary_button": {
"text": "Gérer les fonctionnalités"
}
},
"cycle": {
"title": "Les cycles ne sont pas activés pour ce projet.",
"description": "Divisez le travail en segments limités dans le temps, travaillez à rebours depuis la date limite de votre projet pour fixer des dates et progressez concrètement en équipe. Activez la fonctionnalité des cycles pour votre projet afin de commencer à les utiliser.",
"primary_button": {
"text": "Gérer les fonctionnalités"
}
},
"module": {
"title": "Les modules ne sont pas activés pour le projet.",
"description": "Les modules sont les éléments constitutifs de votre projet. Activez les modules dans les paramètres du projet pour commencer à les utiliser.",
"primary_button": {
"text": "Gérer les fonctionnalités"
}
},
"page": {
"title": "Les pages ne sont pas activées pour le projet.",
"description": "Les pages sont les éléments constitutifs de votre projet. Activez les pages dans les paramètres du projet pour commencer à les utiliser.",
"primary_button": {
"text": "Gérer les fonctionnalités"
}
},
"view": {
"title": "Les vues ne sont pas activées pour le projet.",
"description": "Les vues sont les éléments constitutifs de votre projet. Activez les vues dans les paramètres du projet pour commencer à les utiliser.",
"primary_button": {
"text": "Gérer les fonctionnalités"
}
}
}
},
"inbox": {
"label": "Boîte de réception",
"empty_state": {
"sidebar_open_tab": {
"title": "Aucun problème ouvert",
"description": "Trouvez ici les problèmes ouverts. Créez un nouveau problème."
},
"sidebar_closed_tab": {
"title": "Aucun problème fermé",
"description": "Tous les problèmes, qu'ils soient acceptés ou refusés, se trouvent ici."
},
"sidebar_filter": {
"title": "Aucun problème correspondant",
"description": "Aucun problème ne correspond au filtre appliqué dans l'entrée. Créez un nouveau problème."
},
"detail": {
"title": "Sélectionnez un problème pour voir ses détails."
}
}
},
"workspace_draft_issues": {
"empty_state": {
"title": "Les problèmes à moitié rédigés, et bientôt, les commentaires apparaîtront ici.",
"description": "Pour essayer cela, commencez à rédiger un problème et laissez-le en suspens ou créez votre premier brouillon ci-dessous. 😉",
"primary_button": {
"text": "Créez votre premier brouillon"
}
}
},
"stickies": {
"title": "Vos pense-bêtes",
"placeholder": "cliquez pour écrire ici",
"all": "Tous les pense-bêtes",
"no-data": "Notez une idée, capturez un éclair de génie ou enregistrez une réflexion. Ajoutez un pense-bête pour commencer.",
"add": "Ajouter un pense-bête",
"search_placeholder": "Rechercher par titre",
"delete": "Supprimer le pense-bête",
"delete_confirmation": "Êtes-vous sûr de vouloir supprimer ce pense-bête ?",
"empty_state": {
"simple": "Notez une idée, capturez un éclair de génie ou enregistrez une réflexion. Ajoutez un pense-bête pour commencer.",
"general": {
"title": "Les pense-bêtes sont des notes rapides et des tâches que vous prenez à la volée.",
"description": "Capturez vos pensées et vos idées sans effort en créant des pense-bêtes accessibles à tout moment et de n'importe où.",
"primary_button": {
"text": "Ajouter un pense-bête"
}
},
"search": {
"title": "Cela ne correspond à aucun de vos pense-bêtes.",
"description": "Essayez un terme différent ou informez-nous\nsi vous êtes sûr que votre recherche est correcte.",
"primary_button": {
"text": "Ajouter un pense-bête"
}
}
},
"toasts": {
"errors": {
"wrong_name": "Le nom du pense-bête ne peut pas dépasser 100 caractères.",
"already_exists": "Il existe déjà un pense-bête sans description"
},
"created": {
"title": "Pense-bête créé",
"message": "Le pense-bête a été créé avec succès"
},
"not_created": {
"title": "Pense-bête non créé",
"message": "Le pense-bête n'a pas pu être créé"
},
"updated": {
"title": "Pense-bête mis à jour",
"message": "Le pense-bête a été mis à jour avec succès"
},
"not_updated": {
"title": "Pense-bête non mis à jour",
"message": "Le pense-bête n'a pas pu être mis à jour"
},
"removed": {
"title": "Pense-bête supprimé",
"message": "Le pense-bête a été supprimé avec succès"
},
"not_removed": {
"title": "Pense-bête non supprimé",
"message": "Le pense-bête n'a pas pu être supprimé"
}
}
},
"role_details": {
"guest": {
"title": "Invité",
"description": "Les membres externes des organisations peuvent être invités en tant qu'invités."
},
"member": {
"title": "Membre",
"description": "Capacité à lire, écrire, modifier et supprimer des entités dans les projets, cycles et modules"
},
"admin": {
"title": "Administrateur",
"description": "Toutes les autorisations définies sur vrai dans l'espace de travail."
}
},
"user_roles": {
"product_or_project_manager": "Chef de Produit / Projet",
"development_or_engineering": "Développement / Ingénierie",
"founder_or_executive": "Fondateur / Dirigeant",
"freelancer_or_consultant": "Freelance / Consultant",
"marketing_or_growth": "Marketing / Croissance",
"sales_or_business_development": "Ventes / Développement Commercial",
"support_or_operations": "Support / Opérations",
"student_or_professor": "Étudiant / Professeur",
"human_or_resources": "Ressources Humaines",
"other": "Autre"
},
"importer": {
"github": {
"title": "Github",
"description": "Importer des problèmes depuis les dépôts GitHub et les synchroniser."
},
"jira": {
"title": "Jira",
"description": "Importer des problèmes et des épopées depuis les projets Jira."
}
},
"exporter": {
"csv": {
"title": "CSV",
"description": "Exporter les problèmes vers un fichier CSV."
},
"xlsx": {
"title": "Excel",
"description": "Exporter les problèmes vers un fichier Excel."
},
"json": {
"title": "JSON",
"description": "Exporter les problèmes vers un fichier JSON."
}
},
"default_global_view": {
"all_issues": "Tous les problèmes",
"assigned": "Assignés",
"created": "Créés",
"subscribed": "Abonnés"
}
"inbox": "Boîte de réception"
}
File diff suppressed because it is too large Load Diff
@@ -154,50 +154,11 @@
"stay_ahead_of_blockers_description": "发现项目间的挑战,查看其他视图中不明显的周期依赖关系。",
"analytics": "分析",
"workspace_invites": "工作区邀请",
"workspace_settings": {
"label": "工作区设置",
"settings": {
"general": {
"title": "常规"
},
"members": {
"title": "会员"
},
"billing-and-plans": {
"title": "账单与计划"
},
"exports": {
"title": "导出"
},
"webhooks": {
"title": "网络钩子"
},
"api-tokens": {
"title": "API 令牌"
}
},
"empty_state": {
"api_tokens": {
"title": "尚未创建 API 令牌",
"description": "Plane API 可用于将您在 Plane 中的数据与任何外部系统集成。创建令牌即可开始使用。"
},
"webhooks": {
"title": "尚未添加网络钩子",
"description": "创建网络钩子以接收实时更新并自动执行操作。"
},
"exports": {
"title": "尚无导出记录",
"description": "每次导出时,您都可以在此处找到参考副本。"
},
"imports": {
"title": "尚无导入记录",
"description": "在此处查找所有以前的导入记录并下载。"
}
}
},
"workspace_settings": "工作区设置",
"enter_god_mode": "进入上帝模式",
"workspace_logo": "工作区徽标",
"new_issue": "新建问题",
"home": "首页",
"your_work": "您的工作",
"drafts": "草稿",
"projects": "项目",
@@ -261,6 +222,7 @@
"join_the_project_to_rearrange": "加入项目以重新排序",
"drag_to_rearrange": "拖动以重新排序",
"congrats": "恭喜!",
"project": "项目",
"open_project": "打开项目",
"issues": "问题",
"cycles": "周期",
@@ -353,504 +315,5 @@
"remove_parent_issue": "移除父问题",
"add_parent": "添加父问题",
"loading_members": "正在加载成员...",
"inbox": "收件箱",
"show_all": "显示全部",
"show_less": "显示较少",
"no_data_yet": "暂无数据",
"project_view": {
"access": {
"public": "公开",
"private": "私密"
},
"sort_by": {
"created_at": "创建时间",
"updated_at": "更新时间",
"name": "名称"
}
},
"toast": {
"success": "成功!",
"error": "错误!"
},
"home": {
"empty": {
"create_project": {
"title": "创建项目",
"description": "在Plane中,大多数事情都从项目开始。",
"cta": "开始使用"
},
"invite_team": {
"title": "邀请团队",
"description": "与同事一起构建、部署和管理。",
"cta": "邀请成员"
},
"configure_workspace": {
"title": "设置工作空间",
"description": "开启或关闭功能,或进行更多设置。",
"cta": "配置此工作空间"
},
"personalize_account": {
"title": "让Plane更个性化",
"description": "选择您的头像、颜色等更多设置。",
"cta": "立即个性化"
}
},
"quick_links": {
"empty": "保存您想随手可得的工作相关链接。",
"add": "添加快捷链接",
"title": "快捷链接",
"title_plural": "快捷链接",
"toasts": {
"created": {
"title": "链接已创建",
"message": "链接已成功创建"
},
"not_created": {
"title": "链接未创建",
"message": "无法创建链接"
},
"updated": {
"title": "链接已更新",
"message": "链接已成功更新"
},
"not_updated": {
"title": "链接未更新",
"message": "无法更新链接"
},
"removed": {
"title": "链接已删除",
"message": "链接已成功删除"
},
"not_removed": {
"title": "链接未删除",
"message": "无法删除链接"
}
}
},
"recents": {
"title": "最近",
"empty": {
"project": "访问项目后,您的最近项目将显示在这里。",
"page": "访问页面后,您的最近页面将显示在这里。",
"issue": "访问问题后,您的最近问题将显示在这里。",
"default": "您还没有任何最近项目。"
},
"filters": {
"all": "所有项目",
"projects": "项目",
"pages": "页面",
"issues": "问题"
}
},
"my_stickies": {
"title": "您的便签"
},
"new_at_plane": {
"title": "Plane 新功能"
},
"quick_tutorial": {
"title": "快速教程"
},
"widget": {
"reordered_successfully": "小部件重新排序成功。",
"reordering_failed": "重新排序小部件时发生错误。"
},
"manage_widgets": "管理小部件",
"title": "首页",
"star_us_on_github": "在 GitHub 上为我们加星"
},
"link": {
"modal": {
"url": {
"text": "网址",
"required": "网址无效",
"placeholder": "输入或粘贴网址"
},
"title": {
"text": "显示标题",
"placeholder": "您希望将此链接显示为什么"
}
}
},
"common": {
"all": "全部",
"states": "状态",
"state": "状态",
"state_groups": "状态组",
"priority": "优先级",
"team_project": "团队项目",
"project": "项目",
"cycle": "周期",
"cycles": "周期",
"module": "模块",
"modules": "模块",
"labels": "标签",
"assignees": "分配人",
"assignee": "分配人",
"created_by": "创建者",
"none": "无",
"link": "链接",
"estimate": "估算",
"layout": "布局",
"filters": "筛选",
"display": "显示",
"load_more": "加载更多",
"no_matches_found": "未找到匹配项",
"activity": "活动",
"analytics": "分析",
"success": "成功",
"error": "错误",
"group_by": "分组方式",
"search": "搜索",
"epic": "史诗",
"issue": "问题",
"warning": "警告",
"updating": "更新中",
"update": "更新",
"creating": "创建中",
"cancel": "取消",
"description": "描述",
"title": "标题",
"order_by": {
"label": "排序方式",
"manual": "手动",
"last_created": "最近创建",
"last_updated": "最近更新",
"start_date": "开始日期",
"due_date": "截止日期",
"asc": "升序",
"desc": "降序"
},
"sort": {
"asc": "升序",
"desc": "降序"
},
"comments": "评论",
"updates": "更新"
},
"form": {
"title": {
"required": "标题为必填项",
"max_length": "标题不能超过{length}个字符"
}
},
"entity": {
"grouping_title": "{entity}分组",
"priority": "{entity}",
"all": "所有{entity}",
"drop_here_to_move": "拖放至此以移动{entity}"
},
"epic": {
"all": "所有史诗",
"label": "{count, plural, one {史诗} other {史诗}}"
},
"issue": {
"label": "{count, plural, one {问题} other {问题}}",
"all": "所有问题",
"add": "添加问题",
"priority": {
"urgent": "紧急",
"high": "高",
"medium": "中",
"low": "低"
},
"display": {
"properties": {
"label": "显示属性",
"id": "ID",
"issue_type": "问题类型",
"sub_issue_count": "子问题数量",
"attachment_count": "附件数量"
},
"extra": {
"show_sub_issues": "显示子问题",
"show_empty_groups": "显示空分组"
}
},
"layouts": {
"ordered_by_label": "当前排序方式",
"list": "列表",
"kanban": "看板",
"calendar": "日历",
"spreadsheet": "表格",
"gantt": "时间线",
"title": {
"list": "列表视图",
"kanban": "看板视图",
"calendar": "日历视图",
"spreadsheet": "表格视图",
"gantt": "时间线视图"
}
},
"states": {
"active": "进行中",
"backlog": "待办"
},
"comments": {
"create": {
"success": "评论创建成功",
"error": "评论创建失败,请稍后重试"
},
"update": {
"success": "评论更新成功",
"error": "评论更新失败,请稍后重试"
},
"remove": {
"success": "评论删除成功",
"error": "评论删除失败,请稍后重试"
},
"upload": {
"error": "资源上传失败,请稍后重试"
}
}
},
"project": {
"label": "{count, plural, one {项目} other {项目}}"
},
"view": {
"create": {
"label": "创建视图"
},
"update": {
"label": "更新视图"
}
},
"inbox_issue": {
"status": {
"pending": {
"title": "待处理",
"description": "待处理"
},
"declined": {
"title": "已拒绝",
"description": "已拒绝"
},
"snoozed": {
"title": "已暂停",
"description": "还剩{days}天"
},
"accepted": {
"title": "已接受",
"description": "已接受"
},
"duplicate": {
"title": "重复",
"description": "重复"
}
},
"source": {
"in-app": "应用内"
},
"order_by": {
"created_at": "创建时间",
"updated_at": "更新时间",
"id": "编号"
}
},
"user_profile": {
"title": "你的工作",
"work": "工作",
"details": {
"joined_on": "加入时间",
"time_zone": "时区"
},
"stats": {
"workload": "工作量",
"overview": "概述",
"created": "已创建问题",
"assigned": "已分配问题",
"subscribed": "已订阅问题",
"state_distribution": {
"title": "按状态显示的问题",
"empty": "创建问题以在图表中按状态查看它们,以便更好地分析。"
},
"priority_distribution": {
"title": "按优先级显示的问题",
"empty": "创建问题以在图表中按优先级查看它们,以便更好地分析。"
},
"recent_activity": {
"title": "最近活动",
"empty": "我们找不到数据。请查看您的输入"
}
},
"actions": {
"profile": "个人资料",
"security": "安全",
"activity": "活动",
"appearance": "外观",
"notifications": "通知"
},
"tabs": {
"summary": "摘要",
"assigned": "已分配",
"created": "已创建",
"subscribed": "已订阅",
"activity": "活动"
}
},
"role_details": {
"guest": {
"title": "访客",
"description": "组织的外部成员可以被邀请为访客。"
},
"member": {
"title": "成员",
"description": "可以在项目、周期和模块内读取、写入、编辑和删除实体"
},
"admin": {
"title": "管理员",
"description": "工作区内的所有权限都设置为允许。"
}
},
"user_roles": {
"product_or_project_manager": "产品/项目经理",
"development_or_engineering": "开发/工程",
"founder_or_executive": "创始人/高管",
"freelancer_or_consultant": "自由职业者/顾问",
"marketing_or_growth": "市场营销/增长",
"sales_or_business_development": "销售/业务发展",
"support_or_operations": "支持/运营",
"student_or_professor": "学生/教授",
"human_or_resources": "人力资源",
"other": "其他"
},
"importer": {
"github": {
"title": "Github",
"description": "从 GitHub 仓库导入问题并同步。"
},
"jira": {
"title": "Jira",
"description": "从 Jira 项目导入问题和史诗。"
}
},
"exporter": {
"csv": {
"title": "CSV",
"description": "将问题导出为 CSV 文件。"
},
"xlsx": {
"title": "Excel",
"description": "将问题导出为 Excel 文件。"
},
"json": {
"title": "JSON",
"description": "将问题导出为 JSON 文件。"
}
},
"default_global_view": {
"all_issues": "所有问题",
"assigned": "已分配",
"created": "已创建",
"subscribed": "已订阅"
},
"stickies": {
"title": "便签",
"placeholder": "点击此处输入",
"all": "所有便签",
"no-data": "记下想法,捕捉灵感,或记录突发奇想。添加便签即可开始。",
"add": "添加便签",
"search_placeholder": "按标题搜索",
"delete": "删除便签",
"delete_confirmation": "确定要删除这个便签吗?",
"empty_state": {
"simple": "记下想法,捕捉灵感,或记录突发奇想。添加便签即可开始。",
"general": {
"title": "便签是您随手记下的快速笔记和待办事项。",
"description": "通过创建便签轻松捕捉您的想法和创意,随时随地都可以访问。",
"primary_button": {
"text": "添加便签"
}
},
"search": {
"title": "没有找到匹配的便签。",
"description": "尝试使用不同的搜索词,或如果您确定搜索正确,\n请告诉我们。",
"primary_button": {
"text": "添加便签"
}
}
},
"toasts": {
"errors": {
"wrong_name": "便签名称不能超过100个字符。",
"already_exists": "已存在一个没有描述的便签"
},
"created": {
"title": "便签已创建",
"message": "便签已成功创建"
},
"not_created": {
"title": "便签未创建",
"message": "无法创建便签"
},
"updated": {
"title": "便签已更新",
"message": "便签已成功更新"
},
"not_updated": {
"title": "便签未更新",
"message": "无法更新便签"
},
"removed": {
"title": "便签已删除",
"message": "便签已成功删除"
},
"not_removed": {
"title": "便签未删除",
"message": "无法删除便签"
}
}
},
"workspace_projects": {
"network": {
"private": {
"title": "私有",
"description": "仅限邀请访问"
},
"public": {
"title": "公开",
"description": "除访客外的工作区所有成员都可加入"
}
},
"error": {
"permission": "您没有执行此操作的权限。",
"cycle_delete": "删除周期失败",
"module_delete": "删除模块失败",
"issue_delete": "删除问题失败"
},
"state": {
"backlog": "待办",
"unstarted": "未开始",
"started": "进行中",
"completed": "已完成",
"cancelled": "已取消"
},
"sort": {
"manual": "手动",
"name": "名称",
"created_at": "创建日期",
"members_length": "成员数量"
},
"scope": {
"my_projects": "我的项目",
"archived_projects": "已归档"
},
"common": {
"months_count": "{months, plural, one{#个月} other{#个月}}"
}
}
"inbox": "收件箱"
}
+32 -1
View File
@@ -2,6 +2,23 @@ import { TPaginationInfo } from "./common";
import { TIssuePriorities } from "./issues";
import { TIssue } from "./issues/base";
enum EInboxIssueCurrentTab {
OPEN = "open",
CLOSED = "closed",
}
enum EInboxIssueStatus {
PENDING = -2,
DECLINED = -1,
SNOOZED = 0,
ACCEPTED = 1,
DUPLICATE = 2,
}
export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
export type TInboxIssueStatus = EInboxIssueStatus;
// filters
export type TInboxIssueFilterMemberKeys = "assignees" | "created_by";
@@ -21,7 +38,10 @@ export type TInboxIssueFilter = {
// sorting filters
export type TInboxIssueSortingKeys = "order_by" | "sort_by";
export type TInboxIssueSortingOrderByKeys = "issue__created_at" | "issue__updated_at" | "issue__sequence_id";
export type TInboxIssueSortingOrderByKeys =
| "issue__created_at"
| "issue__updated_at"
| "issue__sequence_id";
export type TInboxIssueSortingSortByKeys = "asc" | "desc";
@@ -58,6 +78,17 @@ export type TInboxDuplicateIssueDetails = {
name: string;
};
export type TInboxIssue = {
id: string;
status: TInboxIssueStatus;
snoozed_till: Date | null;
duplicate_to: string | undefined;
source: string;
issue: TIssue;
created_by: string;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
};
export type TInboxIssuePaginationInfo = TPaginationInfo & {
total_results: number;
};
+1 -1
View File
@@ -15,7 +15,7 @@ import type {
TIssueGroupByOptions,
TIssueOrderByOptions,
TIssueGroupingFilters,
TIssueExtraOptions,
TIssueExtraOptions
} from "@plane/types";
export interface IIssueCycle {
+1 -3
View File
@@ -32,9 +32,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<link rel="shortcut icon" href={`${SPACE_BASE_PATH}/favicon/favicon.ico`} />
</head>
<body>
<AppProvider>
<>{children}</>
</AppProvider>
<AppProvider>{children}</AppProvider>
</body>
</html>
);
@@ -2,9 +2,8 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { ISSUE_PRIORITY_FILTERS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { ISSUE_PRIORITY_FILTERS } from "@plane/constants";
import { PriorityIcon } from "@plane/ui";
// components
import { FilterHeader, FilterOption } from "./helpers";
@@ -19,9 +18,6 @@ type Props = {
export const FilterPriority: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, searchQuery } = props;
// hooks
const { t } = useTranslation();
const [previewEnabled, setPreviewEnabled] = useState(true);
const appliedFiltersCount = appliedFilters?.length ?? 0;
@@ -44,11 +40,11 @@ export const FilterPriority: React.FC<Props> = observer((props) => {
isChecked={appliedFilters?.includes(priority.key) ? true : false}
onClick={() => handleUpdate(priority.key)}
icon={<PriorityIcon priority={priority.key} className="h-3.5 w-3.5" />}
title={t(priority.titleTranslationKey)}
title={priority.title}
/>
))
) : (
<p className="text-xs italic text-custom-text-400">{t("common.no_matches_found")}</p>
<p className="text-xs italic text-custom-text-400">No matches found</p>
)}
</div>
)}
@@ -94,7 +94,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
<HeaderGroupByCard
groupBy={groupBy}
icon={subList.icon as any}
icon={subList.icon}
title={subList.name}
count={getGroupIssueCount(subList.id, undefined, false) ?? 0}
/>
@@ -131,14 +131,10 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup);
if (subGroupByVisibilityToggle === false) return <></>;
return (
<div key={`${subGroupBy}_${group.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
<HeaderGroupByCard
groupBy={groupBy}
icon={group.icon}
title={group.name}
count={groupCount}
/>
<HeaderGroupByCard groupBy={groupBy} icon={group.icon} title={group.name} count={groupCount} />
</div>
);
})}
@@ -266,7 +262,7 @@ const SubGroup: React.FC<ISubGroup> = observer((props) => {
<div className="sticky top-[50px] z-[3] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
<div className="sticky left-0 flex-shrink-0">
<HeaderSubGroupByCard
icon={group.icon as any}
icon={group.icon}
title={group.name || ""}
count={issueCount}
isExpanded={isExpanded}
@@ -2,7 +2,6 @@
import { Fragment, MutableRefObject, forwardRef, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
// plane types
import { IGroupByColumn, TIssueGroupByOptions, IIssueDisplayProperties, TPaginationData, TLoader } from "@plane/types";
// plane utils
@@ -63,8 +62,6 @@ export const ListGroup = observer((props: Props) => {
} = props;
const [isExpanded, setIsExpanded] = useState(true);
const groupRef = useRef<HTMLDivElement | null>(null);
// hooks
const { t } = useTranslation();
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
@@ -88,7 +85,7 @@ export const ListGroup = observer((props: Props) => {
}
onClick={() => loadMoreIssues(group.id)}
>
{t("common.load_more")} &darr;
Load More &darr;
</div>
);
@@ -1,6 +1,5 @@
"use client";
import { useTranslation } from "@plane/i18n";
// types
import { TIssuePriorities } from "@plane/types";
import { Tooltip } from "@plane/ui";
@@ -14,19 +13,17 @@ export const IssueBlockPriority = ({
priority: TIssuePriorities | null;
shouldShowName?: boolean;
}) => {
// hooks
const { t } = useTranslation();
const priority_detail = priority != null ? getIssuePriorityFilters(priority) : null;
if (priority_detail === null) return <></>;
return (
<Tooltip tooltipHeading="Priority" tooltipContent={t(priority_detail?.titleTranslationKey || "")}>
<Tooltip tooltipHeading="Priority" tooltipContent={priority_detail?.title}>
<div className="flex items-center relative w-full h-full">
<div className={`grid h-5 w-5 place-items-center rounded border-[0.5px] gap-2 ${priority_detail?.className}`}>
<span className="material-symbols-rounded text-sm">{priority_detail?.icon}</span>
</div>
{shouldShowName && <span className="pl-2 text-sm">{t(priority_detail?.titleTranslationKey || "")}</span>}
{shouldShowName && <span className="pl-2 text-sm">{priority_detail?.title}</span>}
</div>
</Tooltip>
);
@@ -5,8 +5,6 @@ import { observer } from "mobx-react";
import { useRouter, useSearchParams } from "next/navigation";
// ui
import { SITES_ISSUE_LAYOUTS } from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
import { Tooltip } from "@plane/ui";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
@@ -21,8 +19,6 @@ type Props = {
export const IssuesLayoutSelection: FC<Props> = observer((props) => {
const { anchor } = props;
// hooks
const { t } = useTranslation();
// router
const router = useRouter();
const searchParams = useSearchParams();
@@ -49,7 +45,7 @@ export const IssuesLayoutSelection: FC<Props> = observer((props) => {
if (!layoutOptions[layout.key]) return;
return (
<Tooltip key={layout.key} tooltipContent={t(layout.titleTranslationKey)}>
<Tooltip key={layout.key} tooltipContent={layout.title}>
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
@@ -3,7 +3,6 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { CalendarCheck2, Signal } from "lucide-react";
import { useTranslation } from "@plane/i18n";
// ui
import { DoubleCircleIcon, StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
import { getIssuePriorityFilters } from "@plane/utils";
@@ -25,8 +24,6 @@ type Props = {
};
export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDetails, mode }) => {
// hooks
const { t } = useTranslation();
const { getStateById } = useStates();
const state = getStateById(issueDetails?.state_id ?? undefined);
@@ -98,7 +95,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDet
<Icon iconName={priority?.icon} />
</span>
)}
<span>{t(priority?.titleTranslationKey || "common.none")}</span>
<span>{priority?.title ?? "None"}</span>
</div>
</div>
</div>
-1
View File
@@ -20,7 +20,6 @@
"@mui/material": "^5.14.1",
"@plane/constants": "*",
"@plane/editor": "*",
"@plane/i18n": "*",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/services": "*",
@@ -5,41 +5,28 @@ import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation";
import { Tab } from "@headlessui/react";
// plane package imports
import { ANALYTICS_TABS, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ANALYTICS_TABS } from "@plane/constants";
import { Header, EHeaderVariant } from "@plane/ui";
// components
import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics";
import { PageHead } from "@/components/core";
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// hooks
import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// plane web
import { EUserPermissions } from "@/plane-web/constants";
import { useCommandPalette, useEventTracker, useProject, useWorkspace } from "@/hooks/store";
const AnalyticsPage = observer(() => {
const searchParams = useSearchParams();
const analytics_tab = searchParams.get("analytics_tab");
// plane imports
const { t } = useTranslation();
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { workspaceProjectIds, loader } = useProject();
const { currentWorkspace } = useWorkspace();
const { allowPermissions } = useUserPermissions();
// helper hooks
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" });
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined;
// permissions
const canPerformEmptyStateActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
// TODO: refactor loader implementation
return (
<>
@@ -80,22 +67,12 @@ const AnalyticsPage = observer(() => {
</Tab.Group>
</div>
) : (
<DetailedEmptyState
title={t("workspace_analytics.empty_state.general.title")}
description={t("workspace_analytics.empty_state.general.description")}
assetPath={resolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("workspace_analytics.empty_state.general.primary_button.text")}
title={t("workspace_analytics.empty_state.general.primary_button.comic.title")}
description={t("workspace_analytics.empty_state.general.primary_button.comic.description")}
onClick={() => {
setTrackElement("Analytics empty state");
toggleCreateProjectModal(true);
}}
disabled={!canPerformEmptyStateActions}
/>
}
<EmptyState
type={EmptyStateType.WORKSPACE_ANALYTICS}
primaryButtonOnClick={() => {
setTrackElement("Analytics empty state");
toggleCreateProjectModal(true);
}}
/>
)}
</>
@@ -7,7 +7,6 @@ import { Home } from "lucide-react";
import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png";
// ui
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
@@ -20,7 +19,6 @@ export const WorkspaceDashboardHeader = () => {
// hooks
const { captureEvent } = useEventTracker();
const { resolvedTheme } = useTheme();
const { t } = useTranslation();
return (
<>
@@ -30,9 +28,7 @@ export const WorkspaceDashboardHeader = () => {
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink label={t("home.title")} icon={<Home className="h-4 w-4 text-custom-text-300" />} />
}
link={<BreadcrumbLink label="Home" icon={<Home className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
</div>
@@ -55,7 +51,7 @@ export const WorkspaceDashboardHeader = () => {
width={16}
alt="GitHub Logo"
/>
<span className="hidden text-xs font-medium sm:hidden md:block">{t("home.star_us_on_github")}</span>
<span className="hidden text-xs font-medium sm:hidden md:block">Star us on GitHub</span>
</a>
</Header.RightItem>
</Header>
@@ -4,24 +4,21 @@ import { useCallback, useEffect } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// plane imports
import { useTranslation } from "@plane/i18n";
// components
import { LogoSpinner } from "@/components/common";
import { PageHead } from "@/components/core";
import { SimpleEmptyState } from "@/components/empty-state";
import { EmptyState } from "@/components/empty-state";
import { InboxContentRoot } from "@/components/inbox";
import { IssuePeekOverview } from "@/components/issues";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
// hooks
import { useIssueDetail, useUserPermissions, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties";
const WorkspaceDashboardPage = observer(() => {
const { workspaceSlug } = useParams();
// plane hooks
const { t } = useTranslation();
// hooks
const { currentWorkspace } = useWorkspace();
const {
@@ -37,7 +34,6 @@ const WorkspaceDashboardPage = observer(() => {
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Inbox` : undefined;
const { workspace_slug, project_id, issue_id, is_inbox_issue } =
notificationLiteByNotificationId(currentSelectedNotificationId);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/intake/issue-detail" });
// fetching workspace issue properties
useWorkspaceIssueProperties(workspaceSlug);
@@ -86,7 +82,7 @@ const WorkspaceDashboardPage = observer(() => {
<div className="w-full h-full overflow-hidden overflow-y-auto">
{!currentSelectedNotificationId ? (
<div className="w-full h-screen flex justify-center items-center">
<SimpleEmptyState title={t("notification.empty_state.detail.title")} assetPath={resolvedPath} />
<EmptyState type={EmptyStateType.NOTIFICATION_DETAIL_EMPTY_STATE} layout="screen-simple" />
</div>
) : (
<>
+1 -3
View File
@@ -2,7 +2,6 @@
import { observer } from "mobx-react";
// components
import { useTranslation } from "@plane/i18n";
import { PageHead, AppHeader, ContentWrapper } from "@/components/core";
import { WorkspaceHomeView } from "@/components/home";
// hooks
@@ -12,9 +11,8 @@ import { WorkspaceDashboardHeader } from "./header";
const WorkspaceDashboardPage = observer(() => {
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - ${t("home.title")}` : undefined;
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Home` : undefined;
return (
<>
@@ -6,13 +6,12 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { ChevronDown, PanelRight } from "lucide-react";
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IUserProfileProjectSegregation } from "@plane/types";
import { Breadcrumbs, Header, CustomMenu, UserActivityIcon } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common";
// components
import { ProfileIssuesFilter } from "@/components/profile";
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
import { cn } from "@/helpers/common.helper";
import { useAppTheme, useUser, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
@@ -31,7 +30,6 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme();
const { data: currentUser } = useUser();
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { t } = useTranslation();
// derived values
const isAuthorized = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
@@ -46,7 +44,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
const isCurrentUser = currentUser?.id === userId;
const breadcrumbLabel = isCurrentUser ? t("profile.page_label") : `${userName} ${t("profile.work")}`;
const breadcrumbLabel = `${isCurrentUser ? "Your" : userName} Work`;
return (
<Header>
@@ -88,7 +86,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}
className="w-full text-custom-text-300"
>
{t(tab.i18n_label)}
{tab.label}
</Link>
</CustomMenu.MenuItem>
))}
@@ -4,12 +4,12 @@ import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import useSWR from "swr";
// components
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { AppHeader, ContentWrapper } from "@/components/core";
import { ProfileSidebar } from "@/components/profile";
// constants
import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys";
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
// hooks
import { useUserPermissions } from "@/hooks/store";
import useSize from "@/hooks/use-window-size";
@@ -66,7 +66,7 @@ const UseProfileLayout: React.FC<Props> = observer((props) => {
<AppHeader
header={
<UserProfileHeader
type={currentTab?.i18n_label}
type={currentTab?.label}
userProjectsData={userProjectsData}
showProfileIssuesFilter={isIssuesTab}
/>
@@ -6,30 +6,21 @@ import { useParams } from "next/navigation";
// icons
import { ChevronDown } from "lucide-react";
// plane constants
import {
EIssueLayoutTypes,
EIssueFilterType,
EIssuesStoreType,
ISSUE_LAYOUTS,
ISSUE_DISPLAY_FILTERS_BY_PAGE,
} from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
// ui
import { CustomMenu } from "@plane/ui";
// components
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import { useIssues, useLabel } from "@/hooks/store";
export const ProfileIssuesMobileHeader = observer(() => {
// plane i18n
const { t } = useTranslation();
// router
const { workspaceSlug, userId } = useParams();
// store hook
@@ -121,7 +112,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
placement="bottom-start"
customButton={
<div className="flex flex-center text-sm text-custom-text-200">
{t("common.layout")}
Layout
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
</div>
}
@@ -138,19 +129,19 @@ export const ProfileIssuesMobileHeader = observer(() => {
}}
className="flex items-center gap-2"
>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-custom-text-300">{t(layout.i18n_title)}</div>
<layout.icon className="h-3 w-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
title={t("common.filters")}
title="Filters"
placement="bottom-end"
menuButton={
<div className="flex flex-center text-sm text-custom-text-200">
{t("common.filters")}
Filters
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
</div>
}
@@ -158,7 +149,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
>
<FilterSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined
}
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
@@ -172,18 +163,18 @@ export const ProfileIssuesMobileHeader = observer(() => {
</div>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
title={t("common.display")}
title="Display"
placement="bottom-end"
menuButton={
<div className="flex flex-center text-sm text-custom-text-200">
{t("common.display")}
Display
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
</div>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -2,12 +2,12 @@ import React from "react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
// constants
import { Header, EHeaderVariant } from "@plane/ui";
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
type Props = {
isAuthorized: boolean;
@@ -33,7 +33,7 @@ export const ProfileNavbar: React.FC<Props> = (props) => {
: "border-transparent"
}`}
>
{t(tab.i18n_label)}
{t(tab.label)}
</span>
</Link>
))}
@@ -3,8 +3,6 @@
import { useParams } from "next/navigation";
import useSWR from "swr";
// types
import { GROUP_CHOICES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IUserStateDistribution, TStateGroups } from "@plane/types";
// components
import { ContentWrapper } from "@plane/ui";
@@ -18,6 +16,7 @@ import {
} from "@/components/profile";
// constants
import { USER_PROFILE_DATA } from "@/constants/fetch-keys";
import { GROUP_CHOICES } from "@/constants/project";
// services
import { UserService } from "@/services/user.service";
@@ -27,7 +26,6 @@ const userService = new UserService();
export default function ProfileOverviewPage() {
const { workspaceSlug, userId } = useParams();
const { t } = useTranslation();
const { data: userProfile } = useSWR(
workspaceSlug && userId ? USER_PROFILE_DATA(workspaceSlug.toString(), userId.toString()) : null,
workspaceSlug && userId ? () => userService.getUserProfileData(workspaceSlug.toString(), userId.toString()) : null
@@ -42,7 +40,7 @@ export default function ProfileOverviewPage() {
return (
<>
<PageHead title={t("profile.page_label")} />
<PageHead title="Your work" />
<ContentWrapper className="space-y-7">
<ProfileStats userProfile={userProfile} />
<ProfileWorkload stateDistribution={stateDistribution} />
@@ -7,9 +7,7 @@ import { useParams } from "next/navigation";
// icons
import { ArrowRight, PanelRight } from "lucide-react";
// plane constants
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
@@ -18,6 +16,8 @@ import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, Header } from "
import { ProjectAnalyticsModal } from "@/components/analytics";
import { BreadcrumbLink } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers
import { cn } from "@/helpers/common.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
@@ -72,8 +72,6 @@ export const CycleIssuesHeader: React.FC = observer(() => {
projectId: string;
cycleId: string;
};
// i18n
const { t } = useTranslation();
// store hooks
const {
issuesFilter: { issueFilters, updateFilters },
@@ -186,7 +184,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
type="text"
link={
<BreadcrumbLink
label={t("common.cycles")}
label="Cycles"
href={`/${workspaceSlug}/projects/${projectId}/cycles`}
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />}
/>
@@ -241,7 +239,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
selectedLayout={activeLayout}
/>
<FiltersDropdown
title={t("common.filters")}
title="Filters"
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
@@ -249,7 +247,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -260,10 +258,10 @@ export const CycleIssuesHeader: React.FC = observer(() => {
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title={t("common.display")} placement="bottom-end">
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -278,7 +276,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
{canUserCreateIssue && (
<>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
{t("common.analytics")}
Analytics
</Button>
{!isCompletedCycle && (
<Button
@@ -289,7 +287,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
}}
size="sm"
>
{t("issue.add")}
Add issue
</Button>
)}
</>
@@ -5,42 +5,32 @@ import { useParams } from "next/navigation";
// icons
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
// plane constants
import {
EIssueLayoutTypes,
EIssueFilterType,
EIssuesStoreType,
ISSUE_LAYOUTS,
ISSUE_DISPLAY_FILTERS_BY_PAGE,
} from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
import { CustomMenu } from "@plane/ui";
// components
import { ProjectAnalyticsModal } from "@/components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store";
export const CycleIssuesMobileHeader = () => {
// i18n
const { t } = useTranslation();
const [analyticsModal, setAnalyticsModal] = useState(false);
const { getCycleById } = useCycle();
const 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", title: "List", icon: List },
{ key: "kanban", title: "Board", icon: Kanban },
{ key: "calendar", title: "Calendar", icon: Calendar },
];
const { workspaceSlug, projectId, cycleId } = useParams();
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
// store hooks
const { currentProjectDetails } = useProject();
const {
@@ -133,9 +123,7 @@ export const CycleIssuesMobileHeader = () => {
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm"
placement="bottom-start"
customButton={
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{t("common.layout")}</span>
}
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
@@ -147,18 +135,18 @@ export const CycleIssuesMobileHeader = () => {
}}
className="flex items-center gap-2"
>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="w-3 h-3" />
<div className="text-custom-text-300">{t(layout.titleTranslationKey)}</div>
<layout.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title={t("common.filters")}
title="Filters"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
{t("common.filters")}
Filters
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
@@ -168,7 +156,7 @@ export const CycleIssuesMobileHeader = () => {
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -182,18 +170,18 @@ export const CycleIssuesMobileHeader = () => {
</div>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title={t("common.display")}
title="Display"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
{t("common.display")}
Display
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -210,7 +198,7 @@ export const CycleIssuesMobileHeader = () => {
onClick={() => setAnalyticsModal(true)}
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
>
{t("common.analytics")}
Analytics
</span>
</div>
</>
@@ -3,22 +3,20 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import { TCycleFilters } from "@plane/types";
// components
import { Header, EHeaderVariant } from "@plane/ui";
import { PageHead } from "@/components/core";
import { CyclesView, CycleCreateUpdateModal, CycleAppliedFiltersList } from "@/components/cycles";
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
import { EmptyState } from "@/components/empty-state";
import { CycleModuleListLayout } from "@/components/ui";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useEventTracker, useCycle, useProject, useCycleFilter, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { useEventTracker, useCycle, useProject, useCycleFilter } from "@/hooks/store";
const ProjectCyclesPage = observer(() => {
// states
@@ -28,23 +26,13 @@ const ProjectCyclesPage = observer(() => {
const { currentProjectCycleIds, loader } = useCycle();
const { getProjectById, currentProjectDetails } = useProject();
// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams();
// plane hooks
const { t } = useTranslation();
// cycle filters hook
const { clearAllFilters, currentProjectFilters, updateFilters } = useCycleFilter();
const { allowPermissions } = useUserPermissions();
// derived values
const totalCycles = currentProjectCycleIds?.length ?? 0;
const project = projectId ? getProjectById(projectId?.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined;
const hasAdminLevelPermission = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
const hasMemberLevelPermission = allowPermissions(
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
EUserPermissionsLevel.PROJECT
);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/cycles" });
const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => {
if (!projectId) return;
@@ -62,17 +50,9 @@ const ProjectCyclesPage = observer(() => {
if (currentProjectDetails?.cycle_view === false)
return (
<div className="flex items-center justify-center h-full w-full">
<DetailedEmptyState
title={t("disabled_project.empty_state.cycle.title")}
description={t("disabled_project.empty_state.cycle.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("disabled_project.empty_state.cycle.primary_button.text"),
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
},
disabled: !hasAdminLevelPermission,
}}
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_CYCLE}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
/>
</div>
);
@@ -91,22 +71,12 @@ const ProjectCyclesPage = observer(() => {
/>
{totalCycles === 0 ? (
<div className="h-full place-items-center">
<DetailedEmptyState
title={t("project_cycles.empty_state.general.title")}
description={t("project_cycles.empty_state.general.description")}
assetPath={resolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("project_cycles.empty_state.general.primary_button.text")}
title={t("project_cycles.empty_state.general.primary_button.comic.title")}
description={t("project_cycles.empty_state.general.primary_button.comic.description")}
onClick={() => {
setTrackElement("Cycle empty state");
setCreateModal(true);
}}
disabled={!hasMemberLevelPermission}
/>
}
<EmptyState
type={EmptyStateType.PROJECT_CYCLES}
primaryButtonOnClick={() => {
setTrackElement("Cycle empty state");
setCreateModal(true);
}}
/>
</div>
) : (
@@ -4,9 +4,7 @@ import { FC, useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane constants
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
@@ -14,6 +12,8 @@ import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
@@ -23,8 +23,6 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
export const ProjectDraftIssueHeader: FC = observer(() => {
// i18n
const { t } = useTranslation();
// router
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks
@@ -124,18 +122,14 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown
title={t("common.filters")}
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
@@ -144,10 +138,10 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title={t("common.display")} placement="bottom-end">
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -2,50 +2,35 @@
import { observer } from "mobx-react";
// components
import { useParams, useSearchParams } from "next/navigation";
import { EUserProjectRoles } from "@plane/constants/src/user";
import { useTranslation } from "@plane/i18n";
import { PageHead } from "@/components/core";
import { DetailedEmptyState } from "@/components/empty-state";
import { EmptyState } from "@/components/empty-state";
import { InboxIssueRoot } from "@/components/inbox";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// helpers
import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper";
// hooks
import { useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { useProject } from "@/hooks/store";
const ProjectInboxPage = observer(() => {
/// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams();
const searchParams = useSearchParams();
const navigationTab = searchParams.get("currentTab");
const inboxIssueId = searchParams.get("inboxIssueId");
// plane hooks
const { t } = useTranslation();
// hooks
const { currentProjectDetails } = useProject();
const { allowPermissions } = useUserPermissions();
// derived values
const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/intake" });
// No access to inbox
if (currentProjectDetails?.inbox_view === false)
return (
<div className="flex items-center justify-center h-full w-full">
<DetailedEmptyState
title={t("disabled_project.empty_state.inbox.title")}
description={t("disabled_project.empty_state.inbox.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("disabled_project.empty_state.inbox.primary_button.text"),
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
},
disabled: !canPerformEmptyStateActions,
}}
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_INBOX}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
/>
</div>
);
@@ -6,39 +6,26 @@ import { useParams } from "next/navigation";
// icons
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
// plane constants
import {
EIssueLayoutTypes,
EIssueFilterType,
EIssuesStoreType,
ISSUE_LAYOUTS,
ISSUE_DISPLAY_FILTERS_BY_PAGE,
} from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
import { CustomMenu } from "@plane/ui";
// components
import { ProjectAnalyticsModal } from "@/components/analytics";
import {
DisplayFiltersSelection,
FilterSelection,
FiltersDropdown,
IssueLayoutIcon,
} from "@/components/issues/issue-layouts";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
export const ProjectIssuesMobileHeader = observer(() => {
// i18n
const { t } = useTranslation();
const 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", title: "List", icon: List },
{ key: "kanban", title: "Board", icon: Kanban },
{ key: "calendar", title: "Calendar", icon: Calendar },
];
const [analyticsModal, setAnalyticsModal] = useState(false);
const { workspaceSlug, projectId } = useParams() as {
@@ -117,7 +104,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
placement="bottom-start"
customButton={
<div className="flex flex-start text-sm text-custom-text-200">
{t("common.layout")}
Layout
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
</div>
}
@@ -132,18 +119,18 @@ export const ProjectIssuesMobileHeader = observer(() => {
}}
className="flex items-center gap-2"
>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-custom-text-300">{t(layout.titleTranslationKey)}</div>
<layout.icon className="h-3 w-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
title={t("common.filters")}
title="Filters"
placement="bottom-end"
menuButton={
<span className="flex items-center text-sm text-custom-text-200">
{t("common.filters")}
Filters
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
</span>
}
@@ -155,7 +142,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
@@ -167,18 +154,18 @@ export const ProjectIssuesMobileHeader = observer(() => {
</div>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
title={t("common.display")}
title="Display"
placement="bottom-end"
menuButton={
<span className="flex items-center text-sm text-custom-text-200">
{t("common.display")}
Display
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
</span>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -194,7 +181,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
onClick={() => setAnalyticsModal(true)}
className="flex flex-grow justify-center border-l border-custom-border-200 text-sm text-custom-text-200"
>
{t("common.analytics")}
Analytics
</button>
</div>
</>
@@ -7,7 +7,7 @@ import { useParams } from "next/navigation";
// icons
import { ArrowRight, PanelRight } from "lucide-react";
// plane constants
import { EIssueLayoutTypes, EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
import { EIssueLayoutTypes, EIssuesStoreType, EIssueFilterType } from "@plane/constants";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
@@ -16,6 +16,8 @@ import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip, Header } from "@pla
import { ProjectAnalyticsModal } from "@/components/analytics";
import { BreadcrumbLink } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers
import { cn } from "@/helpers/common.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
@@ -245,7 +247,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
@@ -257,7 +259,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -6,27 +6,16 @@ import { useParams } from "next/navigation";
// icons
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
// plane constants
import {
EIssueLayoutTypes,
EIssueFilterType,
EIssuesStoreType,
ISSUE_LAYOUTS,
ISSUE_DISPLAY_FILTERS_BY_PAGE,
} from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
import { CustomMenu } from "@plane/ui";
// components
import { ProjectAnalyticsModal } from "@/components/analytics";
import {
DisplayFiltersSelection,
FilterSelection,
FiltersDropdown,
IssueLayoutIcon,
} from "@/components/issues/issue-layouts";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
@@ -36,11 +25,10 @@ export const ModuleIssuesMobileHeader = observer(() => {
const [analyticsModal, setAnalyticsModal] = useState(false);
const { currentProjectDetails } = useProject();
const { getModuleById } = useModule();
const { t } = useTranslation();
const 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", title: "List", icon: List },
{ key: "kanban", title: "Board", icon: Kanban },
{ key: "calendar", title: "Calendar", icon: Calendar },
];
const { workspaceSlug, projectId, moduleId } = useParams() as {
workspaceSlug: string;
@@ -128,8 +116,8 @@ export const ModuleIssuesMobileHeader = observer(() => {
}}
className="flex items-center gap-2"
>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-custom-text-300">{t(layout.i18n_title)}</div>
<layout.icon className="h-3 w-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
@@ -151,7 +139,7 @@ export const ModuleIssuesMobileHeader = observer(() => {
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
@@ -174,7 +162,7 @@ export const ModuleIssuesMobileHeader = observer(() => {
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -4,36 +4,27 @@ import { useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// types
import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TModuleFilters } from "@plane/types";
// components
import { PageHead } from "@/components/core";
import { DetailedEmptyState } from "@/components/empty-state";
import { EmptyState } from "@/components/empty-state";
import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useModuleFilter, useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { useModuleFilter, useProject } from "@/hooks/store";
const ProjectModulesPage = observer(() => {
// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams();
// plane hooks
const { t } = useTranslation();
// store
const { getProjectById, currentProjectDetails } = useProject();
const { currentProjectFilters, currentProjectDisplayFilters, clearAllFilters, updateFilters, updateDisplayFilters } =
useModuleFilter();
const { allowPermissions } = useUserPermissions();
// derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Modules` : undefined;
const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/modules" });
const handleRemoveFilter = useCallback(
(key: keyof TModuleFilters, value: string | null) => {
@@ -54,17 +45,9 @@ const ProjectModulesPage = observer(() => {
if (currentProjectDetails?.module_view === false)
return (
<div className="flex items-center justify-center h-full w-full">
<DetailedEmptyState
title={t("disabled_project.empty_state.module.title")}
description={t("disabled_project.empty_state.module.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("disabled_project.empty_state.module.primary_button.text"),
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
},
disabled: !canPerformEmptyStateActions,
}}
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_MODULE}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
/>
</div>
);
@@ -2,35 +2,27 @@
import { observer } from "mobx-react";
import { useParams, useSearchParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import { TPageNavigationTabs } from "@plane/types";
// components
import { PageHead } from "@/components/core";
import { DetailedEmptyState } from "@/components/empty-state";
import { EmptyState } from "@/components/empty-state";
import { PagesListRoot, PagesListView } from "@/components/pages";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// hooks
import { useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { useProject } from "@/hooks/store";
const ProjectPagesPage = observer(() => {
// router
const router = useAppRouter();
const searchParams = useSearchParams();
const type = searchParams.get("type");
const { workspaceSlug, projectId } = useParams();
// plane hooks
const { t } = useTranslation();
// store hooks
const { getProjectById, currentProjectDetails } = useProject();
const { allowPermissions } = useUserPermissions();
// derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Pages` : undefined;
const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/pages" });
const currentPageType = (): TPageNavigationTabs => {
const pageType = type?.toString();
@@ -45,17 +37,9 @@ const ProjectPagesPage = observer(() => {
if (currentProjectDetails?.page_view === false)
return (
<div className="flex items-center justify-center h-full w-full">
<DetailedEmptyState
title={t("disabled_project.empty_state.page.title")}
description={t("disabled_project.empty_state.page.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("disabled_project.empty_state.page.primary_button.text"),
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
},
disabled: !canPerformEmptyStateActions,
}}
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_PAGE}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
/>
</div>
);
@@ -43,6 +43,8 @@ const GeneralSettingsPage = observer(() => {
);
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined;
// const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
// const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
return (
<>
@@ -6,13 +6,7 @@ import Link from "next/link";
import { useParams } from "next/navigation";
import { Layers, Lock } from "lucide-react";
// plane constants
import {
EIssueLayoutTypes,
EIssueFilterType,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_PAGE,
EViewAccess,
} from "@plane/constants";
import { EIssueLayoutTypes, EIssueFilterType, EIssuesStoreType } from "@plane/constants";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
@@ -22,6 +16,8 @@ import { BreadcrumbLink, Logo } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import { ViewQuickActions } from "@/components/views";
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
import { EViewAccess } from "@/constants/views";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
import { truncateText } from "@/helpers/string.helper";
@@ -246,7 +242,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
projectId={projectId.toString()}
labels={projectLabels}
@@ -259,7 +255,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
@@ -4,37 +4,29 @@ import { useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import { EUserPermissionsLevel, EUserProjectRoles, EViewAccess } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TViewFilterProps } from "@plane/types";
import { Header, EHeaderVariant } from "@plane/ui";
import { PageHead } from "@/components/core";
import { DetailedEmptyState } from "@/components/empty-state";
import { EmptyState } from "@/components/empty-state";
import { ProjectViewsList } from "@/components/views";
import { ViewAppliedFiltersList } from "@/components/views/applied-filters";
import { EmptyStateType } from "@/constants/empty-state";
// constants
import { EViewAccess } from "@/constants/views";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useProject, useProjectView, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { useProject, useProjectView } from "@/hooks/store";
const ProjectViewsPage = observer(() => {
// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams();
// plane hooks
const { t } = useTranslation();
// store
const { getProjectById, currentProjectDetails } = useProject();
const { filters, updateFilters, clearAllFilters } = useProjectView();
const { allowPermissions } = useUserPermissions();
// derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Views` : undefined;
const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/views" });
const handleRemoveFilter = useCallback(
(key: keyof TViewFilterProps, value: string | EViewAccess | null) => {
@@ -61,17 +53,9 @@ const ProjectViewsPage = observer(() => {
if (currentProjectDetails?.issue_views_view === false)
return (
<div className="flex items-center justify-center h-full w-full">
<DetailedEmptyState
title={t("disabled_project.empty_state.view.title")}
description={t("disabled_project.empty_state.view.description")}
assetPath={resolvedPath}
primaryButton={{
text: t("disabled_project.empty_state.view.primary_button.text"),
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
},
disabled: !canPerformEmptyStateActions,
}}
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_VIEW}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
/>
</div>
);
@@ -4,19 +4,19 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// plane imports
import { useTranslation } from "@plane/i18n";
// ui
import { Button } from "@plane/ui";
// component
import { ApiTokenListItem, CreateApiTokenModal } from "@/components/api-token";
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { DetailedEmptyState } from "@/components/empty-state";
import { EmptyState } from "@/components/empty-state";
import { APITokenSettingsLoader } from "@/components/ui";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
// store hooks
import { useUserPermissions, useWorkspace } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// services
import { APITokenService } from "@/services/api_token.service";
@@ -28,14 +28,11 @@ const ApiTokensPage = observer(() => {
const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false);
// router
const { workspaceSlug } = useParams();
// plane hooks
const { t } = useTranslation();
// store hooks
const { currentWorkspace } = useWorkspace();
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
// derived values
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/api-tokens" });
const { data: tokens } = useSWR(
workspaceSlug && canPerformWorkspaceAdminActions ? API_TOKENS_LIST(workspaceSlug.toString()) : null,
@@ -81,11 +78,7 @@ const ApiTokensPage = observer(() => {
</Button>
</div>
<div className="h-full w-full flex items-center justify-center">
<DetailedEmptyState
title={t("workspace_settings.empty_state.api_tokens.title")}
description={t("workspace_settings.empty_state.api_tokens.description")}
assetPath={resolvedPath}
/>
<EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_API_TOKENS} />
</div>
</div>
)}
@@ -1,12 +1,11 @@
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { WORKSPACE_SETTINGS_LINKS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// hooks
import { useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// plane web constants
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace";
// plane web helpers
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
@@ -14,7 +13,6 @@ export const MobileWorkspaceSettingsTabs = observer(() => {
const router = useAppRouter();
const { workspaceSlug } = useParams();
const pathname = usePathname();
const { t } = useTranslation();
// mobx store
const { allowPermissions } = useUserPermissions();
@@ -33,7 +31,7 @@ export const MobileWorkspaceSettingsTabs = observer(() => {
key={index}
onClick={() => router.push(`/${workspaceSlug}${item.href}`)}
>
{t(item.i18n_label)}
{item.label}
</div>
)
)}
@@ -4,14 +4,13 @@ import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { WORKSPACE_SETTINGS_LINKS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
import { SidebarNavItem } from "@/components/sidebar";
// hooks
import { useUserPermissions } from "@/hooks/store";
// plane web constants
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace";
// plane web helpers
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
@@ -20,7 +19,6 @@ export const WorkspaceSettingsSidebar = observer(() => {
const { workspaceSlug } = useParams();
const pathname = usePathname();
// mobx store
const { t } = useTranslation();
const { allowPermissions } = useUserPermissions();
return (
@@ -38,7 +36,7 @@ export const WorkspaceSettingsSidebar = observer(() => {
isActive={link.highlight(pathname, `/${workspaceSlug}`)}
className="text-sm font-medium px-4 py-2"
>
{t(link.i18n_label)}
{link.label}
</SidebarNavItem>
</Link>
)
@@ -4,18 +4,18 @@ import React, { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// plane imports
import { useTranslation } from "@plane/i18n";
// ui
import { Button } from "@plane/ui";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { DetailedEmptyState } from "@/components/empty-state";
import { EmptyState } from "@/components/empty-state";
import { WebhookSettingsLoader } from "@/components/ui";
import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// hooks
import { useUserPermissions, useWebhook, useWorkspace } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const WebhooksListPage = observer(() => {
@@ -23,15 +23,13 @@ const WebhooksListPage = observer(() => {
const [showCreateWebhookModal, setShowCreateWebhookModal] = useState(false);
// router
const { workspaceSlug } = useParams();
// plane hooks
const { t } = useTranslation();
// mobx store
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook();
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,
@@ -83,11 +81,7 @@ const WebhooksListPage = observer(() => {
</Button>
</div>
<div className="h-full w-full flex items-center justify-center">
<DetailedEmptyState
title={t("workspace_settings.empty_state.webhooks.title")}
description={t("workspace_settings.empty_state.webhooks.description")}
assetPath={resolvedPath}
/>
<EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_WEBHOOKS} />
</div>
</div>
)}
@@ -3,13 +3,12 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { DEFAULT_GLOBAL_VIEWS_LIST } from "@plane/constants";
// components
import { PageHead } from "@/components/core";
import { AllIssueLayoutRoot, GlobalViewsAppliedFiltersRoot } from "@/components/issues";
import { GlobalViewsHeader } from "@/components/workspace";
// constants
import { DEFAULT_GLOBAL_VIEWS_LIST } from "@/constants/workspace";
// hooks
import { useWorkspace } from "@/hooks/store";
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Layers } from "lucide-react";
// plane constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
import { EIssueFilterType, EIssuesStoreType } from "@plane/constants";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
@@ -14,6 +14,8 @@ import { Breadcrumbs, Button, Header } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues";
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
@@ -117,7 +119,7 @@ export const GlobalIssuesHeader = observer(() => {
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.spreadsheet}
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
@@ -128,7 +130,7 @@ export const GlobalIssuesHeader = observer(() => {
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.spreadsheet}
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
@@ -4,15 +4,13 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
// icons
import { Search } from "lucide-react";
// plane imports
import { DEFAULT_GLOBAL_VIEWS_LIST } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { Input } from "@plane/ui";
// components
import { PageHead } from "@/components/core";
import { GlobalDefaultViewListItem, GlobalViewsList } from "@/components/workspace";
// constants
import { DEFAULT_GLOBAL_VIEWS_LIST } from "@/constants/workspace";
// hooks
import { useWorkspace } from "@/hooks/store";
@@ -20,7 +18,6 @@ const WorkspaceViewsPage = observer(() => {
const [query, setQuery] = useState("");
// store
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - All Views` : undefined;
@@ -39,7 +36,7 @@ const WorkspaceViewsPage = observer(() => {
/>
</div>
<div className="flex flex-col h-full w-full vertical-scrollbar scrollbar-lg">
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => t(v.i18n_label).toLowerCase().includes(query.toLowerCase())).map(
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map(
(option) => (
<GlobalDefaultViewListItem key={option.key} view={option} />
)
+1 -2
View File
@@ -8,8 +8,6 @@ import Link from "next/link";
import { useTheme } from "next-themes";
import useSWR, { mutate } from "swr";
import { CheckCircle2 } from "lucide-react";
// plane imports
import { ROLE } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import type { IWorkspaceMemberInvitation } from "@plane/types";
@@ -20,6 +18,7 @@ import { EmptyState } from "@/components/common";
// constants
import { MEMBER_ACCEPTED } from "@/constants/event-tracker";
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
import { ROLE } from "@/constants/workspace";
// helpers
import { truncateText } from "@/helpers/string.helper";
import { getUserRole } from "@/helpers/user.helper";
+5 -14
View File
@@ -7,27 +7,24 @@ import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/ui";
// components
import { PageHead } from "@/components/core";
import { DetailedEmptyState } from "@/components/empty-state";
import { EmptyState } from "@/components/empty-state";
import {
ProfileActivityListPage,
ProfileSettingContentHeader,
ProfileSettingContentWrapper,
} from "@/components/profile";
// hooks
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// constants
import { EmptyStateType } from "@/constants/empty-state";
const PER_PAGE = 100;
const ProfileActivityPage = observer(() => {
const { t } = useTranslation();
// states
const [pageCount, setPageCount] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [resultsCount, setResultsCount] = useState(0);
const [isEmpty, setIsEmpty] = useState(false);
// plane hooks
const { t } = useTranslation();
// derived values
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/profile/activity" });
const updateTotalPages = (count: number) => setTotalPages(count);
@@ -53,13 +50,7 @@ const ProfileActivityPage = observer(() => {
const isLoadMoreVisible = pageCount < totalPages && resultsCount !== 0;
if (isEmpty) {
return (
<DetailedEmptyState
title={t("profile.empty_state.activity.title")}
description={t("profile.empty_state.activity.description")}
assetPath={resolvedPath}
/>
);
return <EmptyState type={EmptyStateType.PROFILE_ACTIVITY} layout="screen-detailed" />;
}
return (
+1 -1
View File
@@ -25,7 +25,7 @@ export default function ProfileNotificationPage() {
return (
<>
<PageHead title={`${t("profile.label")} - ${t("notifications")}`} />
<PageHead title={`${t("profile")} - ${t("notifications")}`} />
<ProfileSettingContentWrapper>
<ProfileSettingContentHeader
title={t("email_notifications")}
+1 -1
View File
@@ -23,7 +23,7 @@ const ProfileSettingsPage = observer(() => {
return (
<>
<PageHead title={`${t("profile.label")} - ${t("general_settings")}`} />
<PageHead title={`${t("profile")} - ${t("general_settings")}`} />
<ProfileSettingContentWrapper>
<ProfileForm user={currentUser} profile={userProfile.data} />
</ProfileSettingContentWrapper>
+3 -28
View File
@@ -5,26 +5,15 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
// icons
import {
ChevronLeft,
LogOut,
MoveLeft,
Plus,
UserPlus,
Activity,
Bell,
CircleUser,
KeyRound,
Settings2,
} from "lucide-react";
import { ChevronLeft, LogOut, MoveLeft, Plus, UserPlus } from "lucide-react";
// plane imports
import { PROFILE_ACTION_LINKS } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
// components
import { SidebarNavItem } from "@/components/sidebar";
// constants
import { PROFILE_ACTION_LINKS } from "@/constants/profile";
// helpers
import { cn } from "@/helpers/common.helper";
import { getFileURL } from "@/helpers/file.helper";
@@ -47,19 +36,6 @@ const WORKSPACE_ACTION_LINKS = [
},
];
export const ProjectActionIcons = ({ type, size, className }: { type: string; size?: number; className?: string }) => {
const icons = {
profile: CircleUser,
security: KeyRound,
activity: Activity,
appearance: Settings2,
notifications: Bell,
};
if (type === undefined) return null;
const Icon = icons[type as keyof typeof icons];
return <Icon size={size} className={className} />;
};
export const ProfileLayoutSidebar = observer(() => {
// states
const [isSigningOut, setIsSigningOut] = useState(false);
@@ -169,8 +145,7 @@ export const ProfileLayoutSidebar = observer(() => {
isActive={link.highlight(pathname)}
>
<div className="flex items-center gap-1.5 py-[1px]">
<ProjectActionIcons type={link.key} size={16} />
<link.Icon className="size-4" />
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(link.key)}</p>}
</div>
</SidebarNavItem>
+5 -14
View File
@@ -3,8 +3,7 @@
import { useMemo } from "react";
import { observer } from "mobx-react";
import { Disclosure } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
// ui
import { Row } from "@plane/ui";
// components
import {
@@ -15,10 +14,10 @@ import {
CyclesListItem,
} from "@/components/cycles";
import useCyclesDetails from "@/components/cycles/active-cycle/use-cycles-details";
import { DetailedEmptyState } from "@/components/empty-state";
// hooks
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { useCycle } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { ActiveCycleIssueDetails } from "@/store/issue/cycle";
interface IActiveCycleDetails {
@@ -30,13 +29,9 @@ interface IActiveCycleDetails {
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
const { workspaceSlug, projectId, cycleId: propsCycleId, showHeader = true } = props;
// plane hooks
const { t } = useTranslation();
// store hooks
const { currentProjectActiveCycleId } = useCycle();
// derived values
const cycleId = propsCycleId ?? currentProjectActiveCycleId;
const activeCycleResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/cycle/active" });
// fetch cycle details
const {
handleFiltersUpdate,
@@ -48,11 +43,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
() => (
<>
{!cycleId || !activeCycle ? (
<DetailedEmptyState
title={t("project_cycles.empty_state.active.title")}
description={t("project_cycles.empty_state.active.description")}
assetPath={activeCycleResolvedPath}
/>
<EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />
) : (
<div className="flex flex-col border-b border-custom-border-200">
{cycleId && (
@@ -2,9 +2,9 @@
import { FC } from "react";
// components
import { TActivityFilters, ACTIVITY_FILTER_TYPE_OPTIONS, TActivityFilterOption } from "@plane/constants";
import { ActivityFilter } from "@/components/issues";
// plane web constants
import { TActivityFilters, ACTIVITY_FILTER_TYPE_OPTIONS, TActivityFilterOption } from "@/plane-web/constants/issues";
export type TActivityFilterRoot = {
selectedFilters: TActivityFilters[];
@@ -20,7 +20,7 @@ export const ActivityFilterRoot: FC<TActivityFilterRoot> = (props) => {
const filterKey = key as TActivityFilters;
return {
key: filterKey,
labelTranslationKey: value.labelTranslationKey,
label: value.label,
isSelected: selectedFilters.includes(filterKey),
onClick: () => toggleFilter(filterKey),
};
@@ -1,13 +1,15 @@
"use client";
import { FC } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { NETWORK_CHOICES, ETabIndices } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IProject } from "@plane/types";
// ui
import { CustomSelect } from "@plane/ui";
// components
import { MemberDropdown } from "@/components/dropdowns";
// constants
import { NETWORK_CHOICES } from "@/constants/project";
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getTabIndex } from "@/helpers/tab-indices.helper";
@@ -38,7 +40,7 @@ const ProjectAttributes: FC<Props> = (props) => {
{currentNetwork ? (
<>
<currentNetwork.icon className="h-3 w-3" />
{t(currentNetwork.i18n_label)}
{currentNetwork.label}
</>
) : (
<span className="text-custom-text-400">{t("select_network")}</span>
@@ -56,8 +58,8 @@ const ProjectAttributes: FC<Props> = (props) => {
<div className="flex items-start gap-2">
<network.icon className="h-3.5 w-3.5" />
<div className="-mt-1">
<p>{t(network.i18n_label)}</p>
<p className="text-xs text-custom-text-400">{t(network.description)}</p>
<p>{network.label}</p>
<p className="text-xs text-custom-text-400">{network.description}</p>
</div>
</div>
</CustomSelect.Option>
+1 -1
View File
@@ -3,7 +3,6 @@
import { useState, FC } from "react";
import { observer } from "mobx-react";
import { FormProvider, useForm } from "react-hook-form";
import { PROJECT_UNSPLASH_COVERS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { setToast, TOAST_TYPE } from "@plane/ui";
@@ -12,6 +11,7 @@ import ProjectCommonAttributes from "@/components/project/create/common-attribut
import ProjectCreateHeader from "@/components/project/create/header";
import ProjectCreateButtons from "@/components/project/create/project-create-buttons";
import { PROJECT_CREATED } from "@/constants/event-tracker";
import { PROJECT_UNSPLASH_COVERS } from "@/constants/project";
// helpers
import { getRandomEmoji } from "@/helpers/emoji.helper";
// hooks
+3 -1
View File
@@ -1,5 +1,7 @@
export * from "./ai";
export * from "./estimates";
export * from "./gantt-chart";
export * from "./issues";
export * from "./project";
export * from "./user-permissions";
export * from "./user-permissions";
export * from "./workspace";
+38
View File
@@ -0,0 +1,38 @@
import { ILayoutDisplayFiltersOptions, TIssueActivityComment } from "@plane/types";
export enum EActivityFilterType {
ACTIVITY = "ACTIVITY",
COMMENT = "COMMENT",
}
export type TActivityFilters = EActivityFilterType;
export const ACTIVITY_FILTER_TYPE_OPTIONS: Record<EActivityFilterType, { label: string }> = {
[EActivityFilterType.ACTIVITY]: {
label: "Updates",
},
[EActivityFilterType.COMMENT]: {
label: "Comments",
},
};
export const defaultActivityFilters: TActivityFilters[] = [EActivityFilterType.ACTIVITY, EActivityFilterType.COMMENT];
export type TActivityFilterOption = {
key: TActivityFilters;
label: string;
isSelected: boolean;
onClick: () => void;
};
export const filterActivityOnSelectedFilters = (
activity: TIssueActivityComment[],
filter: TActivityFilters[]
): TIssueActivityComment[] =>
activity.filter((activity) => filter.includes(activity.activity_type as TActivityFilters));
export const ENABLE_ISSUE_DEPENDENCIES = false;
export const ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions };
} = {};
+72
View File
@@ -0,0 +1,72 @@
// icons
import { SettingIcon } from "@/components/icons/attachment";
import { Props } from "@/components/icons/types";
import { EUserPermissions } from "./user-permissions";
// constants
export const WORKSPACE_SETTINGS = {
general: {
key: "general",
label: "General",
href: `/settings`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`,
Icon: SettingIcon,
},
members: {
key: "members",
label: "Members",
href: `/settings/members`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`,
Icon: SettingIcon,
},
"billing-and-plans": {
key: "billing-and-plans",
label: "Billing and plans",
href: `/settings/billing`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/billing/`,
Icon: SettingIcon,
},
export: {
key: "export",
label: "Exports",
href: `/settings/exports`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`,
Icon: SettingIcon,
},
webhooks: {
key: "webhooks",
label: "Webhooks",
href: `/settings/webhooks`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/webhooks/`,
Icon: SettingIcon,
},
"api-tokens": {
key: "api-tokens",
label: "API tokens",
href: `/settings/api-tokens`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/api-tokens/`,
Icon: SettingIcon,
},
};
export const WORKSPACE_SETTINGS_LINKS: {
key: string;
label: string;
href: string;
access: EUserPermissions[];
highlight: (pathname: string, baseUrl: string) => boolean;
Icon: React.FC<Props>;
}[] = [
WORKSPACE_SETTINGS["general"],
WORKSPACE_SETTINGS["members"],
WORKSPACE_SETTINGS["billing-and-plans"],
WORKSPACE_SETTINGS["export"],
WORKSPACE_SETTINGS["webhooks"],
WORKSPACE_SETTINGS["api-tokens"],
];

Some files were not shown because too many files have changed in this diff Show More