Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a1f8e6e39 | |||
| 16d63abcdc | |||
| 0568b8d583 | |||
| 64da29b0d9 | |||
| 7c336a65c4 | |||
| 2242a85e5c | |||
| 323920a358 | |||
| 151fc8389e | |||
| 0f828fd5e0 | |||
| 67cbe94d4a | |||
| 322af8c436 | |||
| 41c2aefad4 | |||
| 445c819fbd | |||
| 046a8a1bcf | |||
| 099a1cc12b | |||
| a0a697401b | |||
| cb92108bf4 | |||
| 01b685ea57 | |||
| b16a585102 | |||
| 4a97d7c28c |
+2
-1
@@ -2,6 +2,7 @@
|
||||
*.pyc
|
||||
.env
|
||||
venv
|
||||
.venv
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
npm-debug.log
|
||||
@@ -14,4 +15,4 @@ build/
|
||||
out/
|
||||
**/out/
|
||||
dist/
|
||||
**/dist/
|
||||
**/dist/
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@
|
||||
"lucide-react": "^0.469.0",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"next": "^14.2.28",
|
||||
"next": "^14.2.29",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.38",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -148,10 +148,13 @@ class ProjectMemberAdminSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
|
||||
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
|
||||
original_role = serializers.IntegerField(source='role', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ProjectMember
|
||||
fields = ("id", "role", "member", "project")
|
||||
fields = ("id", "role", "member", "project", "original_role", "created_at")
|
||||
read_only_fields = ["original_role", "created_at"]
|
||||
|
||||
|
||||
class ProjectMemberInviteSerializer(BaseSerializer):
|
||||
|
||||
@@ -3,11 +3,22 @@ from rest_framework import serializers
|
||||
|
||||
# Module import
|
||||
from plane.db.models import Account, Profile, User, Workspace, WorkspaceMemberInvite
|
||||
from plane.utils.url import contains_url
|
||||
|
||||
from .base import BaseSerializer
|
||||
|
||||
|
||||
class UserSerializer(BaseSerializer):
|
||||
def validate_first_name(self, value):
|
||||
if contains_url(value):
|
||||
raise serializers.ValidationError("First name cannot contain a URL.")
|
||||
return value
|
||||
|
||||
def validate_last_name(self, value):
|
||||
if contains_url(value):
|
||||
raise serializers.ValidationError("Last name cannot contain a URL.")
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
# Exclude password field from the serializer
|
||||
@@ -99,11 +110,16 @@ class UserMeSettingsSerializer(BaseSerializer):
|
||||
workspace_member__member=obj.id,
|
||||
workspace_member__is_active=True,
|
||||
).first()
|
||||
logo_asset_url = workspace.logo_asset.asset_url if workspace.logo_asset is not None else ""
|
||||
return {
|
||||
"last_workspace_id": profile.last_workspace_id,
|
||||
"last_workspace_slug": (
|
||||
workspace.slug if workspace is not None else ""
|
||||
),
|
||||
"last_workspace_name": (
|
||||
workspace.name if workspace is not None else ""
|
||||
),
|
||||
"last_workspace_logo": (logo_asset_url),
|
||||
"fallback_workspace_id": profile.last_workspace_id,
|
||||
"fallback_workspace_slug": (
|
||||
workspace.slug if workspace is not None else ""
|
||||
|
||||
@@ -25,10 +25,12 @@ from plane.db.models import (
|
||||
WorkspaceUserPreference,
|
||||
)
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
from plane.utils.url import contains_url
|
||||
|
||||
# Django imports
|
||||
from django.core.validators import URLValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
import re
|
||||
|
||||
|
||||
class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
@@ -36,10 +38,21 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
logo_url = serializers.CharField(read_only=True)
|
||||
role = serializers.IntegerField(read_only=True)
|
||||
|
||||
def validate_name(self, value):
|
||||
# Check if the name contains a URL
|
||||
if contains_url(value):
|
||||
raise serializers.ValidationError("Name must not contain URLs")
|
||||
return value
|
||||
|
||||
def validate_slug(self, value):
|
||||
# Check if the slug is restricted
|
||||
if value in RESTRICTED_WORKSPACE_SLUGS:
|
||||
raise serializers.ValidationError("Slug is not valid")
|
||||
# Slug should only contain alphanumeric characters, hyphens, and underscores
|
||||
if not re.match(r"^[a-zA-Z0-9_-]+$", value):
|
||||
raise serializers.ValidationError(
|
||||
"Slug can only contain letters, numbers, hyphens (-), and underscores (_)"
|
||||
)
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -12,6 +12,7 @@ from plane.app.views import (
|
||||
AssetRestoreEndpoint,
|
||||
ProjectAssetEndpoint,
|
||||
ProjectBulkAssetEndpoint,
|
||||
AssetCheckEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -81,5 +82,11 @@ urlpatterns = [
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/projects/<uuid:project_id>/<uuid:entity_id>/bulk/",
|
||||
ProjectBulkAssetEndpoint.as_view(),
|
||||
name="bulk-asset-update",
|
||||
),
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/check/<uuid:asset_id>/",
|
||||
AssetCheckEndpoint.as_view(),
|
||||
name="asset-check",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -106,6 +106,7 @@ from .asset.v2 import (
|
||||
AssetRestoreEndpoint,
|
||||
ProjectAssetEndpoint,
|
||||
ProjectBulkAssetEndpoint,
|
||||
AssetCheckEndpoint,
|
||||
)
|
||||
from .issue.base import (
|
||||
IssueListEndpoint,
|
||||
|
||||
@@ -707,3 +707,14 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
|
||||
pass
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class AssetCheckEndpoint(BaseAPIView):
|
||||
"""Endpoint to check if an asset exists."""
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def get(self, request, slug, asset_id):
|
||||
asset = FileAsset.all_objects.filter(
|
||||
id=asset_id, workspace__slug=slug, deleted_at__isnull=True
|
||||
).exists()
|
||||
return Response({"exists": asset}, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -15,6 +15,7 @@ from plane.app.serializers import IssueLinkSerializer
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import IssueLink
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.bgtasks.work_item_link_task import crawl_work_item_link_title
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
@@ -44,6 +45,9 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
serializer = IssueLinkSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id, issue_id=issue_id)
|
||||
crawl_work_item_link_title(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="link.activity.created",
|
||||
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
@@ -55,6 +59,10 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
|
||||
issue_link = self.get_queryset().get(id=serializer.data.get("id"))
|
||||
serializer = IssueLinkSerializer(issue_link)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -66,9 +74,14 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
current_instance = json.dumps(
|
||||
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
crawl_work_item_link_title(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
|
||||
issue_activity.delay(
|
||||
type="link.activity.updated",
|
||||
requested_data=requested_data,
|
||||
@@ -80,6 +93,9 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
issue_link = self.get_queryset().get(id=serializer.data.get("id"))
|
||||
serializer = IssueLinkSerializer(issue_link)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import csv
|
||||
import io
|
||||
import os
|
||||
from datetime import date
|
||||
import uuid
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.db import IntegrityError
|
||||
@@ -35,6 +36,7 @@ from plane.db.models import (
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
WorkspaceTheme,
|
||||
Profile
|
||||
)
|
||||
from plane.app.permissions import ROLE, allow_permission
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -43,6 +45,7 @@ from django.views.decorators.vary import vary_on_cookie
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.bgtasks.workspace_seed_task import workspace_seed
|
||||
from plane.utils.url import contains_url
|
||||
|
||||
|
||||
class WorkSpaceViewSet(BaseViewSet):
|
||||
@@ -109,6 +112,12 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if contains_url(name):
|
||||
return Response(
|
||||
{"error": "Name cannot contain a URL"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
serializer.save(owner=request.user)
|
||||
# Create Workspace member
|
||||
@@ -150,8 +159,19 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
|
||||
def remove_last_workspace_ids_from_user_settings(self, id: uuid.UUID) -> None:
|
||||
"""
|
||||
Remove the last workspace id from the user settings
|
||||
"""
|
||||
Profile.objects.filter(last_workspace_id=id).update(last_workspace_id=None)
|
||||
return
|
||||
|
||||
@allow_permission([ROLE.ADMIN], level="WORKSPACE")
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
# Get the workspace
|
||||
workspace = self.get_object()
|
||||
self.remove_last_workspace_ids_from_user_settings(workspace.id)
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# Python imports
|
||||
import logging
|
||||
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urlparse, urljoin
|
||||
import base64
|
||||
import ipaddress
|
||||
from typing import Dict, Any
|
||||
from typing import Optional
|
||||
from plane.db.models import IssueLink
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
logger = logging.getLogger("plane.worker")
|
||||
|
||||
|
||||
DEFAULT_FAVICON = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWxpbmstaWNvbiBsdWNpZGUtbGluayI+PHBhdGggZD0iTTEwIDEzYTUgNSAwIDAgMCA3LjU0LjU0bDMtM2E1IDUgMCAwIDAtNy4wNy03LjA3bC0xLjcyIDEuNzEiLz48cGF0aCBkPSJNMTQgMTFhNSA1IDAgMCAwLTcuNTQtLjU0bC0zIDNhNSA1IDAgMCAwIDcuMDcgNy4wN2wxLjcxLTEuNzEiLz48L3N2Zz4=" # noqa: E501
|
||||
|
||||
|
||||
@shared_task
|
||||
def crawl_work_item_link_title(id: str, url: str) -> None:
|
||||
meta_data = crawl_work_item_link_title_and_favicon(url)
|
||||
issue_link = IssueLink.objects.get(id=id)
|
||||
|
||||
issue_link.metadata = meta_data
|
||||
|
||||
issue_link.save()
|
||||
|
||||
|
||||
def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Crawls a URL to extract the title and favicon.
|
||||
|
||||
Args:
|
||||
url (str): The URL to crawl
|
||||
|
||||
Returns:
|
||||
str: JSON string containing title and base64-encoded favicon
|
||||
"""
|
||||
try:
|
||||
# Prevent access to private IP ranges
|
||||
parsed = urlparse(url)
|
||||
|
||||
try:
|
||||
ip = ipaddress.ip_address(parsed.hostname)
|
||||
if ip.is_private or ip.is_loopback or ip.is_reserved:
|
||||
raise ValueError("Access to private/internal networks is not allowed")
|
||||
except ValueError:
|
||||
# Not an IP address, continue with domain validation
|
||||
pass
|
||||
|
||||
# Set up headers to mimic a real browser
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" # noqa: E501
|
||||
}
|
||||
|
||||
# Fetch the main page
|
||||
response = requests.get(url, headers=headers, timeout=2)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse HTML
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
|
||||
# Extract title
|
||||
title_tag = soup.find("title")
|
||||
title = title_tag.get_text().strip() if title_tag else None
|
||||
|
||||
# Fetch and encode favicon
|
||||
favicon_base64 = fetch_and_encode_favicon(headers, soup, url)
|
||||
|
||||
# Prepare result
|
||||
result = {
|
||||
"title": title,
|
||||
"favicon": favicon_base64["favicon_base64"],
|
||||
"url": url,
|
||||
"favicon_url": favicon_base64["favicon_url"],
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
except requests.RequestException as e:
|
||||
log_exception(e)
|
||||
return {
|
||||
"error": f"Request failed: {str(e)}",
|
||||
"title": None,
|
||||
"favicon": None,
|
||||
"url": url,
|
||||
}
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return {
|
||||
"error": f"Unexpected error: {str(e)}",
|
||||
"title": None,
|
||||
"favicon": None,
|
||||
"url": url,
|
||||
}
|
||||
|
||||
|
||||
def find_favicon_url(soup: BeautifulSoup, base_url: str) -> Optional[str]:
|
||||
"""
|
||||
Find the favicon URL from HTML soup.
|
||||
|
||||
Args:
|
||||
soup: BeautifulSoup object
|
||||
base_url: Base URL for resolving relative paths
|
||||
|
||||
Returns:
|
||||
str: Absolute URL to favicon or None
|
||||
"""
|
||||
# Look for various favicon link tags
|
||||
favicon_selectors = [
|
||||
'link[rel="icon"]',
|
||||
'link[rel="shortcut icon"]',
|
||||
'link[rel="apple-touch-icon"]',
|
||||
'link[rel="apple-touch-icon-precomposed"]',
|
||||
]
|
||||
|
||||
for selector in favicon_selectors:
|
||||
favicon_tag = soup.select_one(selector)
|
||||
if favicon_tag and favicon_tag.get("href"):
|
||||
return urljoin(base_url, favicon_tag["href"])
|
||||
|
||||
# Fallback to /favicon.ico
|
||||
parsed_url = urlparse(base_url)
|
||||
fallback_url = f"{parsed_url.scheme}://{parsed_url.netloc}/favicon.ico"
|
||||
|
||||
# Check if fallback exists
|
||||
try:
|
||||
response = requests.head(fallback_url, timeout=2)
|
||||
response.raise_for_status()
|
||||
if response.status_code == 200:
|
||||
return fallback_url
|
||||
except requests.RequestException as e:
|
||||
log_exception(e)
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def fetch_and_encode_favicon(
|
||||
headers: Dict[str, str], soup: BeautifulSoup, url: str
|
||||
) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
Fetch favicon and encode it as base64.
|
||||
|
||||
Args:
|
||||
favicon_url: URL to the favicon
|
||||
headers: Request headers
|
||||
|
||||
Returns:
|
||||
str: Base64 encoded favicon with data URI prefix or None
|
||||
"""
|
||||
try:
|
||||
favicon_url = find_favicon_url(soup, url)
|
||||
if favicon_url is None:
|
||||
return {
|
||||
"favicon_url": None,
|
||||
"favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}",
|
||||
}
|
||||
|
||||
response = requests.get(favicon_url, headers=headers, timeout=2)
|
||||
response.raise_for_status()
|
||||
|
||||
# Get content type
|
||||
content_type = response.headers.get("content-type", "image/x-icon")
|
||||
|
||||
# Convert to base64
|
||||
favicon_base64 = base64.b64encode(response.content).decode("utf-8")
|
||||
|
||||
# Return as data URI
|
||||
return {
|
||||
"favicon_url": favicon_url,
|
||||
"favicon_base64": f"data:{content_type};base64,{favicon_base64}",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch favicon: {e}")
|
||||
return {
|
||||
"favicon_url": None,
|
||||
"favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}",
|
||||
}
|
||||
@@ -4,6 +4,14 @@ from typing import Optional
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
|
||||
def contains_url(value: str) -> bool:
|
||||
"""
|
||||
Check if the value contains a URL.
|
||||
"""
|
||||
url_pattern = re.compile(r"https?://|www\\.")
|
||||
return bool(url_pattern.search(value))
|
||||
|
||||
|
||||
def is_valid_url(url: str) -> bool:
|
||||
"""
|
||||
Validates whether the given string is a well-formed URL.
|
||||
|
||||
@@ -32,5 +32,6 @@ export * from "./dashboard";
|
||||
export * from "./page";
|
||||
export * from "./emoji";
|
||||
export * from "./subscription";
|
||||
export * from "./settings";
|
||||
export * from "./icon";
|
||||
export * from "./analytics-v2";
|
||||
|
||||
@@ -1,39 +1,53 @@
|
||||
export const PROFILE_SETTINGS = {
|
||||
profile: {
|
||||
key: "profile",
|
||||
i18n_label: "profile.actions.profile",
|
||||
href: `/settings/account`,
|
||||
highlight: (pathname: string) => pathname === "/settings/account/",
|
||||
},
|
||||
security: {
|
||||
key: "security",
|
||||
i18n_label: "profile.actions.security",
|
||||
href: `/settings/account/security`,
|
||||
highlight: (pathname: string) => pathname === "/settings/account/security/",
|
||||
},
|
||||
activity: {
|
||||
key: "activity",
|
||||
i18n_label: "profile.actions.activity",
|
||||
href: `/settings/account/activity`,
|
||||
highlight: (pathname: string) => pathname === "/settings/account/activity/",
|
||||
},
|
||||
preferences: {
|
||||
key: "preferences",
|
||||
i18n_label: "profile.actions.preferences",
|
||||
href: `/settings/account/preferences`,
|
||||
highlight: (pathname: string) => pathname === "/settings/account/preferences",
|
||||
},
|
||||
notifications: {
|
||||
key: "notifications",
|
||||
i18n_label: "profile.actions.notifications",
|
||||
href: `/settings/account/notifications`,
|
||||
highlight: (pathname: string) => pathname === "/settings/account/notifications/",
|
||||
},
|
||||
"api-tokens": {
|
||||
key: "api-tokens",
|
||||
i18n_label: "profile.actions.api-tokens",
|
||||
href: `/settings/account/api-tokens`,
|
||||
highlight: (pathname: string) => pathname === "/settings/account/api-tokens/",
|
||||
},
|
||||
};
|
||||
export const PROFILE_ACTION_LINKS: {
|
||||
key: string;
|
||||
i18n_label: string;
|
||||
href: string;
|
||||
highlight: (pathname: string) => boolean;
|
||||
}[] = [
|
||||
{
|
||||
key: "profile",
|
||||
i18n_label: "profile.actions.profile",
|
||||
href: `/profile`,
|
||||
highlight: (pathname: string) => pathname === "/profile/",
|
||||
},
|
||||
{
|
||||
key: "security",
|
||||
i18n_label: "profile.actions.security",
|
||||
href: `/profile/security`,
|
||||
highlight: (pathname: string) => pathname === "/profile/security/",
|
||||
},
|
||||
{
|
||||
key: "activity",
|
||||
i18n_label: "profile.actions.activity",
|
||||
href: `/profile/activity`,
|
||||
highlight: (pathname: string) => pathname === "/profile/activity/",
|
||||
},
|
||||
{
|
||||
key: "appearance",
|
||||
i18n_label: "profile.actions.appearance",
|
||||
href: `/profile/appearance`,
|
||||
highlight: (pathname: string) => pathname.includes("/profile/appearance"),
|
||||
},
|
||||
{
|
||||
key: "notifications",
|
||||
i18n_label: "profile.actions.notifications",
|
||||
href: `/profile/notifications`,
|
||||
highlight: (pathname: string) => pathname === "/profile/notifications/",
|
||||
},
|
||||
PROFILE_SETTINGS["profile"],
|
||||
PROFILE_SETTINGS["security"],
|
||||
PROFILE_SETTINGS["activity"],
|
||||
PROFILE_SETTINGS["preferences"],
|
||||
PROFILE_SETTINGS["notifications"],
|
||||
PROFILE_SETTINGS["api-tokens"],
|
||||
];
|
||||
|
||||
export const PROFILE_VIEWER_TAB = [
|
||||
@@ -72,6 +86,23 @@ export const PROFILE_ADMINS_TAB = [
|
||||
},
|
||||
];
|
||||
|
||||
export const PREFERENCE_OPTIONS: {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}[] = [
|
||||
{
|
||||
id: "theme",
|
||||
title: "theme",
|
||||
description: "select_or_customize_your_interface_color_scheme",
|
||||
},
|
||||
{
|
||||
id: "start_of_week",
|
||||
title: "First day of the week",
|
||||
description: "This will change how all calendars in your app look.",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @description The start of the week for the user
|
||||
* @enum {number}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { PROFILE_SETTINGS } from ".";
|
||||
import { WORKSPACE_SETTINGS } from "./workspace";
|
||||
|
||||
export enum WORKSPACE_SETTINGS_CATEGORY {
|
||||
ADMINISTRATION = "administration",
|
||||
FEATURES = "features",
|
||||
DEVELOPER = "developer",
|
||||
}
|
||||
|
||||
export enum PROFILE_SETTINGS_CATEGORY {
|
||||
YOUR_PROFILE = "your profile",
|
||||
DEVELOPER = "developer",
|
||||
}
|
||||
|
||||
export enum PROJECT_SETTINGS_CATEGORY {
|
||||
PROJECTS = "projects",
|
||||
}
|
||||
|
||||
export const WORKSPACE_SETTINGS_CATEGORIES = [
|
||||
WORKSPACE_SETTINGS_CATEGORY.ADMINISTRATION,
|
||||
WORKSPACE_SETTINGS_CATEGORY.FEATURES,
|
||||
WORKSPACE_SETTINGS_CATEGORY.DEVELOPER,
|
||||
];
|
||||
|
||||
export const PROFILE_SETTINGS_CATEGORIES = [
|
||||
PROFILE_SETTINGS_CATEGORY.YOUR_PROFILE,
|
||||
PROFILE_SETTINGS_CATEGORY.DEVELOPER,
|
||||
];
|
||||
|
||||
export const PROJECT_SETTINGS_CATEGORIES = [PROJECT_SETTINGS_CATEGORY.PROJECTS];
|
||||
|
||||
export const GROUPED_WORKSPACE_SETTINGS = {
|
||||
[WORKSPACE_SETTINGS_CATEGORY.ADMINISTRATION]: [
|
||||
WORKSPACE_SETTINGS["general"],
|
||||
WORKSPACE_SETTINGS["members"],
|
||||
WORKSPACE_SETTINGS["billing-and-plans"],
|
||||
WORKSPACE_SETTINGS["export"],
|
||||
],
|
||||
[WORKSPACE_SETTINGS_CATEGORY.FEATURES]: [],
|
||||
[WORKSPACE_SETTINGS_CATEGORY.DEVELOPER]: [WORKSPACE_SETTINGS["webhooks"]],
|
||||
};
|
||||
|
||||
export const GROUPED_PROFILE_SETTINGS = {
|
||||
[PROFILE_SETTINGS_CATEGORY.YOUR_PROFILE]: [
|
||||
PROFILE_SETTINGS["profile"],
|
||||
PROFILE_SETTINGS["preferences"],
|
||||
PROFILE_SETTINGS["notifications"],
|
||||
PROFILE_SETTINGS["security"],
|
||||
PROFILE_SETTINGS["activity"],
|
||||
],
|
||||
[PROFILE_SETTINGS_CATEGORY.DEVELOPER]: [PROFILE_SETTINGS["api-tokens"]],
|
||||
};
|
||||
@@ -114,13 +114,6 @@ export const WORKSPACE_SETTINGS = {
|
||||
access: [EUserWorkspaceRoles.ADMIN],
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/webhooks/`,
|
||||
},
|
||||
"api-tokens": {
|
||||
key: "api-tokens",
|
||||
i18n_label: "workspace_settings.settings.api_tokens.title",
|
||||
href: `/settings/api-tokens`,
|
||||
access: [EUserWorkspaceRoles.ADMIN],
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/api-tokens/`,
|
||||
},
|
||||
};
|
||||
|
||||
export const WORKSPACE_SETTINGS_ACCESS = Object.fromEntries(
|
||||
@@ -139,7 +132,6 @@ export const WORKSPACE_SETTINGS_LINKS: {
|
||||
WORKSPACE_SETTINGS["billing-and-plans"],
|
||||
WORKSPACE_SETTINGS["export"],
|
||||
WORKSPACE_SETTINGS["webhooks"],
|
||||
WORKSPACE_SETTINGS["api-tokens"],
|
||||
];
|
||||
|
||||
export const ROLE = {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { LinkViewContainer } from "@/components/editors/link-view-container";
|
||||
|
||||
export const LinkContainer = ({
|
||||
editor,
|
||||
containerRef,
|
||||
}: {
|
||||
editor: Editor;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
}) => (
|
||||
<>
|
||||
<LinkViewContainer editor={editor} containerRef={containerRef} />
|
||||
</>
|
||||
);
|
||||
@@ -91,6 +91,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
|
||||
editorContainerClassName={cn(editorContainerClassNames, "document-editor")}
|
||||
id={id}
|
||||
tabIndex={tabIndex}
|
||||
disabledExtensions={disabledExtensions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Editor } from "@tiptap/react";
|
||||
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
|
||||
import { AIFeaturesMenu, BlockMenu, EditorBubbleMenu } from "@/components/menus";
|
||||
// types
|
||||
import { TAIHandler, TDisplayConfig } from "@/types";
|
||||
import { TAIHandler, TDisplayConfig, TExtensions } from "@/types";
|
||||
|
||||
type IPageRenderer = {
|
||||
aiHandler?: TAIHandler;
|
||||
@@ -13,10 +13,20 @@ type IPageRenderer = {
|
||||
editorContainerClassName: string;
|
||||
id: string;
|
||||
tabIndex?: number;
|
||||
disabledExtensions: TExtensions[];
|
||||
};
|
||||
|
||||
export const PageRenderer = (props: IPageRenderer) => {
|
||||
const { aiHandler, bubbleMenuEnabled, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
|
||||
const {
|
||||
aiHandler,
|
||||
bubbleMenuEnabled,
|
||||
displayConfig,
|
||||
editor,
|
||||
editorContainerClassName,
|
||||
id,
|
||||
tabIndex,
|
||||
disabledExtensions,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="frame-renderer flex-grow w-full">
|
||||
@@ -30,7 +40,7 @@ export const PageRenderer = (props: IPageRenderer) => {
|
||||
{editor.isEditable && (
|
||||
<div>
|
||||
{bubbleMenuEnabled && <EditorBubbleMenu editor={editor} />}
|
||||
<BlockMenu editor={editor} />
|
||||
<BlockMenu editor={editor} disabledExtensions={disabledExtensions} />
|
||||
<AIFeaturesMenu menu={aiHandler?.menu} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -83,6 +83,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
||||
editor={editor}
|
||||
editorContainerClassName={cn(editorContainerClassName, "document-editor")}
|
||||
id={id}
|
||||
disabledExtensions={disabledExtensions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
import { TDisplayConfig } from "@/types";
|
||||
// components
|
||||
import { LinkViewContainer } from "./link-view-container";
|
||||
import { LinkContainer } from "@/plane-editor/components/link-container";
|
||||
|
||||
interface EditorContainerProps {
|
||||
children: ReactNode;
|
||||
@@ -96,7 +97,7 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<LinkViewContainer editor={editor} containerRef={containerRef} />
|
||||
<LinkContainer editor={editor} containerRef={containerRef} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Editor, useEditorState } from "@tiptap/react";
|
||||
import { FC, useCallback, useEffect, useState } from "react";
|
||||
// components
|
||||
import { LinkView, LinkViewProps } from "@/components/links";
|
||||
import { getExtensionStorage } from "@/helpers/get-extension-storage";
|
||||
|
||||
interface LinkViewContainerProps {
|
||||
editor: Editor;
|
||||
@@ -17,7 +18,7 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
selector: ({ editor }: { editor: Editor }) => ({
|
||||
linkExtensionStorage: editor.storage.link,
|
||||
linkExtensionStorage: getExtensionStorage(editor, "link"),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -109,7 +110,7 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
||||
|
||||
// Close link view when bubble menu opens
|
||||
useEffect(() => {
|
||||
if (editorState.linkExtensionStorage.isBubbleMenuOpen && isOpen) {
|
||||
if (editorState.linkExtensionStorage?.isBubbleMenuOpen && isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [editorState.linkExtensionStorage, isOpen]);
|
||||
|
||||
@@ -4,9 +4,11 @@ import { useCallback, useEffect, useRef } from "react";
|
||||
import tippy, { Instance } from "tippy.js";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
import { TExtensions } from "@/types";
|
||||
|
||||
interface BlockMenuProps {
|
||||
editor: Editor;
|
||||
disabledExtensions?: TExtensions[];
|
||||
}
|
||||
|
||||
export const BlockMenu = (props: BlockMenuProps) => {
|
||||
|
||||
@@ -86,6 +86,10 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
[editor]
|
||||
);
|
||||
|
||||
const handleInvalidFile = useCallback((_error: EFileError, _file: File, message: string) => {
|
||||
alert(message);
|
||||
}, []);
|
||||
|
||||
// hooks
|
||||
const { isUploading: isImageBeingUploaded, uploadFile } = useUploader({
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
@@ -94,18 +98,12 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
handleProgressStatus,
|
||||
loadFileFromFileSystem: loadImageFromFileSystem,
|
||||
maxFileSize,
|
||||
onInvalidFile: handleInvalidFile,
|
||||
onUpload,
|
||||
});
|
||||
|
||||
const handleInvalidFile = useCallback((_error: EFileError, message: string) => {
|
||||
alert(message);
|
||||
}, []);
|
||||
|
||||
const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
editor,
|
||||
maxFileSize,
|
||||
onInvalidFile: handleInvalidFile,
|
||||
pos: getPos(),
|
||||
type: "image",
|
||||
uploader: uploadFile,
|
||||
@@ -140,11 +138,8 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
return;
|
||||
}
|
||||
await uploadFirstFileAndInsertRemaining({
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
editor,
|
||||
filesList,
|
||||
maxFileSize,
|
||||
onInvalidFile: (_error, message) => alert(message),
|
||||
pos: getPos(),
|
||||
type: "image",
|
||||
uploader: uploadFile,
|
||||
|
||||
@@ -79,6 +79,7 @@ declare module "@tiptap/core" {
|
||||
export type CustomLinkStorage = {
|
||||
isPreviewOpen: boolean;
|
||||
posToInsert: { from: number; to: number };
|
||||
isBubbleMenuOpen: boolean;
|
||||
};
|
||||
|
||||
export const CustomLinkExtension = Mark.create<LinkOptions, CustomLinkStorage>({
|
||||
|
||||
@@ -81,6 +81,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
immediatelyRender: false,
|
||||
shouldRerenderOnTransaction: false,
|
||||
autofocus,
|
||||
parseOptions: { preserveWhitespace: true },
|
||||
editorProps: {
|
||||
...CoreEditorProps({
|
||||
editorClassName,
|
||||
@@ -119,7 +120,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
const isUploadInProgress = getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress;
|
||||
if (!editor.isDestroyed && !isUploadInProgress) {
|
||||
try {
|
||||
editor.commands.setContent(value, false, { preserveWhitespace: "full" });
|
||||
editor.commands.setContent(value, false, { preserveWhitespace: true });
|
||||
if (editor.state.selection) {
|
||||
const docLength = editor.state.doc.content.size;
|
||||
const relativePosition = Math.min(editor.state.selection.from, docLength - 1);
|
||||
@@ -153,7 +154,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run();
|
||||
},
|
||||
setEditorValue: (content: string, emitUpdate = false) => {
|
||||
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" });
|
||||
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true });
|
||||
},
|
||||
setEditorValueAtCursorPosition: (content: string) => {
|
||||
if (editor?.state.selection) {
|
||||
|
||||
@@ -9,11 +9,11 @@ import { TEditorCommands } from "@/types";
|
||||
|
||||
type TUploaderArgs = {
|
||||
acceptedMimeTypes: string[];
|
||||
editorCommand: (file: File) => Promise<string>;
|
||||
editorCommand: (file: File) => Promise<string | undefined>;
|
||||
handleProgressStatus?: (isUploading: boolean) => void;
|
||||
loadFileFromFileSystem?: (file: string) => void;
|
||||
maxFileSize: number;
|
||||
onInvalidFile: (error: EFileError, message: string) => void;
|
||||
onInvalidFile: (error: EFileError, file: File, message: string) => void;
|
||||
onUpload: (url: string, file: File) => void;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const useUploader = (args: TUploaderArgs) => {
|
||||
acceptedMimeTypes,
|
||||
file,
|
||||
maxFileSize,
|
||||
onError: onInvalidFile,
|
||||
onError: (error, message) => onInvalidFile(error, file, message),
|
||||
});
|
||||
if (!isValid) {
|
||||
handleProgressStatus?.(false);
|
||||
@@ -60,7 +60,7 @@ export const useUploader = (args: TUploaderArgs) => {
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
const url: string = await editorCommand(file);
|
||||
const url = await editorCommand(file);
|
||||
|
||||
if (!url) {
|
||||
throw new Error("Something went wrong while uploading the file.");
|
||||
@@ -89,17 +89,14 @@ export const useUploader = (args: TUploaderArgs) => {
|
||||
};
|
||||
|
||||
type TDropzoneArgs = {
|
||||
acceptedMimeTypes: string[];
|
||||
editor: Editor;
|
||||
maxFileSize: number;
|
||||
onInvalidFile: (error: EFileError, message: string) => void;
|
||||
pos: number;
|
||||
type: Extract<TEditorCommands, "attachment" | "image">;
|
||||
uploader: (file: File) => Promise<void>;
|
||||
};
|
||||
|
||||
export const useDropZone = (args: TDropzoneArgs) => {
|
||||
const { acceptedMimeTypes, editor, maxFileSize, onInvalidFile, pos, type, uploader } = args;
|
||||
const { editor, pos, type, uploader } = args;
|
||||
// states
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [draggedInside, setDraggedInside] = useState<boolean>(false);
|
||||
@@ -126,22 +123,21 @@ export const useDropZone = (args: TDropzoneArgs) => {
|
||||
async (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setDraggedInside(false);
|
||||
if (e.dataTransfer.files.length === 0 || !editor.isEditable) {
|
||||
const filesList = e.dataTransfer.files;
|
||||
|
||||
if (filesList.length === 0 || !editor.isEditable) {
|
||||
return;
|
||||
}
|
||||
const filesList = e.dataTransfer.files;
|
||||
|
||||
await uploadFirstFileAndInsertRemaining({
|
||||
acceptedMimeTypes,
|
||||
editor,
|
||||
filesList,
|
||||
maxFileSize,
|
||||
onInvalidFile,
|
||||
pos,
|
||||
type,
|
||||
uploader,
|
||||
});
|
||||
},
|
||||
[acceptedMimeTypes, editor, maxFileSize, onInvalidFile, pos, type, uploader]
|
||||
[editor, pos, type, uploader]
|
||||
);
|
||||
const onDragEnter = useCallback(() => setDraggedInside(true), []);
|
||||
const onDragLeave = useCallback(() => setDraggedInside(false), []);
|
||||
@@ -156,11 +152,8 @@ export const useDropZone = (args: TDropzoneArgs) => {
|
||||
};
|
||||
|
||||
type TMultipleFileArgs = {
|
||||
acceptedMimeTypes: string[];
|
||||
editor: Editor;
|
||||
filesList: FileList;
|
||||
maxFileSize: number;
|
||||
onInvalidFile: (error: EFileError, message: string) => void;
|
||||
pos: number;
|
||||
type: Extract<TEditorCommands, "attachment" | "image">;
|
||||
uploader: (file: File) => Promise<void>;
|
||||
@@ -168,35 +161,18 @@ type TMultipleFileArgs = {
|
||||
|
||||
// Upload the first file and insert the remaining ones for uploading multiple files
|
||||
export const uploadFirstFileAndInsertRemaining = async (args: TMultipleFileArgs) => {
|
||||
const { acceptedMimeTypes, editor, filesList, maxFileSize, onInvalidFile, pos, type, uploader } = args;
|
||||
const filteredFiles: File[] = [];
|
||||
for (let i = 0; i < filesList.length; i += 1) {
|
||||
const file = filesList.item(i);
|
||||
if (
|
||||
file &&
|
||||
isFileValid({
|
||||
acceptedMimeTypes,
|
||||
file,
|
||||
maxFileSize,
|
||||
onError: onInvalidFile,
|
||||
})
|
||||
) {
|
||||
filteredFiles.push(file);
|
||||
}
|
||||
}
|
||||
if (filteredFiles.length !== filesList.length) {
|
||||
console.warn("Some files were invalid and have been ignored.");
|
||||
}
|
||||
if (filteredFiles.length === 0) {
|
||||
const { editor, filesList, pos, type, uploader } = args;
|
||||
const filesArray = Array.from(filesList);
|
||||
if (filesArray.length === 0) {
|
||||
console.error("No files found to upload.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload the first file
|
||||
const firstFile = filteredFiles[0];
|
||||
const firstFile = filesArray[0];
|
||||
uploader(firstFile);
|
||||
// Insert the remaining files
|
||||
const remainingFiles = filteredFiles.slice(1);
|
||||
const remainingFiles = filesArray.slice(1);
|
||||
if (remainingFiles.length > 0) {
|
||||
const docSize = editor.state.doc.content.size;
|
||||
const posOfNextFileToBeInserted = Math.min(pos + 1, docSize);
|
||||
|
||||
@@ -46,6 +46,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||
immediatelyRender: true,
|
||||
shouldRerenderOnTransaction: false,
|
||||
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
|
||||
parseOptions: { preserveWhitespace: true },
|
||||
editorProps: {
|
||||
...CoreReadOnlyEditorProps({
|
||||
editorClassName,
|
||||
@@ -71,7 +72,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||
// for syncing swr data on tab refocus etc
|
||||
useEffect(() => {
|
||||
if (initialValue === null || initialValue === undefined) return;
|
||||
if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: "full" });
|
||||
if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: true });
|
||||
}, [editor, initialValue]);
|
||||
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
@@ -79,7 +80,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||
editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run();
|
||||
},
|
||||
setEditorValue: (content: string, emitUpdate = false) => {
|
||||
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" });
|
||||
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true });
|
||||
},
|
||||
getMarkDown: (): string => {
|
||||
const markdownOutput = editor?.storage.markdown.getMarkdown();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
|
||||
// constants
|
||||
import { CORE_EDITOR_META } from "@/constants/meta";
|
||||
// plane editor imports
|
||||
import { NODE_FILE_MAP } from "@/plane-editor/constants/utility";
|
||||
// types
|
||||
@@ -32,7 +34,7 @@ export const TrackFileDeletionPlugin = (editor: Editor, deleteHandler: TFileHand
|
||||
|
||||
transactions.forEach((transaction) => {
|
||||
// if the transaction has meta of skipFileDeletion set to true, then return (like while clearing the editor content programmatically)
|
||||
if (transaction.getMeta("skipFileDeletion")) return;
|
||||
if (transaction.getMeta(CORE_EDITOR_META.SKIP_FILE_DELETION)) return;
|
||||
|
||||
const removedFiles: TFileNode[] = [];
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type TReadOnlyFileHandler = {
|
||||
checkIfAssetExists: (assetId: string) => Promise<boolean>;
|
||||
getAssetSrc: (path: string) => Promise<string>;
|
||||
restore: (assetSrc: string) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Sbalit postranní panel",
|
||||
"expand_sidebar": "Rozbalit postranní panel",
|
||||
"edition_badge": "Otevřít modal placených plánů"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Vymazat e-mail",
|
||||
"show_password": "Zobrazit heslo",
|
||||
"hide_password": "Skrýt heslo",
|
||||
"close_alert": "Zavřít upozornění",
|
||||
"close_popover": "Zavřít vyskakovací okno"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -848,6 +848,7 @@
|
||||
"live": "Živě",
|
||||
"change_history": "Historie změn",
|
||||
"coming_soon": "Již brzy",
|
||||
"member": "Člen",
|
||||
"members": "Členové",
|
||||
"you": "Vy",
|
||||
"upgrade_cta": {
|
||||
@@ -1080,7 +1081,9 @@
|
||||
"select": {
|
||||
"error": "Vyberte alespoň jednu pracovní položku",
|
||||
"empty": "Nevybrány žádné pracovní položky",
|
||||
"add_selected": "Přidat vybrané pracovní položky"
|
||||
"add_selected": "Přidat vybrané pracovní položky",
|
||||
"select_all": "Vybrat vše",
|
||||
"deselect_all": "Zrušit výběr všeho"
|
||||
},
|
||||
"open_in_full_screen": "Otevřít pracovní položku na celou obrazovku"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Seitenleiste einklappen",
|
||||
"expand_sidebar": "Seitenleiste ausklappen",
|
||||
"edition_badge": "Modal für kostenpflichtige Pläne öffnen"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "E-Mail löschen",
|
||||
"show_password": "Passwort anzeigen",
|
||||
"hide_password": "Passwort verbergen",
|
||||
"close_alert": "Warnung schließen",
|
||||
"close_popover": "Popover schließen"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -848,6 +848,7 @@
|
||||
"live": "Live",
|
||||
"change_history": "Änderungsverlauf",
|
||||
"coming_soon": "Demnächst verfügbar",
|
||||
"member": "Mitglied",
|
||||
"members": "Mitglieder",
|
||||
"you": "Sie",
|
||||
"upgrade_cta": {
|
||||
@@ -1080,7 +1081,9 @@
|
||||
"select": {
|
||||
"error": "Wählen Sie mindestens ein Arbeitselement aus",
|
||||
"empty": "Keine Arbeitselemente ausgewählt",
|
||||
"add_selected": "Ausgewählte Arbeitselemente hinzufügen"
|
||||
"add_selected": "Ausgewählte Arbeitselemente hinzufügen",
|
||||
"select_all": "Alle auswählen",
|
||||
"deselect_all": "Alle abwählen"
|
||||
},
|
||||
"open_in_full_screen": "Arbeitselement im Vollbild öffnen"
|
||||
},
|
||||
@@ -2459,4 +2462,4 @@
|
||||
"previously_edited_by": "Zuvor bearbeitet von",
|
||||
"edited_by": "Bearbeitet von"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Collapse sidebar",
|
||||
"expand_sidebar": "Expand sidebar",
|
||||
"edition_badge": "Open paid plans' modal"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Clear email",
|
||||
"show_password": "Show password",
|
||||
"hide_password": "Hide password",
|
||||
"close_alert": "Close alert",
|
||||
"close_popover": "Close popover"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
"your_account": "Your account",
|
||||
"security": "Security",
|
||||
"activity": "Activity",
|
||||
"appearance": "Appearance",
|
||||
"preferences": "Preferences",
|
||||
"language_and_time": "Language & Time",
|
||||
"notifications": "Notifications",
|
||||
"workspaces": "Workspaces",
|
||||
"create_workspace": "Create workspace",
|
||||
@@ -56,6 +57,10 @@
|
||||
"something_went_wrong_please_try_again": "Something went wrong. Please try again.",
|
||||
"load_more": "Load more",
|
||||
"select_or_customize_your_interface_color_scheme": "Select or customize your interface color scheme.",
|
||||
"timezone_setting": "Current timezone setting.",
|
||||
"language_setting": "Choose the language used in the user interface.",
|
||||
"settings_moved_to_preferences": "Timezone & Language settings have been moved to preferences.",
|
||||
"go_to_preferences": "Go to preferences",
|
||||
"theme": "Theme",
|
||||
"system_preference": "System preference",
|
||||
"light": "Light",
|
||||
@@ -334,6 +339,8 @@
|
||||
"new_password_must_be_different_from_old_password": "New password must be different from old password",
|
||||
"edited": "edited",
|
||||
"bot": "Bot",
|
||||
"settings_description": "Manage your account, workspace, and project preferences all in one place. Switch between tabs to easily configure.",
|
||||
"back_to_workspace": "Back to workspace",
|
||||
"project_view": {
|
||||
"sort_by": {
|
||||
"created_at": "Created at",
|
||||
@@ -683,6 +690,7 @@
|
||||
"live": "Live",
|
||||
"change_history": "Change History",
|
||||
"coming_soon": "Coming soon",
|
||||
"member": "Member",
|
||||
"members": "Members",
|
||||
"you": "You",
|
||||
"upgrade_cta": {
|
||||
@@ -916,7 +924,9 @@
|
||||
"select": {
|
||||
"error": "Please select at least one work item",
|
||||
"empty": "No work items selected",
|
||||
"add_selected": "Add selected work items"
|
||||
"add_selected": "Add selected work items",
|
||||
"select_all": "Select all",
|
||||
"deselect_all": "Deselect all"
|
||||
},
|
||||
"open_in_full_screen": "Open work item in full screen"
|
||||
},
|
||||
@@ -1301,6 +1311,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"account_settings": {
|
||||
"profile":{},
|
||||
"preferences":{
|
||||
"heading": "Preferences",
|
||||
"description": "Customize your app experience the way you work"
|
||||
},
|
||||
"notifications":{
|
||||
"heading": "Email notifications",
|
||||
"description": "Stay in the loop on Work items you are subscribed to. Enable this to get notified."
|
||||
},
|
||||
"security":{
|
||||
"heading": "Security"
|
||||
},
|
||||
"api_tokens":{
|
||||
"heading": "Personal Access Tokens",
|
||||
"description": "Generate secure API tokens to integrate your data with external systems and applications."
|
||||
},
|
||||
"activity":{
|
||||
"heading": "Activity",
|
||||
"description": "Track your recent actions and changes across all projects and work items."
|
||||
}
|
||||
},
|
||||
"workspace_settings": {
|
||||
"label": "Workspace settings",
|
||||
"page_label": "{workspace} - General settings",
|
||||
@@ -1367,16 +1399,22 @@
|
||||
}
|
||||
},
|
||||
"billing_and_plans": {
|
||||
"heading": "Billing & Plans",
|
||||
"description":"Choose your plan, manage subscriptions, and easily upgrade as your needs grow.",
|
||||
"title": "Billing & Plans",
|
||||
"current_plan": "Current plan",
|
||||
"free_plan": "You are currently using the free plan",
|
||||
"view_plans": "View plans"
|
||||
},
|
||||
"exports": {
|
||||
"heading": "Exports",
|
||||
"description": "Export your project data in various formats and access your export history with download links.",
|
||||
"title": "Exports",
|
||||
"exporting": "Exporting",
|
||||
"previous_exports": "Previous exports",
|
||||
"export_separate_files": "Export the data into separate files",
|
||||
"exporting_projects": "Exporting project",
|
||||
"format": "Format",
|
||||
"modal": {
|
||||
"title": "Export to",
|
||||
"toasts": {
|
||||
@@ -1392,6 +1430,8 @@
|
||||
}
|
||||
},
|
||||
"webhooks": {
|
||||
"heading": "Webhooks",
|
||||
"description": "Automate notifications to external services when project events occur.",
|
||||
"title": "Webhooks",
|
||||
"add_webhook": "Add webhook",
|
||||
"modal": {
|
||||
@@ -1443,29 +1483,29 @@
|
||||
}
|
||||
},
|
||||
"api_tokens": {
|
||||
"title": "API Tokens",
|
||||
"add_token": "Add API token",
|
||||
"title": "Personal Access Tokens",
|
||||
"add_token": "Add personal access token",
|
||||
"create_token": "Create token",
|
||||
"never_expires": "Never expires",
|
||||
"generate_token": "Generate token",
|
||||
"generating": "Generating",
|
||||
"delete": {
|
||||
"title": "Delete API token",
|
||||
"title": "Delete personal access token",
|
||||
"description": "Any application using this token will no longer have the access to Plane data. This action cannot be undone.",
|
||||
"success": {
|
||||
"title": "Success!",
|
||||
"message": "The API token has been successfully deleted"
|
||||
"message": "The token has been successfully deleted"
|
||||
},
|
||||
"error": {
|
||||
"title": "Error!",
|
||||
"message": "The API token could not be deleted"
|
||||
"message": "The token could not be deleted"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
"api_tokens": {
|
||||
"title": "No API tokens created",
|
||||
"title": "No personal access tokens created",
|
||||
"description": "Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started."
|
||||
},
|
||||
"webhooks": {
|
||||
@@ -1515,8 +1555,9 @@
|
||||
"profile": "Profile",
|
||||
"security": "Security",
|
||||
"activity": "Activity",
|
||||
"appearance": "Appearance",
|
||||
"notifications": "Notifications"
|
||||
"preferences": "Preferences",
|
||||
"notifications": "Notifications",
|
||||
"api-tokens": "Personal Access Tokens"
|
||||
},
|
||||
"tabs": {
|
||||
"summary": "Summary",
|
||||
@@ -1578,6 +1619,8 @@
|
||||
}
|
||||
},
|
||||
"states": {
|
||||
"heading": "States",
|
||||
"description": "Define and customize workflow states to track the progress of your work items.",
|
||||
"describe_this_state_for_your_members": "Describe this state for your members.",
|
||||
"empty_state": {
|
||||
"title": "No states available for the {groupKey} group",
|
||||
@@ -1585,6 +1628,8 @@
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"heading": "Labels",
|
||||
"description": "Create custom labels to categorize and organize your work items",
|
||||
"label_title": "Label title",
|
||||
"label_title_is_required": "Label title is required",
|
||||
"label_max_char": "Label name should not exceed 255 characters",
|
||||
@@ -1593,9 +1638,11 @@
|
||||
}
|
||||
},
|
||||
"estimates": {
|
||||
"heading": "Estimates",
|
||||
"description": "Set up estimation systems to track and communicate the effort required for each work item.",
|
||||
"label": "Estimates",
|
||||
"title": "Enable estimates for my project",
|
||||
"description": "They help you in communicating complexity and workload of the team.",
|
||||
"enable_description": "They help you in communicating complexity and workload of the team.",
|
||||
"no_estimate": "No estimate",
|
||||
"new": "New estimate system",
|
||||
"create": {
|
||||
@@ -1677,6 +1724,8 @@
|
||||
},
|
||||
"automations": {
|
||||
"label": "Automations",
|
||||
"heading": "Automations",
|
||||
"description": "Configure automated actions to streamline your project management workflow and reduce manual tasks.",
|
||||
"auto-archive": {
|
||||
"title": "Auto-archive closed work items",
|
||||
"description": "Plane will auto archive work items that have been completed or canceled.",
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Colapsar barra lateral",
|
||||
"expand_sidebar": "Expandir barra lateral",
|
||||
"edition_badge": "Abrir modal de planes de pago"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Limpiar correo electrónico",
|
||||
"show_password": "Mostrar contraseña",
|
||||
"hide_password": "Ocultar contraseña",
|
||||
"close_alert": "Cerrar alerta",
|
||||
"close_popover": "Cerrar ventana emergente"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,6 +851,7 @@
|
||||
"live": "En vivo",
|
||||
"change_history": "Historial de cambios",
|
||||
"coming_soon": "Próximamente",
|
||||
"member": "Miembro",
|
||||
"members": "Miembros",
|
||||
"you": "Tú",
|
||||
"upgrade_cta": {
|
||||
@@ -1083,7 +1084,9 @@
|
||||
"select": {
|
||||
"error": "Por favor selecciona al menos un elemento de trabajo",
|
||||
"empty": "No hay elementos de trabajo seleccionados",
|
||||
"add_selected": "Agregar elementos seleccionados"
|
||||
"add_selected": "Agregar elementos seleccionados",
|
||||
"select_all": "Seleccionar todo",
|
||||
"deselect_all": "Deseleccionar todo"
|
||||
},
|
||||
"open_in_full_screen": "Abrir elemento de trabajo en pantalla completa"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Réduire la barre latérale",
|
||||
"expand_sidebar": "Étendre la barre latérale",
|
||||
"edition_badge": "Ouvrir le modal des plans payants"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Effacer l'e-mail",
|
||||
"show_password": "Afficher le mot de passe",
|
||||
"hide_password": "Masquer le mot de passe",
|
||||
"close_alert": "Fermer l'alerte",
|
||||
"close_popover": "Fermer la fenêtre contextuelle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -849,6 +849,7 @@
|
||||
"live": "En direct",
|
||||
"change_history": "Historique des modifications",
|
||||
"coming_soon": "À venir",
|
||||
"member": "Membre",
|
||||
"members": "Membres",
|
||||
"you": "Vous",
|
||||
"upgrade_cta": {
|
||||
@@ -1081,7 +1082,9 @@
|
||||
"select": {
|
||||
"error": "Veuillez sélectionner au moins un élément de travail",
|
||||
"empty": "Aucun élément de travail sélectionné",
|
||||
"add_selected": "Ajouter les éléments de travail sélectionnés"
|
||||
"add_selected": "Ajouter les éléments de travail sélectionnés",
|
||||
"select_all": "Sélectionner tout",
|
||||
"deselect_all": "Tout désélectionner"
|
||||
},
|
||||
"open_in_full_screen": "Ouvrir l'élément de travail en plein écran"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Tutup sidebar",
|
||||
"expand_sidebar": "Perluas sidebar",
|
||||
"edition_badge": "Buka modal paket berbayar"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Hapus email",
|
||||
"show_password": "Tampilkan kata sandi",
|
||||
"hide_password": "Sembunyikan kata sandi",
|
||||
"close_alert": "Tutup peringatan",
|
||||
"close_popover": "Tutup popover"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -848,6 +848,7 @@
|
||||
"live": "Langsung",
|
||||
"change_history": "Riwayat Perubahan",
|
||||
"coming_soon": "Segera hadir",
|
||||
"member": "Anggota",
|
||||
"members": "Anggota",
|
||||
"you": "Anda",
|
||||
"upgrade_cta": {
|
||||
@@ -1080,7 +1081,9 @@
|
||||
"select": {
|
||||
"error": "Silakan pilih setidaknya satu item kerja",
|
||||
"empty": "Tidak ada item kerja yang dipilih",
|
||||
"add_selected": "Tambah item kerja yang dipilih"
|
||||
"add_selected": "Tambah item kerja yang dipilih",
|
||||
"select_all": "Pilih semua item kerja",
|
||||
"deselect_all": "Batalkan pilihan semua item kerja"
|
||||
},
|
||||
"open_in_full_screen": "Buka item kerja dalam layar penuh"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Comprimi barra laterale",
|
||||
"expand_sidebar": "Espandi barra laterale",
|
||||
"edition_badge": "Apri modal piani a pagamento"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Cancella email",
|
||||
"show_password": "Mostra password",
|
||||
"hide_password": "Nascondi password",
|
||||
"close_alert": "Chiudi avviso",
|
||||
"close_popover": "Chiudi popover"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,6 +847,7 @@
|
||||
"live": "Live",
|
||||
"change_history": "Cronologia modifiche",
|
||||
"coming_soon": "Prossimamente",
|
||||
"member": "Membro",
|
||||
"members": "Membri",
|
||||
"you": "Tu",
|
||||
"upgrade_cta": {
|
||||
@@ -1079,7 +1080,9 @@
|
||||
"select": {
|
||||
"error": "Seleziona almeno un elemento di lavoro",
|
||||
"empty": "Nessun elemento di lavoro selezionato",
|
||||
"add_selected": "Aggiungi gli elementi di lavoro selezionati"
|
||||
"add_selected": "Aggiungi gli elementi di lavoro selezionati",
|
||||
"select_all": "Seleziona tutto",
|
||||
"deselect_all": "Deseleziona tutto"
|
||||
},
|
||||
"open_in_full_screen": "Apri l'elemento di lavoro a schermo intero"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "サイドバーを折りたたむ",
|
||||
"expand_sidebar": "サイドバーを展開",
|
||||
"edition_badge": "有料プランのモーダルを開く"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "メールをクリア",
|
||||
"show_password": "パスワードを表示",
|
||||
"hide_password": "パスワードを非表示",
|
||||
"close_alert": "アラートを閉じる",
|
||||
"close_popover": "ポップオーバーを閉じる"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -849,6 +849,7 @@
|
||||
"live": "ライブ",
|
||||
"change_history": "変更履歴",
|
||||
"coming_soon": "近日公開",
|
||||
"member": "メンバー",
|
||||
"members": "メンバー",
|
||||
"you": "あなた",
|
||||
"upgrade_cta": {
|
||||
@@ -1081,7 +1082,9 @@
|
||||
"select": {
|
||||
"error": "少なくとも1つの作業項目を選択してください",
|
||||
"empty": "作業項目が選択されていません",
|
||||
"add_selected": "選択した作業項目を追加"
|
||||
"add_selected": "選択した作業項目を追加",
|
||||
"select_all": "すべて選択",
|
||||
"deselect_all": "すべての選択を解除"
|
||||
},
|
||||
"open_in_full_screen": "作業項目をフルスクリーンで開く"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "사이드바 축소",
|
||||
"expand_sidebar": "사이드바 확장",
|
||||
"edition_badge": "유료 플랜 모달 열기"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "이메일 지우기",
|
||||
"show_password": "비밀번호 표시",
|
||||
"hide_password": "비밀번호 숨기기",
|
||||
"close_alert": "알림 닫기",
|
||||
"close_popover": "팝오버 닫기"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"live": "라이브",
|
||||
"change_history": "변경 기록",
|
||||
"coming_soon": "곧 출시",
|
||||
"member": "멤버",
|
||||
"members": "멤버",
|
||||
"you": "나",
|
||||
"upgrade_cta": {
|
||||
@@ -1082,7 +1083,9 @@
|
||||
"select": {
|
||||
"error": "최소 하나의 작업 항목을 선택하세요",
|
||||
"empty": "선택된 작업 항목 없음",
|
||||
"add_selected": "선택된 작업 항목 추가"
|
||||
"add_selected": "선택된 작업 항목 추가",
|
||||
"select_all": "모두 선택",
|
||||
"deselect_all": "모두 선택 해제"
|
||||
},
|
||||
"open_in_full_screen": "작업 항목을 전체 화면으로 열기"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Zwiń pasek boczny",
|
||||
"expand_sidebar": "Rozwiń pasek boczny",
|
||||
"edition_badge": "Otwórz modal płatnych planów"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Wyczyść e-mail",
|
||||
"show_password": "Pokaż hasło",
|
||||
"hide_password": "Ukryj hasło",
|
||||
"close_alert": "Zamknij alert",
|
||||
"close_popover": "Zamknij popover"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"live": "Na żywo",
|
||||
"change_history": "Historia zmian",
|
||||
"coming_soon": "Wkrótce",
|
||||
"member": "Członek",
|
||||
"members": "Członkowie",
|
||||
"you": "Ty",
|
||||
"upgrade_cta": {
|
||||
@@ -1082,7 +1083,9 @@
|
||||
"select": {
|
||||
"error": "Wybierz co najmniej jeden element pracy",
|
||||
"empty": "Nie wybrano żadnych elementów pracy",
|
||||
"add_selected": "Dodaj wybrane elementy pracy"
|
||||
"add_selected": "Dodaj wybrane elementy pracy",
|
||||
"select_all": "Wybierz wszystko",
|
||||
"deselect_all": "Odznacz wszystko"
|
||||
},
|
||||
"open_in_full_screen": "Otwórz element pracy na pełnym ekranie"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Recolher barra lateral",
|
||||
"expand_sidebar": "Expandir barra lateral",
|
||||
"edition_badge": "Abrir modal de planos pagos"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Limpar e-mail",
|
||||
"show_password": "Mostrar senha",
|
||||
"hide_password": "Ocultar senha",
|
||||
"close_alert": "Fechar alerta",
|
||||
"close_popover": "Fechar popover"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"live": "Ao vivo",
|
||||
"change_history": "Histórico de alterações",
|
||||
"coming_soon": "Em breve",
|
||||
"member": "Membro",
|
||||
"members": "Membros",
|
||||
"you": "Você",
|
||||
"upgrade_cta": {
|
||||
@@ -1082,7 +1083,9 @@
|
||||
"select": {
|
||||
"error": "Selecione pelo menos um item de trabalho",
|
||||
"empty": "Nenhum item de trabalho selecionado",
|
||||
"add_selected": "Adicionar itens de trabalho selecionados"
|
||||
"add_selected": "Adicionar itens de trabalho selecionados",
|
||||
"select_all": "Selecionar tudo",
|
||||
"deselect_all": "Desmarcar tudo"
|
||||
},
|
||||
"open_in_full_screen": "Abrir item de trabalho em tela cheia"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Restrânge bara laterală",
|
||||
"expand_sidebar": "Extinde bara laterală",
|
||||
"edition_badge": "Deschide modalul planurilor plătite"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Șterge e-mailul",
|
||||
"show_password": "Afișează parola",
|
||||
"hide_password": "Ascunde parola",
|
||||
"close_alert": "Închide alerta",
|
||||
"close_popover": "Închide popover-ul"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -848,6 +848,7 @@
|
||||
"live": "În direct",
|
||||
"change_history": "Istoric modificări",
|
||||
"coming_soon": "În curând",
|
||||
"member": "Membru",
|
||||
"members": "Membri",
|
||||
"you": "Tu",
|
||||
"upgrade_cta": {
|
||||
@@ -1080,7 +1081,9 @@
|
||||
"select": {
|
||||
"error": "Selectează cel puțin o activitate",
|
||||
"empty": "Nicio activitate selectată",
|
||||
"add_selected": "Adaugă activitățile selectate"
|
||||
"add_selected": "Adaugă activitățile selectate",
|
||||
"select_all": "Selectează tot",
|
||||
"deselect_all": "Deselează tot"
|
||||
},
|
||||
"open_in_full_screen": "Deschide activitatea pe tot ecranul"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Свернуть боковую панель",
|
||||
"expand_sidebar": "Развернуть боковую панель",
|
||||
"edition_badge": "Открыть модал платных планов"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Очистить email",
|
||||
"show_password": "Показать пароль",
|
||||
"hide_password": "Скрыть пароль",
|
||||
"close_alert": "Закрыть уведомление",
|
||||
"close_popover": "Закрыть всплывающее окно"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"live": "В прямом эфире",
|
||||
"change_history": "История изменений",
|
||||
"coming_soon": "Скоро",
|
||||
"member": "Участник",
|
||||
"members": "Участники",
|
||||
"you": "Вы",
|
||||
"upgrade_cta": {
|
||||
@@ -1082,7 +1083,9 @@
|
||||
"select": {
|
||||
"error": "Выберите хотя бы один рабочий элемент",
|
||||
"empty": "Рабочие элементы не выбраны",
|
||||
"add_selected": "Добавить выбранные рабочие элементы"
|
||||
"add_selected": "Добавить выбранные рабочие элементы",
|
||||
"select_all": "Выбрать все",
|
||||
"deselect_all": "Снять выделение со всех"
|
||||
},
|
||||
"open_in_full_screen": "Открыть рабочий элемент в полном экране"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Zbaliť bočný panel",
|
||||
"expand_sidebar": "Rozbaliť bočný panel",
|
||||
"edition_badge": "Otvoriť modal platených plánov"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Vymazať e-mail",
|
||||
"show_password": "Zobraziť heslo",
|
||||
"hide_password": "Skryť heslo",
|
||||
"close_alert": "Zavrieť upozornenie",
|
||||
"close_popover": "Zavrieť vyskakovacie okno"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"live": "Živé",
|
||||
"change_history": "História zmien",
|
||||
"coming_soon": "Už čoskoro",
|
||||
"member": "Člen",
|
||||
"members": "Členovia",
|
||||
"you": "Vy",
|
||||
"upgrade_cta": {
|
||||
@@ -1082,7 +1083,9 @@
|
||||
"select": {
|
||||
"error": "Vyberte aspoň jednu pracovnú položku",
|
||||
"empty": "Nie sú vybrané žiadne pracovné položky",
|
||||
"add_selected": "Pridať vybrané pracovné položky"
|
||||
"add_selected": "Pridať vybrané pracovné položky",
|
||||
"select_all": "Vybrať všetko",
|
||||
"deselect_all": "Zrušiť výber všetkého"
|
||||
},
|
||||
"open_in_full_screen": "Otvoriť pracovnú položku na celú obrazovku"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Kenar çubuğunu daralt",
|
||||
"expand_sidebar": "Kenar çubuğunu genişlet",
|
||||
"edition_badge": "Ücretli planlar modalını aç"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "E-postayı temizle",
|
||||
"show_password": "Şifreyi göster",
|
||||
"hide_password": "Şifreyi gizle",
|
||||
"close_alert": "Uyarıyı kapat",
|
||||
"close_popover": "Açılır pencereyi kapat"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,6 +851,7 @@
|
||||
"live": "Canlı",
|
||||
"change_history": "Değişiklik Geçmişi",
|
||||
"coming_soon": "Çok Yakında",
|
||||
"member": "Üye",
|
||||
"members": "Üyeler",
|
||||
"you": "Siz",
|
||||
"upgrade_cta": {
|
||||
@@ -1083,7 +1084,9 @@
|
||||
"select": {
|
||||
"error": "Lütfen en az bir iş öğesi seçin",
|
||||
"empty": "Hiç iş öğesi seçilmedi",
|
||||
"add_selected": "Seçilen iş öğelerini ekle"
|
||||
"add_selected": "Seçilen iş öğelerini ekle",
|
||||
"select_all": "Tümünü seç",
|
||||
"deselect_all": "Tümünü seçme"
|
||||
},
|
||||
"open_in_full_screen": "İş öğesini tam ekranda aç"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Згорнути бічну панель",
|
||||
"expand_sidebar": "Розгорнути бічну панель",
|
||||
"edition_badge": "Відкрити модал платних планів"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Очистити email",
|
||||
"show_password": "Показати пароль",
|
||||
"hide_password": "Приховати пароль",
|
||||
"close_alert": "Закрити сповіщення",
|
||||
"close_popover": "Закрити спливаюче вікно"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"live": "Наживо",
|
||||
"change_history": "Історія змін",
|
||||
"coming_soon": "Незабаром",
|
||||
"member": "Учасник",
|
||||
"members": "Учасники",
|
||||
"you": "Ви",
|
||||
"upgrade_cta": {
|
||||
@@ -1082,7 +1083,9 @@
|
||||
"select": {
|
||||
"error": "Виберіть принаймні одну робочу одиницю",
|
||||
"empty": "Не вибрано жодної робочої одиниці",
|
||||
"add_selected": "Додати вибрані робочі одиниці"
|
||||
"add_selected": "Додати вибрані робочі одиниці",
|
||||
"select_all": "Вибрати всі",
|
||||
"deselect_all": "Скасувати вибір усіх"
|
||||
},
|
||||
"open_in_full_screen": "Відкрити робочу одиницю на повний екран"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "Thu gọn thanh bên",
|
||||
"expand_sidebar": "Mở rộng thanh bên",
|
||||
"edition_badge": "Mở modal gói trả phí"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Xóa email",
|
||||
"show_password": "Hiển thị mật khẩu",
|
||||
"hide_password": "Ẩn mật khẩu",
|
||||
"close_alert": "Đóng cảnh báo",
|
||||
"close_popover": "Đóng popover"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -849,6 +849,7 @@
|
||||
"live": "Trực tiếp",
|
||||
"change_history": "Lịch sử thay đổi",
|
||||
"coming_soon": "Sắp ra mắt",
|
||||
"member": "Thành viên",
|
||||
"members": "Thành viên",
|
||||
"you": "Bạn",
|
||||
"upgrade_cta": {
|
||||
@@ -1081,7 +1082,9 @@
|
||||
"select": {
|
||||
"error": "Vui lòng chọn ít nhất một mục công việc",
|
||||
"empty": "Chưa chọn mục công việc",
|
||||
"add_selected": "Thêm mục công việc đã chọn"
|
||||
"add_selected": "Thêm mục công việc đã chọn",
|
||||
"select_all": "Chọn tất cả",
|
||||
"deselect_all": "Bỏ chọn tất cả"
|
||||
},
|
||||
"open_in_full_screen": "Mở mục công việc trong chế độ toàn màn hình"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "折叠侧边栏",
|
||||
"expand_sidebar": "展开侧边栏",
|
||||
"edition_badge": "打开付费计划模态框"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "清除邮箱",
|
||||
"show_password": "显示密码",
|
||||
"hide_password": "隐藏密码",
|
||||
"close_alert": "关闭警告",
|
||||
"close_popover": "关闭弹出框"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -849,6 +849,7 @@
|
||||
"live": "实时",
|
||||
"change_history": "变更历史",
|
||||
"coming_soon": "即将推出",
|
||||
"member": "成员",
|
||||
"members": "成员",
|
||||
"you": "你",
|
||||
"upgrade_cta": {
|
||||
@@ -1081,7 +1082,9 @@
|
||||
"select": {
|
||||
"error": "请至少选择一个工作项",
|
||||
"empty": "未选择工作项",
|
||||
"add_selected": "添加所选工作项"
|
||||
"add_selected": "添加所选工作项",
|
||||
"select_all": "全选",
|
||||
"deselect_all": "取消全选"
|
||||
},
|
||||
"open_in_full_screen": "在全屏中打开工作项"
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"collapse_sidebar": "摺疊側邊欄",
|
||||
"expand_sidebar": "展開側邊欄",
|
||||
"edition_badge": "打開付費計劃模態框"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "清除電子郵件",
|
||||
"show_password": "顯示密碼",
|
||||
"hide_password": "隱藏密碼",
|
||||
"close_alert": "關閉警告",
|
||||
"close_popover": "關閉彈出框"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"live": "即時",
|
||||
"change_history": "變更歷史記錄",
|
||||
"coming_soon": "即將推出",
|
||||
"member": "成員",
|
||||
"members": "成員",
|
||||
"you": "您",
|
||||
"upgrade_cta": {
|
||||
@@ -1082,7 +1083,9 @@
|
||||
"select": {
|
||||
"error": "請至少選擇一個工作事項",
|
||||
"empty": "未選擇工作事項",
|
||||
"add_selected": "新增已選取的工作事項"
|
||||
"add_selected": "新增已選取的工作事項",
|
||||
"select_all": "全選",
|
||||
"deselect_all": "取消全選"
|
||||
},
|
||||
"open_in_full_screen": "以全螢幕開啟工作事項"
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"main": "tailwind.config.js",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.38",
|
||||
|
||||
@@ -469,6 +469,7 @@ module.exports = {
|
||||
plugins: [
|
||||
require("tailwindcss-animate"),
|
||||
require("@tailwindcss/typography"),
|
||||
require("@tailwindcss/container-queries"),
|
||||
function ({ addUtilities }) {
|
||||
const newUtilities = {
|
||||
// Mobile screens
|
||||
|
||||
+14
-33
@@ -1,14 +1,5 @@
|
||||
import { EUserProjectRoles } from "@plane/constants";
|
||||
import type {
|
||||
IProjectViewProps,
|
||||
IUser,
|
||||
IUserLite,
|
||||
IUserMemberLite,
|
||||
IWorkspace,
|
||||
IWorkspaceLite,
|
||||
TLogoProps,
|
||||
TStateGroups,
|
||||
} from "..";
|
||||
import type { IUser, IUserLite, IWorkspace, TLogoProps, TStateGroups } from "..";
|
||||
import { TUserPermissions } from "../enums";
|
||||
|
||||
export interface IPartialProject {
|
||||
@@ -91,30 +82,20 @@ export interface IProjectMemberLite {
|
||||
member_id: string;
|
||||
}
|
||||
|
||||
export interface IProjectMember {
|
||||
id: string;
|
||||
member: IUserMemberLite;
|
||||
project: IProjectLite;
|
||||
workspace: IWorkspaceLite;
|
||||
comment: string;
|
||||
role: TUserPermissions;
|
||||
|
||||
preferences: ProjectPreferences;
|
||||
|
||||
view_props: IProjectViewProps;
|
||||
default_props: IProjectViewProps;
|
||||
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
}
|
||||
|
||||
export interface IProjectMembership {
|
||||
id: string;
|
||||
export type TProjectMembership = {
|
||||
member: string;
|
||||
role: TUserPermissions;
|
||||
}
|
||||
role: TUserPermissions | EUserProjectRoles;
|
||||
created_at: string;
|
||||
} & (
|
||||
| {
|
||||
id: string;
|
||||
original_role: EUserProjectRoles;
|
||||
}
|
||||
| {
|
||||
id: null;
|
||||
original_role: null;
|
||||
}
|
||||
);
|
||||
|
||||
export interface IProjectBulkAddFormData {
|
||||
members: { role: TUserPermissions | EUserProjectRoles; member_id: string }[];
|
||||
|
||||
Vendored
+3
@@ -12,6 +12,7 @@ export interface IUserLite {
|
||||
id: string;
|
||||
is_bot: boolean;
|
||||
last_name: string;
|
||||
joining_date?: string;
|
||||
}
|
||||
export interface IUser extends IUserLite {
|
||||
// only for uploading the cover image
|
||||
@@ -78,6 +79,8 @@ export interface IUserSettings {
|
||||
workspace: {
|
||||
last_workspace_id: string | undefined;
|
||||
last_workspace_slug: string | undefined;
|
||||
last_workspace_name: string | undefined;
|
||||
last_workspace_logo: string | undefined;
|
||||
fallback_workspace_id: string | undefined;
|
||||
fallback_workspace_slug: string | undefined;
|
||||
invites: number | undefined;
|
||||
|
||||
Vendored
+2
-2
@@ -1,4 +1,4 @@
|
||||
import type { ICycle, IProjectMember, IUser, IUserLite, IWorkspaceViewProps, TPaginationInfo } from "@plane/types";
|
||||
import type { ICycle, TProjectMembership, IUser, IUserLite, IWorkspaceViewProps, TPaginationInfo } from "@plane/types";
|
||||
import { EUserWorkspaceRoles } from "@plane/constants"; // TODO: check if importing this over here causes circular dependency
|
||||
import { TUserPermissions } from "./enums";
|
||||
|
||||
@@ -93,7 +93,7 @@ export interface IWorkspaceMemberMe {
|
||||
|
||||
export interface ILastActiveWorkspaceDetails {
|
||||
workspace_details: IWorkspace;
|
||||
project_details?: IProjectMember[];
|
||||
project_details?: TProjectMembership[];
|
||||
}
|
||||
|
||||
export interface IWorkspaceDefaultSearchResult {
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
export * from "./array";
|
||||
export * from "./attachment";
|
||||
export * from "./auth";
|
||||
export * from "./datetime";
|
||||
export * from "./color";
|
||||
export * from "./common";
|
||||
export * from "./datetime";
|
||||
export * from "./emoji";
|
||||
export * from "./file";
|
||||
export * from "./get-icon-for-link";
|
||||
export * from "./issue";
|
||||
export * from "./permission";
|
||||
export * from "./state";
|
||||
export * from "./string";
|
||||
export * from "./theme";
|
||||
export * from "./workspace";
|
||||
export * from "./work-item";
|
||||
|
||||
export * from "./get-icon-for-link";
|
||||
|
||||
export * from "./subscription";
|
||||
export * from "./theme";
|
||||
export * from "./work-item";
|
||||
export * from "./workspace";
|
||||
@@ -0,0 +1,13 @@
|
||||
import { EUserPermissions, EUserProjectRoles, EUserWorkspaceRoles } from "@plane/constants";
|
||||
|
||||
type TSupportedRole = EUserPermissions | EUserProjectRoles | EUserWorkspaceRoles;
|
||||
|
||||
/**
|
||||
* @description Returns the highest role from an array of supported roles
|
||||
* @param { TSupportedRole[] } roles
|
||||
* @returns { TSupportedRole | undefined }
|
||||
*/
|
||||
export const getHighestRole = <T extends TSupportedRole>(roles: T[]): T | undefined => {
|
||||
if (!roles || roles.length === 0) return undefined;
|
||||
return roles.reduce((highest, current) => (current > highest ? current : highest));
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export const getReadOnlyEditorFileHandlers = (args: Pick<TArgs, "anchor" | "work
|
||||
const { anchor, workspaceId } = args;
|
||||
|
||||
return {
|
||||
checkIfAssetExists: async () => true,
|
||||
getAssetSrc: async (path) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"mobx-utils": "^6.0.8",
|
||||
"next": "^14.2.28",
|
||||
"next": "^14.2.29",
|
||||
"next-themes": "^0.2.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ const ProjectCyclesPage = observer(() => {
|
||||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.cycle.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
},
|
||||
disabled: !hasAdminLevelPermission,
|
||||
}}
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ const ProjectInboxPage = observer(() => {
|
||||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.inbox.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ const ProjectModulesPage = observer(() => {
|
||||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.module.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ const ProjectPagesPage = observer(() => {
|
||||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.page.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { FC, ReactNode } from "react";
|
||||
// components
|
||||
import { AppHeader } from "@/components/core";
|
||||
// local components
|
||||
import { ProjectSettingHeader } from "../header";
|
||||
import { ProjectSettingsSidebar } from "./sidebar";
|
||||
|
||||
export interface IProjectSettingLayout {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const ProjectSettingLayout: FC<IProjectSettingLayout> = (props) => {
|
||||
const { children } = props;
|
||||
return (
|
||||
<>
|
||||
<AppHeader header={<ProjectSettingHeader />} />
|
||||
<div className="inset-y-0 flex flex-row vertical-scrollbar scrollbar-lg h-full w-full overflow-y-auto">
|
||||
<div className="px-page-x !pr-0 py-page-y flex-shrink-0 overflow-y-hidden sm:hidden hidden md:block lg:block">
|
||||
<ProjectSettingsSidebar />
|
||||
</div>
|
||||
<div className="flex flex-col relative w-full overflow-hidden">
|
||||
<div className="h-full w-full overflow-x-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-page-x md:px-9 py-page-y">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectSettingLayout;
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectMemberList, ProjectSettingsMemberDefaults } from "@/components/project";
|
||||
// hooks
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
const MembersSettingsPage = observer(() => {
|
||||
// store
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined;
|
||||
const isProjectMemberOrAdmin = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const canPerformProjectMemberActions = isProjectMemberOrAdmin || isWorkspaceAdmin;
|
||||
|
||||
if (workspaceUserInfo && !canPerformProjectMemberActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className={`w-full overflow-y-auto`}>
|
||||
<ProjectSettingsMemberDefaults />
|
||||
<ProjectMemberList />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default MembersSettingsPage;
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import range from "lodash/range";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
// plane web constants
|
||||
import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project";
|
||||
|
||||
export const ProjectSettingsSidebar = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const pathname = usePathname();
|
||||
// mobx store
|
||||
const { allowPermissions, projectUserInfo } = useUserPermissions();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// derived values
|
||||
const currentProjectRole = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]?.role;
|
||||
|
||||
if (!currentProjectRole) {
|
||||
return (
|
||||
<div className="flex w-[280px] flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-semibold text-custom-sidebar-text-400">SETTINGS</span>
|
||||
<Loader className="flex w-full flex-col gap-2">
|
||||
{range(8).map((index) => (
|
||||
<Loader.Item key={index} height="34px" />
|
||||
))}
|
||||
</Loader>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-[280px] flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-semibold text-custom-sidebar-text-400">SETTINGS</span>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
{PROJECT_SETTINGS_LINKS.map(
|
||||
(link) =>
|
||||
allowPermissions(
|
||||
link.access,
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString()
|
||||
) && (
|
||||
<Link key={link.key} href={`/${workspaceSlug}/projects/${projectId}${link.href}`}>
|
||||
<SidebarNavItem
|
||||
key={link.key}
|
||||
isActive={link.highlight(pathname, `/${workspaceSlug}/projects/${projectId}`)}
|
||||
className="text-sm font-medium px-4 py-2"
|
||||
>
|
||||
{t(link.i18n_label)}
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { Settings } from "lucide-react";
|
||||
import { EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Breadcrumbs, CustomMenu, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// hooks
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project";
|
||||
|
||||
export const ProjectSettingHeader: FC = observer(() => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { loader } = useProject();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<div>
|
||||
<div className="z-50">
|
||||
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||
<ProjectBreadcrumb />
|
||||
<div className="hidden sm:hidden md:block lg:block">
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink label="Settings" icon={<Settings className="h-4 w-4 text-custom-text-300" />} />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
<CustomMenu
|
||||
className="flex-shrink-0 block sm:block md:hidden lg:hidden"
|
||||
maxHeight="lg"
|
||||
customButton={
|
||||
<span className="text-xs px-1.5 py-1 border rounded-md text-custom-text-200 border-custom-border-300">
|
||||
Settings
|
||||
</span>
|
||||
}
|
||||
placement="bottom-start"
|
||||
closeOnSelect
|
||||
>
|
||||
{PROJECT_SETTINGS_LINKS.map(
|
||||
(item) =>
|
||||
allowPermissions(
|
||||
item.access,
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString()
|
||||
) && (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.key}
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)}
|
||||
>
|
||||
{t(item.i18n_label)}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</Header.LeftItem>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
+1
-1
@@ -68,7 +68,7 @@ const ProjectViewsPage = observer(() => {
|
||||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.view.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { FC, ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_ACCESS } from "@plane/constants";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { AppHeader } from "@/components/core";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
// plane web constants
|
||||
// local components
|
||||
import { WorkspaceSettingHeader } from "../header";
|
||||
import { MobileWorkspaceSettingsTabs } from "./mobile-header-tabs";
|
||||
import { WorkspaceSettingsSidebar } from "./sidebar";
|
||||
|
||||
export interface IWorkspaceSettingLayout {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const WorkspaceSettingLayout: FC<IWorkspaceSettingLayout> = observer((props) => {
|
||||
const { children } = props;
|
||||
|
||||
const { workspaceUserInfo } = useUserPermissions();
|
||||
const pathname = usePathname();
|
||||
const [workspaceSlug, suffix, route] = pathname.replace(/^\/|\/$/g, "").split("/"); // Regex removes leading and trailing slashes
|
||||
|
||||
// derived values
|
||||
const userWorkspaceRole = workspaceUserInfo?.[workspaceSlug.toString()]?.role;
|
||||
const isAuthorized =
|
||||
pathname &&
|
||||
workspaceSlug &&
|
||||
userWorkspaceRole &&
|
||||
WORKSPACE_SETTINGS_ACCESS[route ? `/${suffix}/${route}` : `/${suffix}`]?.includes(
|
||||
userWorkspaceRole as EUserWorkspaceRoles
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppHeader header={<WorkspaceSettingHeader />} />
|
||||
<MobileWorkspaceSettingsTabs />
|
||||
<div className="inset-y-0 flex flex-row vertical-scrollbar scrollbar-lg h-full w-full overflow-y-auto">
|
||||
{workspaceUserInfo && !isAuthorized ? (
|
||||
<NotAuthorizedView section="settings" />
|
||||
) : (
|
||||
<>
|
||||
<div className="px-page-x !pr-0 py-page-y flex-shrink-0 overflow-y-hidden sm:hidden hidden md:block lg:block">
|
||||
<WorkspaceSettingsSidebar />
|
||||
</div>
|
||||
<div className="flex flex-col relative w-full overflow-hidden">
|
||||
<div className="w-full h-full overflow-x-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-page-x md:px-9 py-page-y">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default WorkspaceSettingLayout;
|
||||
@@ -1,48 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { WORKSPACE_SETTINGS_LINKS, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
// plane web helpers
|
||||
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
|
||||
|
||||
export const WorkspaceSettingsSidebar = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
const pathname = usePathname();
|
||||
// mobx store
|
||||
const { t } = useTranslation();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
return (
|
||||
<div className="flex w-[280px] flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-semibold text-custom-sidebar-text-400 uppercase">{t("settings")}</span>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
{WORKSPACE_SETTINGS_LINKS.map(
|
||||
(link) =>
|
||||
shouldRenderSettingLink(workspaceSlug.toString(), link.key) &&
|
||||
allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
|
||||
<Link key={link.key} href={`/${workspaceSlug}${link.href}`}>
|
||||
<SidebarNavItem
|
||||
key={link.key}
|
||||
isActive={link.highlight(pathname, `/${workspaceSlug}`)}
|
||||
className="text-sm font-medium px-4 py-2"
|
||||
>
|
||||
{t(link.i18n_label)}
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Settings } from "lucide-react";
|
||||
// ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Breadcrumbs, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
||||
export const WorkspaceSettingHeader: FC = observer(() => {
|
||||
const { currentWorkspace, loader } = useWorkspace();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs isLoading={loader}>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href={`/${currentWorkspace?.slug}/settings`}
|
||||
label={currentWorkspace?.name ?? "Workspace"}
|
||||
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label={t("settings")} />} />
|
||||
</Breadcrumbs>
|
||||
</Header.LeftItem>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { CommandPalette } from "@/components/command-palette";
|
||||
import { ContentWrapper } from "@/components/core";
|
||||
import { SettingsContentLayout, SettingsHeader } from "@/components/settings";
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
|
||||
|
||||
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<AuthenticationWrapper>
|
||||
<WorkspaceAuthWrapper>
|
||||
<CommandPalette />
|
||||
<main className="relative flex h-screen w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
{/* Header */}
|
||||
<SettingsHeader />
|
||||
{/* Content */}
|
||||
<ContentWrapper className="px-4 md:pl-12 md:pt-page-y md:flex w-full">
|
||||
<SettingsContentLayout>{children}</SettingsContentLayout>
|
||||
</ContentWrapper>
|
||||
</main>
|
||||
</WorkspaceAuthWrapper>
|
||||
</AuthenticationWrapper>
|
||||
);
|
||||
}
|
||||
+4
-3
@@ -6,6 +6,7 @@ import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
// hooks
|
||||
import { SettingsContentWrapper } from "@/components/settings";
|
||||
import { useUserPermissions, useWorkspace } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { BillingRoot } from "@/plane-web/components/workspace";
|
||||
@@ -19,14 +20,14 @@ const BillingSettingsPage = observer(() => {
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined;
|
||||
|
||||
if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
return <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsContentWrapper size="lg">
|
||||
<PageHead title={pageTitle} />
|
||||
<BillingRoot />
|
||||
</>
|
||||
</SettingsContentWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
+9
-7
@@ -8,6 +8,7 @@ import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import ExportGuide from "@/components/exporter/guide";
|
||||
// helpers
|
||||
import { SettingsContentWrapper, SettingsHeading } from "@/components/settings";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useUserPermissions, useWorkspace } from "@/hooks/store";
|
||||
@@ -29,23 +30,24 @@ const ExportsPage = observer(() => {
|
||||
|
||||
// if user is not authorized to view this page
|
||||
if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
return <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsContentWrapper size="lg">
|
||||
<PageHead title={pageTitle} />
|
||||
<div
|
||||
className={cn("w-full overflow-y-auto", {
|
||||
className={cn("w-full", {
|
||||
"opacity-60": !canPerformWorkspaceMemberActions,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
||||
<h3 className="text-xl font-medium">{t("workspace_settings.settings.exports.title")}</h3>
|
||||
</div>
|
||||
<SettingsHeading
|
||||
title={t("workspace_settings.settings.exports.heading")}
|
||||
description={t("workspace_settings.settings.exports.description")}
|
||||
/>
|
||||
<ExportGuide />
|
||||
</div>
|
||||
</>
|
||||
</SettingsContentWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
+8
-16
@@ -3,40 +3,32 @@
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import IntegrationGuide from "@/components/integration/guide";
|
||||
// hooks
|
||||
import { SettingsContentWrapper, SettingsHeading } from "@/components/settings";
|
||||
import { useUserPermissions, useWorkspace } from "@/hooks/store";
|
||||
|
||||
const ImportsPage = observer(() => {
|
||||
// router
|
||||
// store hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
// derived values
|
||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Imports` : undefined;
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
if (!isAdmin) return <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsContentWrapper size="lg">
|
||||
<PageHead title={pageTitle} />
|
||||
<section className="w-full overflow-y-auto">
|
||||
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
||||
<h3 className="text-xl font-medium">Imports</h3>
|
||||
</div>
|
||||
<section className="w-full">
|
||||
<SettingsHeading title="Imports" />
|
||||
<IntegrationGuide />
|
||||
</section>
|
||||
</>
|
||||
</SettingsContentWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
+6
-13
@@ -4,8 +4,10 @@ import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { SingleIntegrationCard } from "@/components/integration";
|
||||
import { SettingsContentWrapper } from "@/components/settings";
|
||||
import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "@/components/ui";
|
||||
// constants
|
||||
import { APP_INTEGRATIONS } from "@/constants/fetch-keys";
|
||||
@@ -26,23 +28,14 @@ const WorkspaceIntegrationsPage = observer(() => {
|
||||
// derived values
|
||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Integrations` : undefined;
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const { data: appIntegrations } = useSWR(workspaceSlug && isAdmin ? APP_INTEGRATIONS : null, () =>
|
||||
workspaceSlug && isAdmin ? integrationService.getAppIntegrationsList() : null
|
||||
);
|
||||
|
||||
if (!isAdmin) return <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsContentWrapper size="lg">
|
||||
<PageHead title={pageTitle} />
|
||||
<section className="w-full overflow-y-auto">
|
||||
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
||||
@@ -56,7 +49,7 @@ const WorkspaceIntegrationsPage = observer(() => {
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
</SettingsContentWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { FC, ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_ACCESS } from "@plane/constants";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { CommandPalette } from "@/components/command-palette";
|
||||
import { SettingsMobileNav } from "@/components/settings";
|
||||
import { getWorkspaceActivePath, pathnameToAccessKey } from "@/components/settings/helper";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
// local components
|
||||
import { WorkspaceSettingsSidebar } from "./sidebar";
|
||||
|
||||
export interface IWorkspaceSettingLayout {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const WorkspaceSettingLayout: FC<IWorkspaceSettingLayout> = observer((props) => {
|
||||
const { children } = props;
|
||||
// store hooks
|
||||
const { workspaceUserInfo, getWorkspaceRoleByWorkspaceSlug } = useUserPermissions();
|
||||
// next hooks
|
||||
const pathname = usePathname();
|
||||
// derived values
|
||||
const { workspaceSlug, accessKey } = pathnameToAccessKey(pathname);
|
||||
const userWorkspaceRole = getWorkspaceRoleByWorkspaceSlug(workspaceSlug.toString());
|
||||
|
||||
let isAuthorized: boolean | string = false;
|
||||
if (pathname && workspaceSlug && userWorkspaceRole) {
|
||||
isAuthorized = WORKSPACE_SETTINGS_ACCESS[accessKey]?.includes(userWorkspaceRole as EUserWorkspaceRoles);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommandPalette />
|
||||
<SettingsMobileNav
|
||||
hamburgerContent={WorkspaceSettingsSidebar}
|
||||
activePath={getWorkspaceActivePath(pathname) || ""}
|
||||
/>
|
||||
<div className="inset-y-0 flex flex-row w-full">
|
||||
{workspaceUserInfo && !isAuthorized ? (
|
||||
<NotAuthorizedView section="settings" className="h-auto" />
|
||||
) : (
|
||||
<div className="relative flex h-full w-full">
|
||||
<div className="hidden md:block">{<WorkspaceSettingsSidebar />}</div>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default WorkspaceSettingLayout;
|
||||
+5
-4
@@ -14,6 +14,7 @@ import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { CountChip } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { SettingsContentWrapper } from "@/components/settings";
|
||||
import { WorkspaceMembersList } from "@/components/workspace";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
@@ -95,11 +96,11 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||
|
||||
// if user is not authorized to view this page
|
||||
if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
return <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsContentWrapper size="lg">
|
||||
<PageHead title={pageTitle} />
|
||||
<SendWorkspaceInvitationModal
|
||||
isOpen={inviteModal}
|
||||
@@ -107,7 +108,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||
onSubmit={handleWorkspaceInvite}
|
||||
/>
|
||||
<section
|
||||
className={cn("w-full h-full overflow-y-auto", {
|
||||
className={cn("w-full h-full", {
|
||||
"opacity-60": !canPerformWorkspaceMemberActions,
|
||||
})}
|
||||
>
|
||||
@@ -137,7 +138,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||
</div>
|
||||
<WorkspaceMembersList searchQuery={searchQuery} isAdmin={canPerformWorkspaceAdminActions} />
|
||||
</section>
|
||||
</>
|
||||
</SettingsContentWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
+3
-2
@@ -4,6 +4,7 @@ import { observer } from "mobx-react";
|
||||
// components
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { SettingsContentWrapper } from "@/components/settings";
|
||||
import { WorkspaceDetails } from "@/components/workspace";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
@@ -18,10 +19,10 @@ const WorkspaceSettingsPage = observer(() => {
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsContentWrapper>
|
||||
<PageHead title={pageTitle} />
|
||||
<WorkspaceDetails />
|
||||
</>
|
||||
</SettingsContentWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { ArrowUpToLine, Building, CreditCard, Users, Webhook } from "lucide-react";
|
||||
import {
|
||||
EUserPermissionsLevel,
|
||||
GROUPED_WORKSPACE_SETTINGS,
|
||||
WORKSPACE_SETTINGS_CATEGORIES,
|
||||
EUserWorkspaceRoles,
|
||||
EUserPermissions,
|
||||
WORKSPACE_SETTINGS_CATEGORY,
|
||||
} from "@plane/constants";
|
||||
import { SettingsSidebar } from "@/components/settings";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
|
||||
|
||||
const ICONS = {
|
||||
general: Building,
|
||||
members: Users,
|
||||
export: ArrowUpToLine,
|
||||
"billing-and-plans": CreditCard,
|
||||
webhooks: Webhook,
|
||||
};
|
||||
|
||||
export const WorkspaceActionIcons = ({
|
||||
type,
|
||||
size,
|
||||
className,
|
||||
}: {
|
||||
type: string;
|
||||
size?: number;
|
||||
className?: string;
|
||||
}) => {
|
||||
if (type === undefined) return null;
|
||||
const Icon = ICONS[type as keyof typeof ICONS];
|
||||
if (!Icon) return null;
|
||||
return <Icon size={size} className={className} strokeWidth={2} />;
|
||||
};
|
||||
|
||||
type TWorkspaceSettingsSidebarProps = {
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
export const WorkspaceSettingsSidebar = (props: TWorkspaceSettingsSidebarProps) => {
|
||||
const { isMobile = false } = props;
|
||||
// router
|
||||
const pathname = usePathname();
|
||||
const { workspaceSlug } = useParams(); // store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
|
||||
return (
|
||||
<SettingsSidebar
|
||||
isMobile={isMobile}
|
||||
categories={WORKSPACE_SETTINGS_CATEGORIES.filter(
|
||||
(category) =>
|
||||
isAdmin || ![WORKSPACE_SETTINGS_CATEGORY.FEATURES, WORKSPACE_SETTINGS_CATEGORY.DEVELOPER].includes(category)
|
||||
)}
|
||||
groupedSettings={GROUPED_WORKSPACE_SETTINGS}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
isActive={(data: { href: string }) =>
|
||||
data.href === "/settings"
|
||||
? pathname === `/${workspaceSlug}${data.href}/`
|
||||
: new RegExp(`^/${workspaceSlug}${data.href}/`).test(pathname)
|
||||
}
|
||||
shouldRender={(data: { key: string; access?: EUserWorkspaceRoles[] | undefined }) =>
|
||||
data.access
|
||||
? shouldRenderSettingLink(workspaceSlug.toString(), data.key) &&
|
||||
allowPermissions(data.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString())
|
||||
: false
|
||||
}
|
||||
actionIcons={WorkspaceActionIcons}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+3
-2
@@ -11,6 +11,7 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { SettingsContentWrapper } from "@/components/settings";
|
||||
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "@/components/web-hooks";
|
||||
// hooks
|
||||
import { useUserPermissions, useWebhook, useWorkspace } from "@/hooks/store";
|
||||
@@ -87,7 +88,7 @@ const WebhookDetailsPage = observer(() => {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsContentWrapper>
|
||||
<PageHead title={pageTitle} />
|
||||
<DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} />
|
||||
<div className="w-full space-y-8 overflow-y-auto">
|
||||
@@ -96,7 +97,7 @@ const WebhookDetailsPage = observer(() => {
|
||||
</div>
|
||||
{currentWebhook && <WebhookDeleteSection openDeleteModal={() => setDeleteWebhookModal(true)} />}
|
||||
</div>
|
||||
</>
|
||||
</SettingsContentWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user