Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4eab66e3d | |||
| d9d39199ae | |||
| f30e31e294 | |||
| dc57098507 |
+1
-2
@@ -2,7 +2,6 @@
|
||||
*.pyc
|
||||
.env
|
||||
venv
|
||||
.venv
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
npm-debug.log
|
||||
@@ -15,4 +14,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.29",
|
||||
"next": "^14.2.28",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.38",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -148,13 +148,10 @@ class ProjectMemberAdminSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
|
||||
original_role = serializers.IntegerField(source='role', read_only=True)
|
||||
|
||||
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
|
||||
class Meta:
|
||||
model = ProjectMember
|
||||
fields = ("id", "role", "member", "project", "original_role", "created_at")
|
||||
read_only_fields = ["original_role", "created_at"]
|
||||
fields = ("id", "role", "member", "project")
|
||||
|
||||
|
||||
class ProjectMemberInviteSerializer(BaseSerializer):
|
||||
|
||||
@@ -3,22 +3,11 @@ 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
|
||||
@@ -110,16 +99,11 @@ 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,12 +25,10 @@ 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):
|
||||
@@ -38,21 +36,10 @@ 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,7 +12,6 @@ from plane.app.views import (
|
||||
AssetRestoreEndpoint,
|
||||
ProjectAssetEndpoint,
|
||||
ProjectBulkAssetEndpoint,
|
||||
AssetCheckEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -82,11 +81,5 @@ 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,7 +106,6 @@ from .asset.v2 import (
|
||||
AssetRestoreEndpoint,
|
||||
ProjectAssetEndpoint,
|
||||
ProjectBulkAssetEndpoint,
|
||||
AssetCheckEndpoint,
|
||||
)
|
||||
from .issue.base import (
|
||||
IssueListEndpoint,
|
||||
|
||||
@@ -707,14 +707,3 @@ 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,7 +15,6 @@ 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
|
||||
|
||||
|
||||
@@ -45,9 +44,6 @@ 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),
|
||||
@@ -59,10 +55,6 @@ 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)
|
||||
|
||||
@@ -74,14 +66,9 @@ 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,
|
||||
@@ -93,9 +80,6 @@ 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,7 +3,6 @@ import csv
|
||||
import io
|
||||
import os
|
||||
from datetime import date
|
||||
import uuid
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.db import IntegrityError
|
||||
@@ -36,7 +35,6 @@ from plane.db.models import (
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
WorkspaceTheme,
|
||||
Profile
|
||||
)
|
||||
from plane.app.permissions import ROLE, allow_permission
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -45,7 +43,6 @@ 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):
|
||||
@@ -112,12 +109,6 @@ 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
|
||||
@@ -159,19 +150,8 @@ 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)
|
||||
|
||||
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
# 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,14 +4,6 @@ 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,6 +32,5 @@ export * from "./dashboard";
|
||||
export * from "./page";
|
||||
export * from "./emoji";
|
||||
export * from "./subscription";
|
||||
export * from "./settings";
|
||||
export * from "./icon";
|
||||
export * from "./analytics-v2";
|
||||
|
||||
@@ -1,53 +1,39 @@
|
||||
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;
|
||||
}[] = [
|
||||
PROFILE_SETTINGS["profile"],
|
||||
PROFILE_SETTINGS["security"],
|
||||
PROFILE_SETTINGS["activity"],
|
||||
PROFILE_SETTINGS["preferences"],
|
||||
PROFILE_SETTINGS["notifications"],
|
||||
PROFILE_SETTINGS["api-tokens"],
|
||||
{
|
||||
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/",
|
||||
},
|
||||
];
|
||||
|
||||
export const PROFILE_VIEWER_TAB = [
|
||||
@@ -86,23 +72,6 @@ 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}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
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,6 +114,13 @@ 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(
|
||||
@@ -132,6 +139,7 @@ export const WORKSPACE_SETTINGS_LINKS: {
|
||||
WORKSPACE_SETTINGS["billing-and-plans"],
|
||||
WORKSPACE_SETTINGS["export"],
|
||||
WORKSPACE_SETTINGS["webhooks"],
|
||||
WORKSPACE_SETTINGS["api-tokens"],
|
||||
];
|
||||
|
||||
export const ROLE = {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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,7 +91,6 @@ 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, TExtensions } from "@/types";
|
||||
import { TAIHandler, TDisplayConfig } from "@/types";
|
||||
|
||||
type IPageRenderer = {
|
||||
aiHandler?: TAIHandler;
|
||||
@@ -13,20 +13,10 @@ type IPageRenderer = {
|
||||
editorContainerClassName: string;
|
||||
id: string;
|
||||
tabIndex?: number;
|
||||
disabledExtensions: TExtensions[];
|
||||
};
|
||||
|
||||
export const PageRenderer = (props: IPageRenderer) => {
|
||||
const {
|
||||
aiHandler,
|
||||
bubbleMenuEnabled,
|
||||
displayConfig,
|
||||
editor,
|
||||
editorContainerClassName,
|
||||
id,
|
||||
tabIndex,
|
||||
disabledExtensions,
|
||||
} = props;
|
||||
const { aiHandler, bubbleMenuEnabled, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
|
||||
|
||||
return (
|
||||
<div className="frame-renderer flex-grow w-full">
|
||||
@@ -40,7 +30,7 @@ export const PageRenderer = (props: IPageRenderer) => {
|
||||
{editor.isEditable && (
|
||||
<div>
|
||||
{bubbleMenuEnabled && <EditorBubbleMenu editor={editor} />}
|
||||
<BlockMenu editor={editor} disabledExtensions={disabledExtensions} />
|
||||
<BlockMenu editor={editor} />
|
||||
<AIFeaturesMenu menu={aiHandler?.menu} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -83,7 +83,6 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
||||
editor={editor}
|
||||
editorContainerClassName={cn(editorContainerClassName, "document-editor")}
|
||||
id={id}
|
||||
disabledExtensions={disabledExtensions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ 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;
|
||||
@@ -97,7 +96,7 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<LinkContainer editor={editor} containerRef={containerRef} />
|
||||
<LinkViewContainer editor={editor} containerRef={containerRef} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ 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;
|
||||
@@ -18,7 +17,7 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
selector: ({ editor }: { editor: Editor }) => ({
|
||||
linkExtensionStorage: getExtensionStorage(editor, "link"),
|
||||
linkExtensionStorage: editor.storage.link,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -110,7 +109,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,11 +4,9 @@ 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,10 +86,6 @@ 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,
|
||||
@@ -98,12 +94,18 @@ 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,
|
||||
@@ -138,8 +140,11 @@ 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,7 +79,6 @@ declare module "@tiptap/core" {
|
||||
export type CustomLinkStorage = {
|
||||
isPreviewOpen: boolean;
|
||||
posToInsert: { from: number; to: number };
|
||||
isBubbleMenuOpen: boolean;
|
||||
};
|
||||
|
||||
export const CustomLinkExtension = Mark.create<LinkOptions, CustomLinkStorage>({
|
||||
|
||||
@@ -81,7 +81,6 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
immediatelyRender: false,
|
||||
shouldRerenderOnTransaction: false,
|
||||
autofocus,
|
||||
parseOptions: { preserveWhitespace: true },
|
||||
editorProps: {
|
||||
...CoreEditorProps({
|
||||
editorClassName,
|
||||
@@ -120,7 +119,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: true });
|
||||
editor.commands.setContent(value, false, { preserveWhitespace: "full" });
|
||||
if (editor.state.selection) {
|
||||
const docLength = editor.state.doc.content.size;
|
||||
const relativePosition = Math.min(editor.state.selection.from, docLength - 1);
|
||||
@@ -154,7 +153,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: true });
|
||||
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" });
|
||||
},
|
||||
setEditorValueAtCursorPosition: (content: string) => {
|
||||
if (editor?.state.selection) {
|
||||
|
||||
@@ -9,11 +9,11 @@ import { TEditorCommands } from "@/types";
|
||||
|
||||
type TUploaderArgs = {
|
||||
acceptedMimeTypes: string[];
|
||||
editorCommand: (file: File) => Promise<string | undefined>;
|
||||
editorCommand: (file: File) => Promise<string>;
|
||||
handleProgressStatus?: (isUploading: boolean) => void;
|
||||
loadFileFromFileSystem?: (file: string) => void;
|
||||
maxFileSize: number;
|
||||
onInvalidFile: (error: EFileError, file: File, message: string) => void;
|
||||
onInvalidFile: (error: EFileError, message: string) => void;
|
||||
onUpload: (url: string, file: File) => void;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const useUploader = (args: TUploaderArgs) => {
|
||||
acceptedMimeTypes,
|
||||
file,
|
||||
maxFileSize,
|
||||
onError: (error, message) => onInvalidFile(error, file, message),
|
||||
onError: onInvalidFile,
|
||||
});
|
||||
if (!isValid) {
|
||||
handleProgressStatus?.(false);
|
||||
@@ -60,7 +60,7 @@ export const useUploader = (args: TUploaderArgs) => {
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
const url = await editorCommand(file);
|
||||
const url: string = await editorCommand(file);
|
||||
|
||||
if (!url) {
|
||||
throw new Error("Something went wrong while uploading the file.");
|
||||
@@ -89,14 +89,17 @@ 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 { editor, pos, type, uploader } = args;
|
||||
const { acceptedMimeTypes, editor, maxFileSize, onInvalidFile, pos, type, uploader } = args;
|
||||
// states
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [draggedInside, setDraggedInside] = useState<boolean>(false);
|
||||
@@ -123,21 +126,22 @@ export const useDropZone = (args: TDropzoneArgs) => {
|
||||
async (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setDraggedInside(false);
|
||||
const filesList = e.dataTransfer.files;
|
||||
|
||||
if (filesList.length === 0 || !editor.isEditable) {
|
||||
if (e.dataTransfer.files.length === 0 || !editor.isEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filesList = e.dataTransfer.files;
|
||||
await uploadFirstFileAndInsertRemaining({
|
||||
acceptedMimeTypes,
|
||||
editor,
|
||||
filesList,
|
||||
maxFileSize,
|
||||
onInvalidFile,
|
||||
pos,
|
||||
type,
|
||||
uploader,
|
||||
});
|
||||
},
|
||||
[editor, pos, type, uploader]
|
||||
[acceptedMimeTypes, editor, maxFileSize, onInvalidFile, pos, type, uploader]
|
||||
);
|
||||
const onDragEnter = useCallback(() => setDraggedInside(true), []);
|
||||
const onDragLeave = useCallback(() => setDraggedInside(false), []);
|
||||
@@ -152,8 +156,11 @@ 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>;
|
||||
@@ -161,18 +168,35 @@ type TMultipleFileArgs = {
|
||||
|
||||
// Upload the first file and insert the remaining ones for uploading multiple files
|
||||
export const uploadFirstFileAndInsertRemaining = async (args: TMultipleFileArgs) => {
|
||||
const { editor, filesList, pos, type, uploader } = args;
|
||||
const filesArray = Array.from(filesList);
|
||||
if (filesArray.length === 0) {
|
||||
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) {
|
||||
console.error("No files found to upload.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload the first file
|
||||
const firstFile = filesArray[0];
|
||||
const firstFile = filteredFiles[0];
|
||||
uploader(firstFile);
|
||||
// Insert the remaining files
|
||||
const remainingFiles = filesArray.slice(1);
|
||||
const remainingFiles = filteredFiles.slice(1);
|
||||
if (remainingFiles.length > 0) {
|
||||
const docSize = editor.state.doc.content.size;
|
||||
const posOfNextFileToBeInserted = Math.min(pos + 1, docSize);
|
||||
|
||||
@@ -46,7 +46,6 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||
immediatelyRender: true,
|
||||
shouldRerenderOnTransaction: false,
|
||||
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
|
||||
parseOptions: { preserveWhitespace: true },
|
||||
editorProps: {
|
||||
...CoreReadOnlyEditorProps({
|
||||
editorClassName,
|
||||
@@ -72,7 +71,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: true });
|
||||
if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: "full" });
|
||||
}, [editor, initialValue]);
|
||||
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
@@ -80,7 +79,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: true });
|
||||
editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" });
|
||||
},
|
||||
getMarkDown: (): string => {
|
||||
const markdownOutput = editor?.storage.markdown.getMarkdown();
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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
|
||||
@@ -34,7 +32,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(CORE_EDITOR_META.SKIP_FILE_DELETION)) return;
|
||||
if (transaction.getMeta("skipFileDeletion")) return;
|
||||
|
||||
const removedFiles: TFileNode[] = [];
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export type TReadOnlyFileHandler = {
|
||||
checkIfAssetExists: (assetId: string) => Promise<boolean>;
|
||||
getAssetSrc: (path: string) => Promise<string>;
|
||||
restore: (assetSrc: string) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"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,7 +848,6 @@
|
||||
"live": "Živě",
|
||||
"change_history": "Historie změn",
|
||||
"coming_soon": "Již brzy",
|
||||
"member": "Člen",
|
||||
"members": "Členové",
|
||||
"you": "Vy",
|
||||
"upgrade_cta": {
|
||||
@@ -1081,9 +1080,7 @@
|
||||
"select": {
|
||||
"error": "Vyberte alespoň jednu pracovní položku",
|
||||
"empty": "Nevybrány žádné 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"
|
||||
"add_selected": "Přidat vybrané pracovní položky"
|
||||
},
|
||||
"open_in_full_screen": "Otevřít pracovní položku na celou obrazovku"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"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,7 +848,6 @@
|
||||
"live": "Live",
|
||||
"change_history": "Änderungsverlauf",
|
||||
"coming_soon": "Demnächst verfügbar",
|
||||
"member": "Mitglied",
|
||||
"members": "Mitglieder",
|
||||
"you": "Sie",
|
||||
"upgrade_cta": {
|
||||
@@ -1081,9 +1080,7 @@
|
||||
"select": {
|
||||
"error": "Wählen Sie mindestens ein Arbeitselement aus",
|
||||
"empty": "Keine Arbeitselemente ausgewählt",
|
||||
"add_selected": "Ausgewählte Arbeitselemente hinzufügen",
|
||||
"select_all": "Alle auswählen",
|
||||
"deselect_all": "Alle abwählen"
|
||||
"add_selected": "Ausgewählte Arbeitselemente hinzufügen"
|
||||
},
|
||||
"open_in_full_screen": "Arbeitselement im Vollbild öffnen"
|
||||
},
|
||||
@@ -2462,4 +2459,4 @@
|
||||
"previously_edited_by": "Zuvor bearbeitet von",
|
||||
"edited_by": "Bearbeitet von"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,13 +22,6 @@
|
||||
"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,8 +43,7 @@
|
||||
"your_account": "Your account",
|
||||
"security": "Security",
|
||||
"activity": "Activity",
|
||||
"preferences": "Preferences",
|
||||
"language_and_time": "Language & Time",
|
||||
"appearance": "Appearance",
|
||||
"notifications": "Notifications",
|
||||
"workspaces": "Workspaces",
|
||||
"create_workspace": "Create workspace",
|
||||
@@ -57,10 +56,6 @@
|
||||
"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",
|
||||
@@ -339,8 +334,6 @@
|
||||
"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",
|
||||
@@ -690,7 +683,6 @@
|
||||
"live": "Live",
|
||||
"change_history": "Change History",
|
||||
"coming_soon": "Coming soon",
|
||||
"member": "Member",
|
||||
"members": "Members",
|
||||
"you": "You",
|
||||
"upgrade_cta": {
|
||||
@@ -924,9 +916,7 @@
|
||||
"select": {
|
||||
"error": "Please select at least one work item",
|
||||
"empty": "No work items selected",
|
||||
"add_selected": "Add selected work items",
|
||||
"select_all": "Select all",
|
||||
"deselect_all": "Deselect all"
|
||||
"add_selected": "Add selected work items"
|
||||
},
|
||||
"open_in_full_screen": "Open work item in full screen"
|
||||
},
|
||||
@@ -1311,28 +1301,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -1399,22 +1367,16 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@@ -1430,8 +1392,6 @@
|
||||
}
|
||||
},
|
||||
"webhooks": {
|
||||
"heading": "Webhooks",
|
||||
"description": "Automate notifications to external services when project events occur.",
|
||||
"title": "Webhooks",
|
||||
"add_webhook": "Add webhook",
|
||||
"modal": {
|
||||
@@ -1483,29 +1443,29 @@
|
||||
}
|
||||
},
|
||||
"api_tokens": {
|
||||
"title": "Personal Access Tokens",
|
||||
"add_token": "Add personal access token",
|
||||
"title": "API Tokens",
|
||||
"add_token": "Add API token",
|
||||
"create_token": "Create token",
|
||||
"never_expires": "Never expires",
|
||||
"generate_token": "Generate token",
|
||||
"generating": "Generating",
|
||||
"delete": {
|
||||
"title": "Delete personal access token",
|
||||
"title": "Delete API 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 token has been successfully deleted"
|
||||
"message": "The API token has been successfully deleted"
|
||||
},
|
||||
"error": {
|
||||
"title": "Error!",
|
||||
"message": "The token could not be deleted"
|
||||
"message": "The API token could not be deleted"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
"api_tokens": {
|
||||
"title": "No personal access tokens created",
|
||||
"title": "No API 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": {
|
||||
@@ -1555,9 +1515,8 @@
|
||||
"profile": "Profile",
|
||||
"security": "Security",
|
||||
"activity": "Activity",
|
||||
"preferences": "Preferences",
|
||||
"notifications": "Notifications",
|
||||
"api-tokens": "Personal Access Tokens"
|
||||
"appearance": "Appearance",
|
||||
"notifications": "Notifications"
|
||||
},
|
||||
"tabs": {
|
||||
"summary": "Summary",
|
||||
@@ -1619,8 +1578,6 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -1628,8 +1585,6 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -1638,11 +1593,9 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"enable_description": "They help you in communicating complexity and workload of the team.",
|
||||
"description": "They help you in communicating complexity and workload of the team.",
|
||||
"no_estimate": "No estimate",
|
||||
"new": "New estimate system",
|
||||
"create": {
|
||||
@@ -1724,8 +1677,6 @@
|
||||
},
|
||||
"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,13 +22,6 @@
|
||||
"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,7 +851,6 @@
|
||||
"live": "En vivo",
|
||||
"change_history": "Historial de cambios",
|
||||
"coming_soon": "Próximamente",
|
||||
"member": "Miembro",
|
||||
"members": "Miembros",
|
||||
"you": "Tú",
|
||||
"upgrade_cta": {
|
||||
@@ -1084,9 +1083,7 @@
|
||||
"select": {
|
||||
"error": "Por favor selecciona al menos un elemento de trabajo",
|
||||
"empty": "No hay elementos de trabajo seleccionados",
|
||||
"add_selected": "Agregar elementos seleccionados",
|
||||
"select_all": "Seleccionar todo",
|
||||
"deselect_all": "Deseleccionar todo"
|
||||
"add_selected": "Agregar elementos seleccionados"
|
||||
},
|
||||
"open_in_full_screen": "Abrir elemento de trabajo en pantalla completa"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"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,7 +849,6 @@
|
||||
"live": "En direct",
|
||||
"change_history": "Historique des modifications",
|
||||
"coming_soon": "À venir",
|
||||
"member": "Membre",
|
||||
"members": "Membres",
|
||||
"you": "Vous",
|
||||
"upgrade_cta": {
|
||||
@@ -1082,9 +1081,7 @@
|
||||
"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",
|
||||
"select_all": "Sélectionner tout",
|
||||
"deselect_all": "Tout désélectionner"
|
||||
"add_selected": "Ajouter les éléments de travail sélectionnés"
|
||||
},
|
||||
"open_in_full_screen": "Ouvrir l'élément de travail en plein écran"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"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,7 +848,6 @@
|
||||
"live": "Langsung",
|
||||
"change_history": "Riwayat Perubahan",
|
||||
"coming_soon": "Segera hadir",
|
||||
"member": "Anggota",
|
||||
"members": "Anggota",
|
||||
"you": "Anda",
|
||||
"upgrade_cta": {
|
||||
@@ -1081,9 +1080,7 @@
|
||||
"select": {
|
||||
"error": "Silakan pilih setidaknya satu item kerja",
|
||||
"empty": "Tidak ada item kerja yang dipilih",
|
||||
"add_selected": "Tambah item kerja yang dipilih",
|
||||
"select_all": "Pilih semua item kerja",
|
||||
"deselect_all": "Batalkan pilihan semua item kerja"
|
||||
"add_selected": "Tambah item kerja yang dipilih"
|
||||
},
|
||||
"open_in_full_screen": "Buka item kerja dalam layar penuh"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"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,7 +847,6 @@
|
||||
"live": "Live",
|
||||
"change_history": "Cronologia modifiche",
|
||||
"coming_soon": "Prossimamente",
|
||||
"member": "Membro",
|
||||
"members": "Membri",
|
||||
"you": "Tu",
|
||||
"upgrade_cta": {
|
||||
@@ -1080,9 +1079,7 @@
|
||||
"select": {
|
||||
"error": "Seleziona almeno un elemento di lavoro",
|
||||
"empty": "Nessun elemento di lavoro selezionato",
|
||||
"add_selected": "Aggiungi gli elementi di lavoro selezionati",
|
||||
"select_all": "Seleziona tutto",
|
||||
"deselect_all": "Deseleziona tutto"
|
||||
"add_selected": "Aggiungi gli elementi di lavoro selezionati"
|
||||
},
|
||||
"open_in_full_screen": "Apri l'elemento di lavoro a schermo intero"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"collapse_sidebar": "サイドバーを折りたたむ",
|
||||
"expand_sidebar": "サイドバーを展開",
|
||||
"edition_badge": "有料プランのモーダルを開く"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "メールをクリア",
|
||||
"show_password": "パスワードを表示",
|
||||
"hide_password": "パスワードを非表示",
|
||||
"close_alert": "アラートを閉じる",
|
||||
"close_popover": "ポップオーバーを閉じる"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -849,7 +849,6 @@
|
||||
"live": "ライブ",
|
||||
"change_history": "変更履歴",
|
||||
"coming_soon": "近日公開",
|
||||
"member": "メンバー",
|
||||
"members": "メンバー",
|
||||
"you": "あなた",
|
||||
"upgrade_cta": {
|
||||
@@ -1082,9 +1081,7 @@
|
||||
"select": {
|
||||
"error": "少なくとも1つの作業項目を選択してください",
|
||||
"empty": "作業項目が選択されていません",
|
||||
"add_selected": "選択した作業項目を追加",
|
||||
"select_all": "すべて選択",
|
||||
"deselect_all": "すべての選択を解除"
|
||||
"add_selected": "選択した作業項目を追加"
|
||||
},
|
||||
"open_in_full_screen": "作業項目をフルスクリーンで開く"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"collapse_sidebar": "사이드바 축소",
|
||||
"expand_sidebar": "사이드바 확장",
|
||||
"edition_badge": "유료 플랜 모달 열기"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "이메일 지우기",
|
||||
"show_password": "비밀번호 표시",
|
||||
"hide_password": "비밀번호 숨기기",
|
||||
"close_alert": "알림 닫기",
|
||||
"close_popover": "팝오버 닫기"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -850,7 +850,6 @@
|
||||
"live": "라이브",
|
||||
"change_history": "변경 기록",
|
||||
"coming_soon": "곧 출시",
|
||||
"member": "멤버",
|
||||
"members": "멤버",
|
||||
"you": "나",
|
||||
"upgrade_cta": {
|
||||
@@ -1083,9 +1082,7 @@
|
||||
"select": {
|
||||
"error": "최소 하나의 작업 항목을 선택하세요",
|
||||
"empty": "선택된 작업 항목 없음",
|
||||
"add_selected": "선택된 작업 항목 추가",
|
||||
"select_all": "모두 선택",
|
||||
"deselect_all": "모두 선택 해제"
|
||||
"add_selected": "선택된 작업 항목 추가"
|
||||
},
|
||||
"open_in_full_screen": "작업 항목을 전체 화면으로 열기"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"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,7 +850,6 @@
|
||||
"live": "Na żywo",
|
||||
"change_history": "Historia zmian",
|
||||
"coming_soon": "Wkrótce",
|
||||
"member": "Członek",
|
||||
"members": "Członkowie",
|
||||
"you": "Ty",
|
||||
"upgrade_cta": {
|
||||
@@ -1083,9 +1082,7 @@
|
||||
"select": {
|
||||
"error": "Wybierz co najmniej jeden element pracy",
|
||||
"empty": "Nie wybrano żadnych elementów pracy",
|
||||
"add_selected": "Dodaj wybrane elementy pracy",
|
||||
"select_all": "Wybierz wszystko",
|
||||
"deselect_all": "Odznacz wszystko"
|
||||
"add_selected": "Dodaj wybrane elementy pracy"
|
||||
},
|
||||
"open_in_full_screen": "Otwórz element pracy na pełnym ekranie"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"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,7 +850,6 @@
|
||||
"live": "Ao vivo",
|
||||
"change_history": "Histórico de alterações",
|
||||
"coming_soon": "Em breve",
|
||||
"member": "Membro",
|
||||
"members": "Membros",
|
||||
"you": "Você",
|
||||
"upgrade_cta": {
|
||||
@@ -1083,9 +1082,7 @@
|
||||
"select": {
|
||||
"error": "Selecione pelo menos um item de trabalho",
|
||||
"empty": "Nenhum item de trabalho selecionado",
|
||||
"add_selected": "Adicionar itens de trabalho selecionados",
|
||||
"select_all": "Selecionar tudo",
|
||||
"deselect_all": "Desmarcar tudo"
|
||||
"add_selected": "Adicionar itens de trabalho selecionados"
|
||||
},
|
||||
"open_in_full_screen": "Abrir item de trabalho em tela cheia"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"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,7 +848,6 @@
|
||||
"live": "În direct",
|
||||
"change_history": "Istoric modificări",
|
||||
"coming_soon": "În curând",
|
||||
"member": "Membru",
|
||||
"members": "Membri",
|
||||
"you": "Tu",
|
||||
"upgrade_cta": {
|
||||
@@ -1081,9 +1080,7 @@
|
||||
"select": {
|
||||
"error": "Selectează cel puțin o activitate",
|
||||
"empty": "Nicio activitate selectată",
|
||||
"add_selected": "Adaugă activitățile selectate",
|
||||
"select_all": "Selectează tot",
|
||||
"deselect_all": "Deselează tot"
|
||||
"add_selected": "Adaugă activitățile selectate"
|
||||
},
|
||||
"open_in_full_screen": "Deschide activitatea pe tot ecranul"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"collapse_sidebar": "Свернуть боковую панель",
|
||||
"expand_sidebar": "Развернуть боковую панель",
|
||||
"edition_badge": "Открыть модал платных планов"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Очистить email",
|
||||
"show_password": "Показать пароль",
|
||||
"hide_password": "Скрыть пароль",
|
||||
"close_alert": "Закрыть уведомление",
|
||||
"close_popover": "Закрыть всплывающее окно"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -850,7 +850,6 @@
|
||||
"live": "В прямом эфире",
|
||||
"change_history": "История изменений",
|
||||
"coming_soon": "Скоро",
|
||||
"member": "Участник",
|
||||
"members": "Участники",
|
||||
"you": "Вы",
|
||||
"upgrade_cta": {
|
||||
@@ -1083,9 +1082,7 @@
|
||||
"select": {
|
||||
"error": "Выберите хотя бы один рабочий элемент",
|
||||
"empty": "Рабочие элементы не выбраны",
|
||||
"add_selected": "Добавить выбранные рабочие элементы",
|
||||
"select_all": "Выбрать все",
|
||||
"deselect_all": "Снять выделение со всех"
|
||||
"add_selected": "Добавить выбранные рабочие элементы"
|
||||
},
|
||||
"open_in_full_screen": "Открыть рабочий элемент в полном экране"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"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,7 +850,6 @@
|
||||
"live": "Živé",
|
||||
"change_history": "História zmien",
|
||||
"coming_soon": "Už čoskoro",
|
||||
"member": "Člen",
|
||||
"members": "Členovia",
|
||||
"you": "Vy",
|
||||
"upgrade_cta": {
|
||||
@@ -1083,9 +1082,7 @@
|
||||
"select": {
|
||||
"error": "Vyberte aspoň jednu pracovnú položku",
|
||||
"empty": "Nie sú vybrané žiadne pracovné položky",
|
||||
"add_selected": "Pridať vybrané pracovné položky",
|
||||
"select_all": "Vybrať všetko",
|
||||
"deselect_all": "Zrušiť výber všetkého"
|
||||
"add_selected": "Pridať vybrané pracovné položky"
|
||||
},
|
||||
"open_in_full_screen": "Otvoriť pracovnú položku na celú obrazovku"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"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,7 +851,6 @@
|
||||
"live": "Canlı",
|
||||
"change_history": "Değişiklik Geçmişi",
|
||||
"coming_soon": "Çok Yakında",
|
||||
"member": "Üye",
|
||||
"members": "Üyeler",
|
||||
"you": "Siz",
|
||||
"upgrade_cta": {
|
||||
@@ -1084,9 +1083,7 @@
|
||||
"select": {
|
||||
"error": "Lütfen en az bir iş öğesi seçin",
|
||||
"empty": "Hiç iş öğesi seçilmedi",
|
||||
"add_selected": "Seçilen iş öğelerini ekle",
|
||||
"select_all": "Tümünü seç",
|
||||
"deselect_all": "Tümünü seçme"
|
||||
"add_selected": "Seçilen iş öğelerini ekle"
|
||||
},
|
||||
"open_in_full_screen": "İş öğesini tam ekranda aç"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"collapse_sidebar": "Згорнути бічну панель",
|
||||
"expand_sidebar": "Розгорнути бічну панель",
|
||||
"edition_badge": "Відкрити модал платних планів"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "Очистити email",
|
||||
"show_password": "Показати пароль",
|
||||
"hide_password": "Приховати пароль",
|
||||
"close_alert": "Закрити сповіщення",
|
||||
"close_popover": "Закрити спливаюче вікно"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -850,7 +850,6 @@
|
||||
"live": "Наживо",
|
||||
"change_history": "Історія змін",
|
||||
"coming_soon": "Незабаром",
|
||||
"member": "Учасник",
|
||||
"members": "Учасники",
|
||||
"you": "Ви",
|
||||
"upgrade_cta": {
|
||||
@@ -1083,9 +1082,7 @@
|
||||
"select": {
|
||||
"error": "Виберіть принаймні одну робочу одиницю",
|
||||
"empty": "Не вибрано жодної робочої одиниці",
|
||||
"add_selected": "Додати вибрані робочі одиниці",
|
||||
"select_all": "Вибрати всі",
|
||||
"deselect_all": "Скасувати вибір усіх"
|
||||
"add_selected": "Додати вибрані робочі одиниці"
|
||||
},
|
||||
"open_in_full_screen": "Відкрити робочу одиницю на повний екран"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"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,7 +849,6 @@
|
||||
"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": {
|
||||
@@ -1082,9 +1081,7 @@
|
||||
"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",
|
||||
"select_all": "Chọn tất cả",
|
||||
"deselect_all": "Bỏ chọn tất cả"
|
||||
"add_selected": "Thêm mục công việc đã chọn"
|
||||
},
|
||||
"open_in_full_screen": "Mở mục công việc trong chế độ toàn màn hình"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"collapse_sidebar": "折叠侧边栏",
|
||||
"expand_sidebar": "展开侧边栏",
|
||||
"edition_badge": "打开付费计划模态框"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "清除邮箱",
|
||||
"show_password": "显示密码",
|
||||
"hide_password": "隐藏密码",
|
||||
"close_alert": "关闭警告",
|
||||
"close_popover": "关闭弹出框"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -849,7 +849,6 @@
|
||||
"live": "实时",
|
||||
"change_history": "变更历史",
|
||||
"coming_soon": "即将推出",
|
||||
"member": "成员",
|
||||
"members": "成员",
|
||||
"you": "你",
|
||||
"upgrade_cta": {
|
||||
@@ -1082,9 +1081,7 @@
|
||||
"select": {
|
||||
"error": "请至少选择一个工作项",
|
||||
"empty": "未选择工作项",
|
||||
"add_selected": "添加所选工作项",
|
||||
"select_all": "全选",
|
||||
"deselect_all": "取消全选"
|
||||
"add_selected": "添加所选工作项"
|
||||
},
|
||||
"open_in_full_screen": "在全屏中打开工作项"
|
||||
},
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
"collapse_sidebar": "摺疊側邊欄",
|
||||
"expand_sidebar": "展開側邊欄",
|
||||
"edition_badge": "打開付費計劃模態框"
|
||||
},
|
||||
"auth_forms": {
|
||||
"clear_email": "清除電子郵件",
|
||||
"show_password": "顯示密碼",
|
||||
"hide_password": "隱藏密碼",
|
||||
"close_alert": "關閉警告",
|
||||
"close_popover": "關閉彈出框"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -850,7 +850,6 @@
|
||||
"live": "即時",
|
||||
"change_history": "變更歷史記錄",
|
||||
"coming_soon": "即將推出",
|
||||
"member": "成員",
|
||||
"members": "成員",
|
||||
"you": "您",
|
||||
"upgrade_cta": {
|
||||
@@ -1083,9 +1082,7 @@
|
||||
"select": {
|
||||
"error": "請至少選擇一個工作事項",
|
||||
"empty": "未選擇工作事項",
|
||||
"add_selected": "新增已選取的工作事項",
|
||||
"select_all": "全選",
|
||||
"deselect_all": "取消全選"
|
||||
"add_selected": "新增已選取的工作事項"
|
||||
},
|
||||
"open_in_full_screen": "以全螢幕開啟工作事項"
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"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,7 +469,6 @@ module.exports = {
|
||||
plugins: [
|
||||
require("tailwindcss-animate"),
|
||||
require("@tailwindcss/typography"),
|
||||
require("@tailwindcss/container-queries"),
|
||||
function ({ addUtilities }) {
|
||||
const newUtilities = {
|
||||
// Mobile screens
|
||||
|
||||
+33
-14
@@ -1,5 +1,14 @@
|
||||
import { EUserProjectRoles } from "@plane/constants";
|
||||
import type { IUser, IUserLite, IWorkspace, TLogoProps, TStateGroups } from "..";
|
||||
import type {
|
||||
IProjectViewProps,
|
||||
IUser,
|
||||
IUserLite,
|
||||
IUserMemberLite,
|
||||
IWorkspace,
|
||||
IWorkspaceLite,
|
||||
TLogoProps,
|
||||
TStateGroups,
|
||||
} from "..";
|
||||
import { TUserPermissions } from "../enums";
|
||||
|
||||
export interface IPartialProject {
|
||||
@@ -82,20 +91,30 @@ export interface IProjectMemberLite {
|
||||
member_id: string;
|
||||
}
|
||||
|
||||
export type TProjectMembership = {
|
||||
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;
|
||||
member: string;
|
||||
role: TUserPermissions | EUserProjectRoles;
|
||||
created_at: string;
|
||||
} & (
|
||||
| {
|
||||
id: string;
|
||||
original_role: EUserProjectRoles;
|
||||
}
|
||||
| {
|
||||
id: null;
|
||||
original_role: null;
|
||||
}
|
||||
);
|
||||
role: TUserPermissions;
|
||||
}
|
||||
|
||||
export interface IProjectBulkAddFormData {
|
||||
members: { role: TUserPermissions | EUserProjectRoles; member_id: string }[];
|
||||
|
||||
Vendored
-3
@@ -12,7 +12,6 @@ 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
|
||||
@@ -79,8 +78,6 @@ 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, TProjectMembership, IUser, IUserLite, IWorkspaceViewProps, TPaginationInfo } from "@plane/types";
|
||||
import type { ICycle, IProjectMember, 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?: TProjectMembership[];
|
||||
project_details?: IProjectMember[];
|
||||
}
|
||||
|
||||
export interface IWorkspaceDefaultSearchResult {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
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 "./subscription";
|
||||
export * from "./theme";
|
||||
export * from "./workspace";
|
||||
export * from "./work-item";
|
||||
export * from "./workspace";
|
||||
|
||||
export * from "./get-icon-for-link";
|
||||
|
||||
export * from "./subscription";
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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,7 +29,6 @@ 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.29",
|
||||
"next": "^14.2.28",
|
||||
"next-themes": "^0.2.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { CommandPalette } from "@/components/command-palette";
|
||||
import dynamic from "next/dynamic";
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||
// plane web components
|
||||
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
|
||||
import { AppSidebar } from "./sidebar";
|
||||
|
||||
// Dynamically import heavy components
|
||||
const CommandPalette = dynamic(
|
||||
() => import("@/components/command-palette").then((module) => ({ default: module.CommandPalette })),
|
||||
{
|
||||
ssr: false, // Command palette doesn't need SSR
|
||||
loading: () => null,
|
||||
}
|
||||
);
|
||||
|
||||
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<AuthenticationWrapper>
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ const ProjectCyclesPage = observer(() => {
|
||||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.cycle.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/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}/settings/projects/${projectId}/features`);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/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}/settings/projects/${projectId}/features`);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/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}/settings/projects/${projectId}/features`);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
||||
+7
-9
@@ -13,7 +13,6 @@ import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automation";
|
||||
import { PageHead } from "@/components/core";
|
||||
// hooks
|
||||
import { SettingsContentWrapper, SettingsHeading } from "@/components/settings";
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
const AutomationSettingsPage = observer(() => {
|
||||
@@ -44,21 +43,20 @@ const AutomationSettingsPage = observer(() => {
|
||||
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined;
|
||||
|
||||
if (workspaceUserInfo && !canPerformProjectAdminActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContentWrapper>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className={`w-full ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
|
||||
<SettingsHeading
|
||||
title={t("project_settings.automations.heading")}
|
||||
description={t("project_settings.automations.description")}
|
||||
/>
|
||||
<section className={`w-full overflow-y-auto ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
|
||||
<div className="flex flex-col items-start border-b border-custom-border-100 pb-3.5">
|
||||
<h3 className="text-xl font-medium leading-normal">{t("project_settings.automations.label")}</h3>
|
||||
</div>
|
||||
<AutoArchiveAutomation handleChange={handleChange} />
|
||||
<AutoCloseAutomation handleChange={handleChange} />
|
||||
</section>
|
||||
</SettingsContentWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
+6
-5
@@ -8,7 +8,6 @@ import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EstimateRoot } from "@/components/estimates";
|
||||
// hooks
|
||||
import { SettingsContentWrapper } from "@/components/settings";
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
const EstimatesSettingsPage = observer(() => {
|
||||
@@ -24,20 +23,22 @@ const EstimatesSettingsPage = observer(() => {
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (workspaceUserInfo && !canPerformProjectAdminActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContentWrapper>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className={`w-full ${canPerformProjectAdminActions ? "" : "pointer-events-none opacity-60"}`}>
|
||||
<div
|
||||
className={`w-full overflow-y-auto ${canPerformProjectAdminActions ? "" : "pointer-events-none opacity-60"}`}
|
||||
>
|
||||
<EstimateRoot
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
isAdmin={canPerformProjectAdminActions}
|
||||
/>
|
||||
</div>
|
||||
</SettingsContentWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
+4
-5
@@ -8,7 +8,6 @@ import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectFeaturesList } from "@/components/project";
|
||||
// hooks
|
||||
import { SettingsContentWrapper } from "@/components/settings";
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
const FeaturesSettingsPage = observer(() => {
|
||||
@@ -24,20 +23,20 @@ const FeaturesSettingsPage = observer(() => {
|
||||
if (!workspaceSlug || !projectId) return null;
|
||||
|
||||
if (workspaceUserInfo && !canPerformProjectAdminActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContentWrapper>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className={`w-full ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
|
||||
<section className={`w-full overflow-y-auto ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
|
||||
<ProjectFeaturesList
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
isAdmin={canPerformProjectAdminActions}
|
||||
/>
|
||||
</section>
|
||||
</SettingsContentWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
+5
-6
@@ -10,7 +10,6 @@ import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectSettingsLabelList } from "@/components/labels";
|
||||
// hooks
|
||||
import { SettingsContentWrapper } from "@/components/settings";
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
const LabelsSettingsPage = observer(() => {
|
||||
@@ -39,19 +38,19 @@ const LabelsSettingsPage = observer(() => {
|
||||
element,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
}, [scrollableContainerRef?.current]);
|
||||
|
||||
if (workspaceUserInfo && !canPerformProjectMemberActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContentWrapper>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div ref={scrollableContainerRef} className="h-full w-full gap-10">
|
||||
<div ref={scrollableContainerRef} className="h-full w-full gap-10 overflow-y-auto">
|
||||
<ProjectSettingsLabelList />
|
||||
</div>
|
||||
</SettingsContentWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
"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
@@ -0,0 +1,40 @@
|
||||
"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;
|
||||
+6
-6
@@ -16,9 +16,9 @@ import {
|
||||
ProjectDetailsFormLoader,
|
||||
} from "@/components/project";
|
||||
// hooks
|
||||
import { SettingsContentWrapper } from "@/components/settings";
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
const ProjectSettingsPage = observer(() => {
|
||||
|
||||
const GeneralSettingsPage = observer(() => {
|
||||
// states
|
||||
const [selectProject, setSelectedProject] = useState<string | null>(null);
|
||||
const [archiveProject, setArchiveProject] = useState<boolean>(false);
|
||||
@@ -45,7 +45,7 @@ const ProjectSettingsPage = observer(() => {
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined;
|
||||
|
||||
return (
|
||||
<SettingsContentWrapper>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
{currentProjectDetails && workspaceSlug && projectId && (
|
||||
<>
|
||||
@@ -64,7 +64,7 @@ const ProjectSettingsPage = observer(() => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={`w-full ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className={`w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
|
||||
{currentProjectDetails && workspaceSlug && projectId && !isLoading ? (
|
||||
<ProjectDetailsForm
|
||||
project={currentProjectDetails}
|
||||
@@ -89,8 +89,8 @@ const ProjectSettingsPage = observer(() => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SettingsContentWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default ProjectSettingsPage;
|
||||
export default GeneralSettingsPage;
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
"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>
|
||||
);
|
||||
});
|
||||
+8
-12
@@ -9,7 +9,6 @@ import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectStateRoot } from "@/components/project-states";
|
||||
// hook
|
||||
import { SettingsContentWrapper, SettingsHeading } from "@/components/settings";
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
const StatesSettingsPage = observer(() => {
|
||||
@@ -29,22 +28,19 @@ const StatesSettingsPage = observer(() => {
|
||||
);
|
||||
|
||||
if (workspaceUserInfo && !canPerformProjectMemberActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContentWrapper>
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="w-full">
|
||||
<SettingsHeading
|
||||
title={t("project_settings.states.heading")}
|
||||
description={t("project_settings.states.description")}
|
||||
/>
|
||||
{workspaceSlug && projectId && (
|
||||
<ProjectStateRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||
)}
|
||||
<div className="flex items-center border-b border-custom-border-100">
|
||||
<h3 className="text-xl font-medium">{t("common.states")}</h3>
|
||||
</div>
|
||||
</SettingsContentWrapper>
|
||||
{workspaceSlug && projectId && (
|
||||
<ProjectStateRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
"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}/settings/projects/${projectId}/features`);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
||||
+19
-29
@@ -7,12 +7,12 @@ import useSWR from "swr";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/ui";
|
||||
// component
|
||||
import { ApiTokenListItem, CreateApiTokenModal } from "@/components/api-token";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { DetailedEmptyState } from "@/components/empty-state";
|
||||
import { SettingsHeading } from "@/components/settings";
|
||||
import { APITokenSettingsLoader } from "@/components/ui";
|
||||
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
// store hooks
|
||||
@@ -48,7 +48,7 @@ const ApiTokensPage = observer(() => {
|
||||
: undefined;
|
||||
|
||||
if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
|
||||
return <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
if (!tokens) {
|
||||
@@ -56,20 +56,18 @@ const ApiTokensPage = observer(() => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
|
||||
<section className="w-full">
|
||||
<section className="w-full overflow-y-auto">
|
||||
{tokens.length > 0 ? (
|
||||
<>
|
||||
<SettingsHeading
|
||||
title={t("account_settings.api_tokens.heading")}
|
||||
description={t("account_settings.api_tokens.description")}
|
||||
button={{
|
||||
label: t("workspace_settings.settings.api_tokens.add_token"),
|
||||
onClick: () => setIsCreateTokenModalOpen(true),
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between border-b border-custom-border-200 pb-3.5">
|
||||
<h3 className="text-xl font-medium">{t("workspace_settings.settings.api_tokens.title")}</h3>
|
||||
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
|
||||
{t("workspace_settings.settings.api_tokens.add_token")}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
{tokens.map((token) => (
|
||||
<ApiTokenListItem key={token.id} token={token} />
|
||||
@@ -78,31 +76,23 @@ const ApiTokensPage = observer(() => {
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<SettingsHeading
|
||||
title={t("account_settings.api_tokens.heading")}
|
||||
description={t("account_settings.api_tokens.description")}
|
||||
button={{
|
||||
label: t("workspace_settings.settings.api_tokens.add_token"),
|
||||
onClick: () => setIsCreateTokenModalOpen(true),
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
|
||||
<h3 className="text-xl font-medium">{t("workspace_settings.settings.api_tokens.title")}</h3>
|
||||
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
|
||||
{t("workspace_settings.settings.api_tokens.add_token")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<DetailedEmptyState
|
||||
title=""
|
||||
description=""
|
||||
title={t("workspace_settings.empty_state.api_tokens.title")}
|
||||
description={t("workspace_settings.empty_state.api_tokens.description")}
|
||||
assetPath={resolvedPath}
|
||||
className="w-full !p-0 justify-center mx-auto"
|
||||
size="md"
|
||||
primaryButton={{
|
||||
text: t("workspace_settings.settings.api_tokens.add_token"),
|
||||
onClick: () => setIsCreateTokenModalOpen(true),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
+3
-4
@@ -6,7 +6,6 @@ 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";
|
||||
@@ -20,14 +19,14 @@ const BillingSettingsPage = observer(() => {
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined;
|
||||
|
||||
if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
|
||||
return <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContentWrapper size="lg">
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<BillingRoot />
|
||||
</SettingsContentWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
+7
-9
@@ -8,7 +8,6 @@ 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";
|
||||
@@ -30,24 +29,23 @@ const ExportsPage = observer(() => {
|
||||
|
||||
// if user is not authorized to view this page
|
||||
if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
|
||||
return <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContentWrapper size="lg">
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div
|
||||
className={cn("w-full", {
|
||||
className={cn("w-full overflow-y-auto", {
|
||||
"opacity-60": !canPerformWorkspaceMemberActions,
|
||||
})}
|
||||
>
|
||||
<SettingsHeading
|
||||
title={t("workspace_settings.settings.exports.heading")}
|
||||
description={t("workspace_settings.settings.exports.description")}
|
||||
/>
|
||||
<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>
|
||||
<ExportGuide />
|
||||
</div>
|
||||
</SettingsContentWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
+16
-8
@@ -3,32 +3,40 @@
|
||||
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 <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
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>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsContentWrapper size="lg">
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className="w-full">
|
||||
<SettingsHeading title="Imports" />
|
||||
<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>
|
||||
<IntegrationGuide />
|
||||
</section>
|
||||
</SettingsContentWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
+13
-6
@@ -4,10 +4,8 @@ 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";
|
||||
@@ -28,14 +26,23 @@ 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" />
|
||||
@@ -49,7 +56,7 @@ const WorkspaceIntegrationsPage = observer(() => {
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</SettingsContentWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"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;
|
||||
+4
-5
@@ -14,7 +14,6 @@ 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";
|
||||
@@ -96,11 +95,11 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||
|
||||
// if user is not authorized to view this page
|
||||
if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
|
||||
return <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContentWrapper size="lg">
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<SendWorkspaceInvitationModal
|
||||
isOpen={inviteModal}
|
||||
@@ -108,7 +107,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||
onSubmit={handleWorkspaceInvite}
|
||||
/>
|
||||
<section
|
||||
className={cn("w-full h-full", {
|
||||
className={cn("w-full h-full overflow-y-auto", {
|
||||
"opacity-60": !canPerformWorkspaceMemberActions,
|
||||
})}
|
||||
>
|
||||
@@ -138,7 +137,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||
</div>
|
||||
<WorkspaceMembersList searchQuery={searchQuery} isAdmin={canPerformWorkspaceAdminActions} />
|
||||
</section>
|
||||
</SettingsContentWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user