Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ba77abe61 | |||
| 065bf85a25 | |||
| fcda9c5343 | |||
| 9ff238816b | |||
| 6bd5caf008 | |||
| c021aff58f | |||
| 683be55883 | |||
| 970ce8cf26 | |||
| cbbe1a4e4d | |||
| 6a74677cc9 | |||
| f6ea4f931d | |||
| 950fcfdb40 | |||
| 053c895120 | |||
| 245167e8aa | |||
| 6be3f0ea73 | |||
| 14d2d69120 | |||
| 570a9e319e | |||
| 469a027bb6 | |||
| 8c99a7df88 | |||
| f34f078bd2 | |||
| 0fe2549bc6 | |||
| 118964de01 | |||
| 9f37f1ef0e | |||
| 986f29d1f2 | |||
| 1113f9fc19 | |||
| ef3ec7274c | |||
| a0a45b7916 | |||
| 2792d48288 | |||
| b2ccca0567 | |||
| 2e822b38e4 | |||
| e570fe404f | |||
| 48b613ae66 | |||
| 461e099bbc | |||
| 45e25ce18b | |||
| 4d88dbaf49 | |||
| e61ff879c4 | |||
| adeb7d977d |
@@ -290,5 +290,6 @@ jobs:
|
||||
${{ github.workspace }}/deploy/selfhost/setup.sh
|
||||
${{ github.workspace }}/deploy/selfhost/swarm.sh
|
||||
${{ github.workspace }}/deploy/selfhost/restore.sh
|
||||
${{ github.workspace }}/deploy/selfhost/restore-airgapped.sh
|
||||
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
|
||||
${{ github.workspace }}/deploy/selfhost/variables.env
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"description": "Admin UI for Plane",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"description": "API server powering Plane's backend"
|
||||
|
||||
@@ -58,7 +58,7 @@ from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||
from .base import BaseAPIView
|
||||
from plane.utils.host import base_host
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
|
||||
from plane.bgtasks.work_item_link_task import crawl_work_item_link_title
|
||||
|
||||
class WorkspaceIssueAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
@@ -692,6 +692,9 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
||||
serializer = IssueLinkSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id, issue_id=issue_id)
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
|
||||
link = IssueLink.objects.get(pk=serializer.data["id"])
|
||||
link.created_by_id = request.data.get("created_by", request.user.id)
|
||||
@@ -719,6 +722,9 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
||||
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="link.activity.updated",
|
||||
requested_data=requested_data,
|
||||
|
||||
@@ -45,7 +45,7 @@ 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(
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
issue_activity.delay(
|
||||
@@ -78,7 +78,7 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
crawl_work_item_link_title(
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
|
||||
|
||||
@@ -168,6 +168,8 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
workspace__slug=slug,
|
||||
member__is_bot=False,
|
||||
is_active=True,
|
||||
member__member_workspace__workspace__slug=slug,
|
||||
member__member_workspace__is_active=True,
|
||||
).select_related("project", "member", "workspace")
|
||||
|
||||
serializer = ProjectMemberRoleSerializer(
|
||||
@@ -313,7 +315,11 @@ class UserProjectRolesEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug):
|
||||
project_members = ProjectMember.objects.filter(
|
||||
workspace__slug=slug, member_id=request.user.id, is_active=True
|
||||
workspace__slug=slug,
|
||||
member_id=request.user.id,
|
||||
is_active=True,
|
||||
member__member_workspace__workspace__slug=slug,
|
||||
member__member_workspace__is_active=True,
|
||||
).values("project_id", "role")
|
||||
|
||||
project_members = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Django imports
|
||||
from django.db.models import Count, Q, OuterRef, Subquery, IntegerField
|
||||
from django.utils import timezone
|
||||
from django.db.models.functions import Coalesce
|
||||
|
||||
# Third party modules
|
||||
@@ -133,7 +134,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
# Deactivate the users from the projects where the user is part of
|
||||
_ = ProjectMember.objects.filter(
|
||||
workspace__slug=slug, member_id=workspace_member.member_id, is_active=True
|
||||
).update(is_active=False)
|
||||
).update(is_active=False, updated_at=timezone.now())
|
||||
|
||||
workspace_member.is_active = False
|
||||
workspace_member.save()
|
||||
@@ -194,7 +195,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
# # Deactivate the users from the projects where the user is part of
|
||||
_ = ProjectMember.objects.filter(
|
||||
workspace__slug=slug, member_id=workspace_member.member_id, is_active=True
|
||||
).update(is_active=False)
|
||||
).update(is_active=False, updated_at=timezone.now())
|
||||
|
||||
# # Deactivate the user
|
||||
workspace_member.is_active = False
|
||||
|
||||
@@ -19,17 +19,6 @@ 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.
|
||||
@@ -57,17 +46,18 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
"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)
|
||||
soup = None
|
||||
title = None
|
||||
|
||||
response.raise_for_status()
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=1)
|
||||
|
||||
# Parse HTML
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
title_tag = soup.find("title")
|
||||
title = title_tag.get_text().strip() if title_tag else None
|
||||
|
||||
# Extract title
|
||||
title_tag = soup.find("title")
|
||||
title = title_tag.get_text().strip() if title_tag else None
|
||||
except requests.RequestException as e:
|
||||
logger.warning(f"Failed to fetch HTML for title: {str(e)}")
|
||||
|
||||
# Fetch and encode favicon
|
||||
favicon_base64 = fetch_and_encode_favicon(headers, soup, url)
|
||||
@@ -82,14 +72,6 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
|
||||
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 {
|
||||
@@ -100,7 +82,7 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def find_favicon_url(soup: BeautifulSoup, base_url: str) -> Optional[str]:
|
||||
def find_favicon_url(soup: Optional[BeautifulSoup], base_url: str) -> Optional[str]:
|
||||
"""
|
||||
Find the favicon URL from HTML soup.
|
||||
|
||||
@@ -111,18 +93,20 @@ def find_favicon_url(soup: BeautifulSoup, base_url: str) -> Optional[str]:
|
||||
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"])
|
||||
if soup is not 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)
|
||||
@@ -131,7 +115,6 @@ def find_favicon_url(soup: BeautifulSoup, base_url: str) -> Optional[str]:
|
||||
# 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:
|
||||
@@ -142,8 +125,8 @@ def find_favicon_url(soup: BeautifulSoup, base_url: str) -> Optional[str]:
|
||||
|
||||
|
||||
def fetch_and_encode_favicon(
|
||||
headers: Dict[str, str], soup: BeautifulSoup, url: str
|
||||
) -> Optional[Dict[str, str]]:
|
||||
headers: Dict[str, str], soup: Optional[BeautifulSoup], url: str
|
||||
) -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
Fetch favicon and encode it as base64.
|
||||
|
||||
@@ -162,8 +145,7 @@ def fetch_and_encode_favicon(
|
||||
"favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}",
|
||||
}
|
||||
|
||||
response = requests.get(favicon_url, headers=headers, timeout=2)
|
||||
response.raise_for_status()
|
||||
response = requests.get(favicon_url, headers=headers, timeout=1)
|
||||
|
||||
# Get content type
|
||||
content_type = response.headers.get("content-type", "image/x-icon")
|
||||
@@ -183,3 +165,13 @@ def fetch_and_encode_favicon(
|
||||
"favicon_url": None,
|
||||
"favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}",
|
||||
}
|
||||
|
||||
|
||||
@shared_task
|
||||
def crawl_work_item_link_title(id: str, url: str) -> None:
|
||||
meta_data = crawl_work_item_link_title_and_favicon(url)
|
||||
issue_link = IssueLink.objects.get(id=id)
|
||||
|
||||
issue_link.metadata = meta_data
|
||||
|
||||
issue_link.save()
|
||||
|
||||
@@ -486,7 +486,7 @@ When you want to restore the previously backed-up data, follow the instructions
|
||||
1. Download the restore script using the command below. We suggest downloading it in the same folder as `setup.sh`.
|
||||
|
||||
```bash
|
||||
curl -fsSL -o restore.sh https://raw.githubusercontent.com/makeplane/plane/master/deploy/selfhost/restore.sh
|
||||
curl -fsSL -o restore.sh https://github.com/makeplane/plane/releases/latest/download/restore.sh
|
||||
chmod +x restore.sh
|
||||
```
|
||||
|
||||
@@ -529,6 +529,31 @@ When you want to restore the previously backed-up data, follow the instructions
|
||||
|
||||
---
|
||||
|
||||
### Restore for Commercial Air-Gapped (Docker Compose)
|
||||
|
||||
When you want to restore the previously backed-up data on Plane Commercial Air-Gapped version, follow the instructions below.
|
||||
|
||||
1. Download the restore script using the command below
|
||||
|
||||
```bash
|
||||
curl -fsSL -o restore-airgapped.sh https://github.com/makeplane/plane/releases/latest/download/restore-airgapped.sh
|
||||
chmod +x restore-airgapped.sh
|
||||
```
|
||||
|
||||
1. Copy the backup folder and the `restore-airgapped.sh` to `Commercial Airgapped Edition` server
|
||||
|
||||
1. Make sure that Plane Commercial (Airgapped) is extracted and ready to get started. In case it is running, you would need to stop that.
|
||||
|
||||
1. Execute the command below to restore your data.
|
||||
|
||||
```bash
|
||||
./restore-airgapped.sh <path to backup folder containing *.tar.gz files>
|
||||
```
|
||||
|
||||
1. After restoration, you are ready to start Plane Commercial (Airgapped) will all your previously saved data.
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary><h2>Upgrading from v0.13.2 to v0.14.x</h2></summary>
|
||||
|
||||
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/bin/bash
|
||||
+set -euo pipefail
|
||||
|
||||
function print_header() {
|
||||
clear
|
||||
|
||||
cat <<"EOF"
|
||||
--------------------------------------------
|
||||
____ _ /////////
|
||||
| _ \| | __ _ _ __ ___ /////////
|
||||
| |_) | |/ _` | '_ \ / _ \ ///// /////
|
||||
| __/| | (_| | | | | __/ ///// /////
|
||||
|_| |_|\__,_|_| |_|\___| ////
|
||||
////
|
||||
--------------------------------------------
|
||||
Project management tool from the future
|
||||
--------------------------------------------
|
||||
EOF
|
||||
}
|
||||
|
||||
function restoreData() {
|
||||
|
||||
echo ""
|
||||
echo "****************************************************"
|
||||
echo "We are about to restore your data from the backup files."
|
||||
echo "****************************************************"
|
||||
echo ""
|
||||
|
||||
# set the backup folder path
|
||||
BACKUP_FOLDER=${1}
|
||||
|
||||
if [ -z "$BACKUP_FOLDER" ]; then
|
||||
BACKUP_FOLDER="$PWD/backup"
|
||||
read -p "Enter the backup folder path [$BACKUP_FOLDER]: " BACKUP_FOLDER
|
||||
if [ -z "$BACKUP_FOLDER" ]; then
|
||||
BACKUP_FOLDER="$PWD/backup"
|
||||
fi
|
||||
fi
|
||||
|
||||
# check if the backup folder exists
|
||||
if [ ! -d "$BACKUP_FOLDER" ]; then
|
||||
echo "Error: Backup folder not found at $BACKUP_FOLDER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if there are any .tar.gz files in the backup folder
|
||||
if ! ls "$BACKUP_FOLDER"/*.tar.gz 1> /dev/null 2>&1; then
|
||||
echo "Error: Backup folder does not contain .tar.gz files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Using backup folder: $BACKUP_FOLDER"
|
||||
echo ""
|
||||
|
||||
# ask for current install path
|
||||
AIRGAPPED_INSTALL_PATH="$HOME/planeairgapped"
|
||||
read -p "Enter the airgapped instance install path [$AIRGAPPED_INSTALL_PATH]: " AIRGAPPED_INSTALL_PATH
|
||||
if [ -z "$AIRGAPPED_INSTALL_PATH" ]; then
|
||||
AIRGAPPED_INSTALL_PATH="$HOME/planeairgapped"
|
||||
fi
|
||||
|
||||
# check if the airgapped instance install path exists
|
||||
if [ ! -d "$AIRGAPPED_INSTALL_PATH" ]; then
|
||||
echo "Error: Airgapped instance install path not found at $AIRGAPPED_INSTALL_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Using airgapped instance install path: $AIRGAPPED_INSTALL_PATH"
|
||||
echo ""
|
||||
|
||||
# check if the docker-compose.yaml exists
|
||||
if [ ! -f "$AIRGAPPED_INSTALL_PATH/docker-compose.yml" ]; then
|
||||
echo "Error: docker-compose.yml not found at $AIRGAPPED_INSTALL_PATH/docker-compose.yml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local dockerServiceStatus
|
||||
if command -v jq &> /dev/null; then
|
||||
dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-airgapped --format=json | jq -r .[0].Status)
|
||||
else
|
||||
dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-airgapped | grep -o "running" | head -n 1)
|
||||
fi
|
||||
|
||||
if [[ $dockerServiceStatus == "running" ]]; then
|
||||
echo "Plane Airgapped is running. Please STOP the Plane Airgapped before restoring data."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_USER_ID=$(id -u)
|
||||
CURRENT_GROUP_ID=$(id -g)
|
||||
|
||||
# if the data folder not exists, create it
|
||||
if [ ! -d "$AIRGAPPED_INSTALL_PATH/data" ]; then
|
||||
mkdir -p "$AIRGAPPED_INSTALL_PATH/data"
|
||||
chown -R $CURRENT_USER_ID:$CURRENT_GROUP_ID "$AIRGAPPED_INSTALL_PATH/data"
|
||||
fi
|
||||
|
||||
for BACKUP_FILE in "$BACKUP_FOLDER/*.tar.gz"; do
|
||||
if [ -e "$BACKUP_FILE" ]; then
|
||||
|
||||
# get the basefilename without the extension
|
||||
BASE_FILE_NAME=$(basename "$BACKUP_FILE" ".tar.gz")
|
||||
|
||||
# extract the restoreFile to the airgapped instance install path
|
||||
echo "Restoring $BASE_FILE_NAME"
|
||||
rm -rf "$AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME" || true
|
||||
|
||||
tar -xvzf "$BACKUP_FILE" -C "$AIRGAPPED_INSTALL_PATH/data/"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to extract $BACKUP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
chown -R $CURRENT_USER_ID:$CURRENT_GROUP_ID "$AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to change ownership of $AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No .tar.gz files found in the current directory."
|
||||
echo ""
|
||||
echo "Please provide the path to the backup file."
|
||||
echo ""
|
||||
echo "Usage: $0 /path/to/backup"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Restore completed successfully."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# if docker-compose is installed
|
||||
if command -v docker-compose &> /dev/null
|
||||
then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
COMPOSE_CMD="docker compose"
|
||||
fi
|
||||
|
||||
print_header
|
||||
restoreData "$@"
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "live",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "A realtime collaborative server powers Plane's rich text editor",
|
||||
"main": "./src/server.ts",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "plane",
|
||||
"description": "Open-source project management that unlocks customer value",
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"license": "AGPL-3.0"
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { TAnalyticsTabsV2Base } from "@plane/types";
|
||||
import { ChartXAxisProperty, ChartYAxisMetric } from "../chart";
|
||||
|
||||
export const insightsFields: Record<TAnalyticsTabsV2Base, string[]> = {
|
||||
overview: [
|
||||
"total_users",
|
||||
"total_admins",
|
||||
"total_members",
|
||||
"total_guests",
|
||||
"total_projects",
|
||||
"total_work_items",
|
||||
"total_cycles",
|
||||
"total_intake",
|
||||
],
|
||||
"work-items": [
|
||||
"total_work_items",
|
||||
"started_work_items",
|
||||
"backlog_work_items",
|
||||
"un_started_work_items",
|
||||
"completed_work_items",
|
||||
],
|
||||
};
|
||||
|
||||
export const ANALYTICS_V2_DURATION_FILTER_OPTIONS = [
|
||||
{
|
||||
name: "Yesterday",
|
||||
value: "yesterday",
|
||||
},
|
||||
{
|
||||
name: "Last 7 days",
|
||||
value: "last_7_days",
|
||||
},
|
||||
{
|
||||
name: "Last 30 days",
|
||||
value: "last_30_days",
|
||||
},
|
||||
{
|
||||
name: "Last 3 months",
|
||||
value: "last_3_months",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_V2_X_AXIS_VALUES: { value: ChartXAxisProperty; label: string }[] = [
|
||||
{
|
||||
value: ChartXAxisProperty.STATES,
|
||||
label: "State name",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.STATE_GROUPS,
|
||||
label: "State group",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.PRIORITY,
|
||||
label: "Priority",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.LABELS,
|
||||
label: "Label",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.ASSIGNEES,
|
||||
label: "Assignee",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.ESTIMATE_POINTS,
|
||||
label: "Estimate point",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.CYCLES,
|
||||
label: "Cycle",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.MODULES,
|
||||
label: "Module",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.COMPLETED_AT,
|
||||
label: "Completed date",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.TARGET_DATE,
|
||||
label: "Due date",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.START_DATE,
|
||||
label: "Start date",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.CREATED_AT,
|
||||
label: "Created date",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_V2_Y_AXIS_VALUES: { value: ChartYAxisMetric; label: string }[] = [
|
||||
{
|
||||
value: ChartYAxisMetric.WORK_ITEM_COUNT,
|
||||
label: "Work item",
|
||||
},
|
||||
{
|
||||
value: ChartYAxisMetric.ESTIMATE_POINT_COUNT,
|
||||
label: "Estimate",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_V2_DATE_KEYS = ["completed_at", "target_date", "start_date", "created_at"];
|
||||
@@ -1,81 +0,0 @@
|
||||
// types
|
||||
import { TXAxisValues, TYAxisValues } from "@plane/types";
|
||||
|
||||
export const ANALYTICS_TABS = [
|
||||
{
|
||||
key: "scope_and_demand",
|
||||
i18n_title: "workspace_analytics.tabs.scope_and_demand",
|
||||
},
|
||||
{ key: "custom", i18n_title: "workspace_analytics.tabs.custom" },
|
||||
];
|
||||
|
||||
export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
|
||||
[
|
||||
{
|
||||
value: "state_id",
|
||||
label: "State name",
|
||||
},
|
||||
{
|
||||
value: "state__group",
|
||||
label: "State group",
|
||||
},
|
||||
{
|
||||
value: "priority",
|
||||
label: "Priority",
|
||||
},
|
||||
{
|
||||
value: "labels__id",
|
||||
label: "Label",
|
||||
},
|
||||
{
|
||||
value: "assignees__id",
|
||||
label: "Assignee",
|
||||
},
|
||||
{
|
||||
value: "estimate_point__value",
|
||||
label: "Estimate point",
|
||||
},
|
||||
{
|
||||
value: "issue_cycle__cycle_id",
|
||||
label: "Cycle",
|
||||
},
|
||||
{
|
||||
value: "issue_module__module_id",
|
||||
label: "Module",
|
||||
},
|
||||
{
|
||||
value: "completed_at",
|
||||
label: "Completed date",
|
||||
},
|
||||
{
|
||||
value: "target_date",
|
||||
label: "Due date",
|
||||
},
|
||||
{
|
||||
value: "start_date",
|
||||
label: "Start date",
|
||||
},
|
||||
{
|
||||
value: "created_at",
|
||||
label: "Created date",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] =
|
||||
[
|
||||
{
|
||||
value: "issue_count",
|
||||
label: "Work item Count",
|
||||
},
|
||||
{
|
||||
value: "estimate",
|
||||
label: "Estimate",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_DATE_KEYS = [
|
||||
"completed_at",
|
||||
"target_date",
|
||||
"start_date",
|
||||
"created_at",
|
||||
];
|
||||
@@ -0,0 +1,178 @@
|
||||
import { TAnalyticsTabsBase } from "@plane/types";
|
||||
import { ChartXAxisProperty, ChartYAxisMetric } from "../chart";
|
||||
|
||||
export interface IInsightField {
|
||||
key: string;
|
||||
i18nKey: string;
|
||||
i18nProps?: {
|
||||
entity?: string;
|
||||
entityPlural?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export const insightsFields: Record<TAnalyticsTabsBase, IInsightField[]> = {
|
||||
overview: [
|
||||
{
|
||||
key: "total_users",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.users",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_admins",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.admins",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_members",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.members",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_guests",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.guests",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_projects",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.projects",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_work_items",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.work_items",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_cycles",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.cycles",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_intake",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "sidebar.intake",
|
||||
},
|
||||
},
|
||||
],
|
||||
"work-items": [
|
||||
{
|
||||
key: "total_work_items",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
},
|
||||
{
|
||||
key: "started_work_items",
|
||||
i18nKey: "workspace_analytics.started_work_items",
|
||||
},
|
||||
{
|
||||
key: "backlog_work_items",
|
||||
i18nKey: "workspace_analytics.backlog_work_items",
|
||||
},
|
||||
{
|
||||
key: "un_started_work_items",
|
||||
i18nKey: "workspace_analytics.un_started_work_items",
|
||||
},
|
||||
{
|
||||
key: "completed_work_items",
|
||||
i18nKey: "workspace_analytics.completed_work_items",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const ANALYTICS_DURATION_FILTER_OPTIONS = [
|
||||
{
|
||||
name: "Yesterday",
|
||||
value: "yesterday",
|
||||
},
|
||||
{
|
||||
name: "Last 7 days",
|
||||
value: "last_7_days",
|
||||
},
|
||||
{
|
||||
name: "Last 30 days",
|
||||
value: "last_30_days",
|
||||
},
|
||||
{
|
||||
name: "Last 3 months",
|
||||
value: "last_3_months",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_X_AXIS_VALUES: { value: ChartXAxisProperty; label: string }[] = [
|
||||
{
|
||||
value: ChartXAxisProperty.STATES,
|
||||
label: "State name",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.STATE_GROUPS,
|
||||
label: "State group",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.PRIORITY,
|
||||
label: "Priority",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.LABELS,
|
||||
label: "Label",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.ASSIGNEES,
|
||||
label: "Assignee",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.ESTIMATE_POINTS,
|
||||
label: "Estimate point",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.CYCLES,
|
||||
label: "Cycle",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.MODULES,
|
||||
label: "Module",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.COMPLETED_AT,
|
||||
label: "Completed date",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.TARGET_DATE,
|
||||
label: "Due date",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.START_DATE,
|
||||
label: "Start date",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.CREATED_AT,
|
||||
label: "Created date",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_Y_AXIS_VALUES: { value: ChartYAxisMetric; label: string }[] = [
|
||||
{
|
||||
value: ChartYAxisMetric.WORK_ITEM_COUNT,
|
||||
label: "Work item",
|
||||
},
|
||||
{
|
||||
value: ChartYAxisMetric.ESTIMATE_POINT_COUNT,
|
||||
label: "Estimate",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_V2_DATE_KEYS = ["completed_at", "target_date", "start_date", "created_at"];
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from "./ai";
|
||||
export * from "./analytics";
|
||||
export * from "./auth";
|
||||
export * from "./chart";
|
||||
export * from "./endpoints";
|
||||
@@ -34,4 +33,4 @@ export * from "./emoji";
|
||||
export * from "./subscription";
|
||||
export * from "./settings";
|
||||
export * from "./icon";
|
||||
export * from "./analytics-v2";
|
||||
export * from "./analytics";
|
||||
|
||||
@@ -72,23 +72,23 @@ export const PLANE_COMMUNITY_PRODUCTS: Record<string, IPaymentProduct> = {
|
||||
prices: [
|
||||
{
|
||||
id: `price_yearly_${EProductSubscriptionEnum.BUSINESS}`,
|
||||
unit_amount: 0,
|
||||
unit_amount: 15600,
|
||||
recurring: "year",
|
||||
currency: "usd",
|
||||
workspace_amount: 0,
|
||||
workspace_amount: 15600,
|
||||
product: EProductSubscriptionEnum.BUSINESS,
|
||||
},
|
||||
{
|
||||
id: `price_monthly_${EProductSubscriptionEnum.BUSINESS}`,
|
||||
unit_amount: 0,
|
||||
unit_amount: 1500,
|
||||
recurring: "month",
|
||||
currency: "usd",
|
||||
workspace_amount: 0,
|
||||
workspace_amount: 1500,
|
||||
product: EProductSubscriptionEnum.BUSINESS,
|
||||
},
|
||||
],
|
||||
payment_quantity: 1,
|
||||
is_active: false,
|
||||
is_active: true,
|
||||
},
|
||||
[EProductSubscriptionEnum.ENTERPRISE]: {
|
||||
id: EProductSubscriptionEnum.ENTERPRISE,
|
||||
@@ -141,8 +141,8 @@ export const SUBSCRIPTION_REDIRECTION_URLS: Record<EProductSubscriptionEnum, Rec
|
||||
year: "https://app.plane.so/upgrade/pro/self-hosted?plan=year",
|
||||
},
|
||||
[EProductSubscriptionEnum.BUSINESS]: {
|
||||
month: TALK_TO_SALES_URL,
|
||||
year: TALK_TO_SALES_URL,
|
||||
month: "https://app.plane.so/upgrade/business/self-hosted?plan=month",
|
||||
year: "https://app.plane.so/upgrade/business/self-hosted?plan=year",
|
||||
},
|
||||
[EProductSubscriptionEnum.ENTERPRISE]: {
|
||||
month: TALK_TO_SALES_URL,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
|
||||
@@ -2,30 +2,31 @@ import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||
import { AnyExtension } from "@tiptap/core";
|
||||
import { SlashCommands } from "@/extensions";
|
||||
// plane editor types
|
||||
import { TIssueEmbedConfig } from "@/plane-editor/types";
|
||||
import { TEmbedConfig } from "@/plane-editor/types";
|
||||
// types
|
||||
import { TExtensions, TUserDetails } from "@/types";
|
||||
import { TExtensions, TFileHandler, TUserDetails } from "@/types";
|
||||
|
||||
type Props = {
|
||||
disabledExtensions?: TExtensions[];
|
||||
issueEmbedConfig: TIssueEmbedConfig | undefined;
|
||||
provider: HocuspocusProvider;
|
||||
export type TDocumentEditorAdditionalExtensionsProps = {
|
||||
disabledExtensions: TExtensions[];
|
||||
embedConfig: TEmbedConfig | undefined;
|
||||
fileHandler: TFileHandler;
|
||||
provider?: HocuspocusProvider;
|
||||
userDetails: TUserDetails;
|
||||
};
|
||||
|
||||
type ExtensionConfig = {
|
||||
export type TDocumentEditorAdditionalExtensionsRegistry = {
|
||||
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
||||
getExtension: (props: Props) => AnyExtension;
|
||||
getExtension: (props: TDocumentEditorAdditionalExtensionsProps) => AnyExtension;
|
||||
};
|
||||
|
||||
const extensionRegistry: ExtensionConfig[] = [
|
||||
const extensionRegistry: TDocumentEditorAdditionalExtensionsRegistry[] = [
|
||||
{
|
||||
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
|
||||
getExtension: () => SlashCommands({}),
|
||||
getExtension: ({ disabledExtensions }) => SlashCommands({ disabledExtensions }),
|
||||
},
|
||||
];
|
||||
|
||||
export const DocumentEditorAdditionalExtensions = (_props: Props) => {
|
||||
export const DocumentEditorAdditionalExtensions = (_props: TDocumentEditorAdditionalExtensionsProps) => {
|
||||
const { disabledExtensions = [] } = _props;
|
||||
|
||||
const documentExtensions = extensionRegistry
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { AnyExtension, Extensions } from "@tiptap/core";
|
||||
// extensions
|
||||
import { SlashCommands } from "@/extensions/slash-commands/root";
|
||||
// types
|
||||
import { TExtensions, TFileHandler } from "@/types";
|
||||
|
||||
export type TRichTextEditorAdditionalExtensionsProps = {
|
||||
disabledExtensions: TExtensions[];
|
||||
fileHandler: TFileHandler;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registry entry configuration for extensions
|
||||
*/
|
||||
export type TRichTextEditorAdditionalExtensionsRegistry = {
|
||||
/** Determines if the extension should be enabled based on disabled extensions */
|
||||
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
||||
/** Returns the extension instance(s) when enabled */
|
||||
getExtension: (props: TRichTextEditorAdditionalExtensionsProps) => AnyExtension | undefined;
|
||||
};
|
||||
|
||||
const extensionRegistry: TRichTextEditorAdditionalExtensionsRegistry[] = [
|
||||
{
|
||||
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
|
||||
getExtension: ({ disabledExtensions }) =>
|
||||
SlashCommands({
|
||||
disabledExtensions,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export const RichTextEditorAdditionalExtensions = (props: TRichTextEditorAdditionalExtensionsProps) => {
|
||||
const { disabledExtensions } = props;
|
||||
|
||||
const extensions: Extensions = extensionRegistry
|
||||
.filter((config) => config.isEnabled(disabledExtensions))
|
||||
.map((config) => config.getExtension(props))
|
||||
.filter((extension): extension is AnyExtension => extension !== undefined);
|
||||
|
||||
return extensions;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { AnyExtension, Extensions } from "@tiptap/core";
|
||||
// types
|
||||
import { TExtensions, TReadOnlyFileHandler } from "@/types";
|
||||
|
||||
export type TRichTextReadOnlyEditorAdditionalExtensionsProps = {
|
||||
disabledExtensions: TExtensions[];
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registry entry configuration for extensions
|
||||
*/
|
||||
export type TRichTextReadOnlyEditorAdditionalExtensionsRegistry = {
|
||||
/** Determines if the extension should be enabled based on disabled extensions */
|
||||
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
||||
/** Returns the extension instance(s) when enabled */
|
||||
getExtension: (props: TRichTextReadOnlyEditorAdditionalExtensionsProps) => AnyExtension | undefined;
|
||||
};
|
||||
|
||||
const extensionRegistry: TRichTextReadOnlyEditorAdditionalExtensionsRegistry[] = [];
|
||||
|
||||
export const RichTextReadOnlyEditorAdditionalExtensions = (props: TRichTextReadOnlyEditorAdditionalExtensionsProps) => {
|
||||
const { disabledExtensions } = props;
|
||||
|
||||
const extensions: Extensions = extensionRegistry
|
||||
.filter((config) => config.isEnabled(disabledExtensions))
|
||||
.map((config) => config.getExtension(props))
|
||||
.filter((extension): extension is AnyExtension => extension !== undefined);
|
||||
|
||||
return extensions;
|
||||
};
|
||||
@@ -15,6 +15,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||
disabledExtensions,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editorClassName = "",
|
||||
extensions,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
id,
|
||||
@@ -25,6 +26,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||
const editor = useReadOnlyEditor({
|
||||
disabledExtensions,
|
||||
editorClassName,
|
||||
extensions,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
initialValue,
|
||||
|
||||
@@ -3,12 +3,20 @@ import { forwardRef, useCallback } from "react";
|
||||
import { EditorWrapper } from "@/components/editors";
|
||||
import { EditorBubbleMenu } from "@/components/menus";
|
||||
// extensions
|
||||
import { SideMenuExtension, SlashCommands } from "@/extensions";
|
||||
import { SideMenuExtension } from "@/extensions";
|
||||
// plane editor imports
|
||||
import { RichTextEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/extensions";
|
||||
// types
|
||||
import { EditorRefApi, IRichTextEditor } from "@/types";
|
||||
|
||||
const RichTextEditor = (props: IRichTextEditor) => {
|
||||
const { disabledExtensions, dragDropEnabled, bubbleMenuEnabled = true, extensions: externalExtensions = [] } = props;
|
||||
const {
|
||||
disabledExtensions,
|
||||
dragDropEnabled,
|
||||
fileHandler,
|
||||
bubbleMenuEnabled = true,
|
||||
extensions: externalExtensions = [],
|
||||
} = props;
|
||||
|
||||
const getExtensions = useCallback(() => {
|
||||
const extensions = [
|
||||
@@ -17,17 +25,14 @@ const RichTextEditor = (props: IRichTextEditor) => {
|
||||
aiEnabled: false,
|
||||
dragDropEnabled: !!dragDropEnabled,
|
||||
}),
|
||||
...RichTextEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
fileHandler,
|
||||
}),
|
||||
];
|
||||
if (!disabledExtensions?.includes("slash-commands")) {
|
||||
extensions.push(
|
||||
SlashCommands({
|
||||
disabledExtensions,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}, [dragDropEnabled, disabledExtensions, externalExtensions]);
|
||||
}, [dragDropEnabled, disabledExtensions, externalExtensions, fileHandler]);
|
||||
|
||||
return (
|
||||
<EditorWrapper {...props} extensions={getExtensions()}>
|
||||
|
||||
@@ -1,11 +1,33 @@
|
||||
import { forwardRef } from "react";
|
||||
import { forwardRef, useCallback } from "react";
|
||||
// plane editor extensions
|
||||
import { RichTextReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/read-only-extensions";
|
||||
// types
|
||||
import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor } from "@/types";
|
||||
// local imports
|
||||
import { ReadOnlyEditorWrapper } from "../read-only-editor-wrapper";
|
||||
|
||||
const RichTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IRichTextReadOnlyEditor>((props, ref) => (
|
||||
<ReadOnlyEditorWrapper {...props} forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>} />
|
||||
));
|
||||
const RichTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IRichTextReadOnlyEditor>((props, ref) => {
|
||||
const { disabledExtensions, fileHandler } = props;
|
||||
|
||||
const getExtensions = useCallback(() => {
|
||||
const extensions = [
|
||||
...RichTextReadOnlyEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
fileHandler,
|
||||
}),
|
||||
];
|
||||
|
||||
return extensions;
|
||||
}, [disabledExtensions, fileHandler]);
|
||||
|
||||
return (
|
||||
<ReadOnlyEditorWrapper
|
||||
{...props}
|
||||
extensions={getExtensions()}
|
||||
forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
RichTextReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef";
|
||||
|
||||
|
||||
@@ -170,8 +170,9 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutExtension,
|
||||
UtilityExtension({
|
||||
isEditable: editable,
|
||||
disabledExtensions,
|
||||
fileHandler,
|
||||
isEditable: editable,
|
||||
}),
|
||||
CustomColorExtension,
|
||||
...CoreEditorAdditionalExtensions({
|
||||
|
||||
@@ -127,8 +127,9 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutReadOnlyExtension,
|
||||
UtilityExtension({
|
||||
isEditable: false,
|
||||
disabledExtensions,
|
||||
fileHandler,
|
||||
isEditable: false,
|
||||
}),
|
||||
...CoreReadOnlyEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DropHandlerPlugin } from "@/plugins/drop";
|
||||
import { FilePlugins } from "@/plugins/file/root";
|
||||
import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
|
||||
// types
|
||||
import { TFileHandler, TReadOnlyFileHandler } from "@/types";
|
||||
import { TExtensions, TFileHandler, TReadOnlyFileHandler } from "@/types";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands {
|
||||
@@ -24,13 +24,14 @@ export interface UtilityExtensionStorage {
|
||||
}
|
||||
|
||||
type Props = {
|
||||
disabledExtensions: TExtensions[];
|
||||
fileHandler: TFileHandler | TReadOnlyFileHandler;
|
||||
isEditable: boolean;
|
||||
};
|
||||
|
||||
export const UtilityExtension = (props: Props) => {
|
||||
const { fileHandler, isEditable } = props;
|
||||
const { restore: restoreImageFn } = fileHandler;
|
||||
const { disabledExtensions, fileHandler, isEditable } = props;
|
||||
const { restore } = fileHandler;
|
||||
|
||||
return Extension.create<Record<string, unknown>, UtilityExtensionStorage>({
|
||||
name: "utility",
|
||||
@@ -45,12 +46,15 @@ export const UtilityExtension = (props: Props) => {
|
||||
}),
|
||||
...codemark({ markType: this.editor.schema.marks.code }),
|
||||
MarkdownClipboardPlugin(this.editor),
|
||||
DropHandlerPlugin(this.editor),
|
||||
DropHandlerPlugin({
|
||||
disabledExtensions,
|
||||
editor: this.editor,
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
||||
onCreate() {
|
||||
restorePublicImages(this.editor, restoreImageFn);
|
||||
restorePublicImages(this.editor, restore);
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
|
||||
@@ -92,7 +92,8 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
|
||||
...(extensions ?? []),
|
||||
...DocumentEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
issueEmbedConfig: embedHandler?.issue,
|
||||
embedConfig: embedHandler,
|
||||
fileHandler,
|
||||
provider,
|
||||
userDetails: user,
|
||||
}),
|
||||
|
||||
@@ -3,10 +3,17 @@ import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
// constants
|
||||
import { ACCEPTED_ATTACHMENT_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config";
|
||||
// types
|
||||
import { TEditorCommands } from "@/types";
|
||||
import { TEditorCommands, TExtensions } from "@/types";
|
||||
|
||||
export const DropHandlerPlugin = (editor: Editor): Plugin =>
|
||||
new Plugin({
|
||||
type Props = {
|
||||
disabledExtensions?: TExtensions[];
|
||||
editor: Editor;
|
||||
};
|
||||
|
||||
export const DropHandlerPlugin = (props: Props): Plugin => {
|
||||
const { disabledExtensions, editor } = props;
|
||||
|
||||
return new Plugin({
|
||||
key: new PluginKey("drop-handler-plugin"),
|
||||
props: {
|
||||
handlePaste: (view, event) => {
|
||||
@@ -25,6 +32,7 @@ export const DropHandlerPlugin = (editor: Editor): Plugin =>
|
||||
if (acceptedFiles.length) {
|
||||
const pos = view.state.selection.from;
|
||||
insertFilesSafely({
|
||||
disabledExtensions,
|
||||
editor,
|
||||
files: acceptedFiles,
|
||||
initialPos: pos,
|
||||
@@ -58,6 +66,7 @@ export const DropHandlerPlugin = (editor: Editor): Plugin =>
|
||||
if (coordinates) {
|
||||
const pos = coordinates.pos;
|
||||
insertFilesSafely({
|
||||
disabledExtensions,
|
||||
editor,
|
||||
files: acceptedFiles,
|
||||
initialPos: pos,
|
||||
@@ -71,8 +80,10 @@ export const DropHandlerPlugin = (editor: Editor): Plugin =>
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
type InsertFilesSafelyArgs = {
|
||||
disabledExtensions?: TExtensions[];
|
||||
editor: Editor;
|
||||
event: "insert" | "drop";
|
||||
files: File[];
|
||||
@@ -81,7 +92,7 @@ type InsertFilesSafelyArgs = {
|
||||
};
|
||||
|
||||
export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => {
|
||||
const { editor, event, files, initialPos, type } = args;
|
||||
const { disabledExtensions, editor, event, files, initialPos, type } = args;
|
||||
let pos = initialPos;
|
||||
|
||||
for (const file of files) {
|
||||
@@ -100,7 +111,7 @@ export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => {
|
||||
else if (ACCEPTED_ATTACHMENT_MIME_TYPES.includes(file.type)) fileType = "attachment";
|
||||
}
|
||||
// insert file depending on the type at the current position
|
||||
if (fileType === "image") {
|
||||
if (fileType === "image" && !disabledExtensions?.includes("image")) {
|
||||
editor.commands.insertImageComponent({
|
||||
file,
|
||||
pos,
|
||||
|
||||
@@ -160,6 +160,7 @@ export interface IReadOnlyEditorProps {
|
||||
disabledExtensions: TExtensions[];
|
||||
displayConfig?: TDisplayConfig;
|
||||
editorClassName?: string;
|
||||
extensions?: Extensions;
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
id: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@plane/eslint-config",
|
||||
"private": true,
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"files": [
|
||||
"library.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/hooks",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "React hooks that are shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/i18n",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "I18n shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
||||
@@ -31,6 +31,7 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
|
||||
export enum ETranslationFiles {
|
||||
TRANSLATIONS = "translations",
|
||||
ACCESSIBILITY = "accessibility",
|
||||
EDITOR = "editor",
|
||||
}
|
||||
|
||||
export const LANGUAGE_STORAGE_KEY = "userLanguage";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -866,7 +866,19 @@
|
||||
"view": "Pohled",
|
||||
"deactivated_user": "Deaktivovaný uživatel",
|
||||
"apply": "Použít",
|
||||
"applying": "Používání"
|
||||
"applying": "Používání",
|
||||
"users": "Uživatelé",
|
||||
"admins": "Administrátoři",
|
||||
"guests": "Hosté",
|
||||
"on_track": "Na správné cestě",
|
||||
"off_track": "Mimo plán",
|
||||
"timeline": "Časová osa",
|
||||
"completion": "Dokončení",
|
||||
"upcoming": "Nadcházející",
|
||||
"completed": "Dokončeno",
|
||||
"in_progress": "Probíhá",
|
||||
"planned": "Plánováno",
|
||||
"paused": "Pozastaveno"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Osa X",
|
||||
@@ -1316,19 +1328,6 @@
|
||||
"custom": "Vlastní analytika"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Sledujte pokrok, vytížení a alokace. Identifikujte trendy, odstraňte překážky a zrychlete práci",
|
||||
"description": "Sledujte rozsah vs. poptávku, odhady a rozsah. Zjistěte výkonnost členů a týmů, zajistěte včasné dokončení projektů.",
|
||||
"primary_button": {
|
||||
"text": "Začněte první projekt",
|
||||
"comic": {
|
||||
"title": "Analytika funguje nejlépe s Cykly + Moduly",
|
||||
"description": "Nejprve časově ohraničte práci do Cyklů a seskupte položky přesahující cyklus do Modulů. Najdete je v levém menu."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Pracovní položky přiřazené vám, rozdělené podle stavu, se zde zobrazí.",
|
||||
"title": "Zatím žádná data"
|
||||
@@ -1344,21 +1343,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Vytvořeno vs Vyřešeno",
|
||||
"customized_insights": "Přizpůsobené přehledy",
|
||||
"backlog_work_items": "Pracovní položky v backlogu",
|
||||
"backlog_work_items": "Backlog {entity}",
|
||||
"active_projects": "Aktivní projekty",
|
||||
"trend_on_charts": "Trend na grafech",
|
||||
"all_projects": "Všechny projekty",
|
||||
"summary_of_projects": "Souhrn projektů",
|
||||
"project_insights": "Přehled projektu",
|
||||
"started_work_items": "Zahájené pracovní položky",
|
||||
"total_work_items": "Celkový počet pracovních položek",
|
||||
"started_work_items": "Zahájené {entity}",
|
||||
"total_work_items": "Celkový počet {entity}",
|
||||
"total_projects": "Celkový počet projektů",
|
||||
"total_admins": "Celkový počet administrátorů",
|
||||
"total_users": "Celkový počet uživatelů",
|
||||
"total_intake": "Celkový příjem",
|
||||
"un_started_work_items": "Nezahájené pracovní položky",
|
||||
"un_started_work_items": "Nezahájené {entity}",
|
||||
"total_guests": "Celkový počet hostů",
|
||||
"completed_work_items": "Dokončené pracovní položky"
|
||||
"completed_work_items": "Dokončené {entity}",
|
||||
"total": "Celkový počet {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {Projekt} few {Projekty} other {Projektů}}",
|
||||
@@ -2462,5 +2462,9 @@
|
||||
"last_edited_by": "Naposledy upraveno uživatelem",
|
||||
"previously_edited_by": "Dříve upraveno uživatelem",
|
||||
"edited_by": "Upraveno uživatelem"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane se nespustil. To může být způsobeno tím, že se jeden nebo více služeb Plane nepodařilo spustit.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Vyberte View Logs z setup.sh a Docker logů, abyste si byli jisti."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -866,7 +866,19 @@
|
||||
"view": "Ansicht",
|
||||
"deactivated_user": "Deaktivierter Benutzer",
|
||||
"apply": "Anwenden",
|
||||
"applying": "Wird angewendet"
|
||||
"applying": "Wird angewendet",
|
||||
"users": "Benutzer",
|
||||
"admins": "Administratoren",
|
||||
"guests": "Gäste",
|
||||
"on_track": "Im Plan",
|
||||
"off_track": "Außer Plan",
|
||||
"timeline": "Zeitleiste",
|
||||
"completion": "Fertigstellung",
|
||||
"upcoming": "Bevorstehend",
|
||||
"completed": "Abgeschlossen",
|
||||
"in_progress": "In Bearbeitung",
|
||||
"planned": "Geplant",
|
||||
"paused": "Pausiert"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "X-Achse",
|
||||
@@ -1316,19 +1328,6 @@
|
||||
"custom": "Benutzerdefinierte Analysen"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Verfolgen Sie Fortschritt, Auslastung und Zuordnungen. Erkennen Sie Trends, entfernen Sie Blocker und beschleunigen Sie die Arbeit",
|
||||
"description": "Behalten Sie Umfang vs. Nachfrage, Schätzungen und Umfang im Blick. Verfolgen Sie die Leistung von Mitgliedern und Teams, um sicherzustellen, dass Projekte pünktlich abgeschlossen werden.",
|
||||
"primary_button": {
|
||||
"text": "Erstes Projekt starten",
|
||||
"comic": {
|
||||
"title": "Analysen funktionieren am besten mit Zyklen + Modulen",
|
||||
"description": "Begrenzen Sie zuerst Arbeit zeitlich in Zyklen und gruppieren Sie die übergreifenden Elemente in Module. Sie finden sie im linken Menü."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Ihnen zugewiesene Arbeitselemente, aufgeschlüsselt nach Status, werden hier angezeigt.",
|
||||
"title": "Noch keine Daten"
|
||||
@@ -1344,21 +1343,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Erstellt vs Gelöst",
|
||||
"customized_insights": "Individuelle Einblicke",
|
||||
"backlog_work_items": "Backlog-Arbeitselemente",
|
||||
"backlog_work_items": "Backlog-{entity}",
|
||||
"active_projects": "Aktive Projekte",
|
||||
"trend_on_charts": "Trend in Diagrammen",
|
||||
"all_projects": "Alle Projekte",
|
||||
"summary_of_projects": "Projektübersicht",
|
||||
"project_insights": "Projekteinblicke",
|
||||
"started_work_items": "Begonnene Arbeitselemente",
|
||||
"total_work_items": "Gesamte Arbeitselemente",
|
||||
"started_work_items": "Begonnene {entity}",
|
||||
"total_work_items": "Gesamte {entity}",
|
||||
"total_projects": "Gesamtprojekte",
|
||||
"total_admins": "Gesamtanzahl der Admins",
|
||||
"total_users": "Gesamtanzahl der Benutzer",
|
||||
"total_intake": "Gesamteinnahmen",
|
||||
"un_started_work_items": "Nicht begonnene Arbeitselemente",
|
||||
"un_started_work_items": "Nicht begonnene {entity}",
|
||||
"total_guests": "Gesamtanzahl der Gäste",
|
||||
"completed_work_items": "Abgeschlossene Arbeitselemente"
|
||||
"completed_work_items": "Abgeschlossene {entity}",
|
||||
"total": "Gesamte {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {Projekt} few {Projekte} other {Projekte}}",
|
||||
@@ -2461,5 +2461,9 @@
|
||||
"last_edited_by": "Zuletzt bearbeitet von",
|
||||
"previously_edited_by": "Zuvor bearbeitet von",
|
||||
"edited_by": "Bearbeitet von"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane ist nicht gestartet. Dies könnte daran liegen, dass einer oder mehrere Plane-Services nicht starten konnten.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Wählen Sie View Logs aus setup.sh und Docker-Logs, um sicherzugehen."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -476,6 +476,9 @@
|
||||
"modules": "Modules",
|
||||
"labels": "Labels",
|
||||
"label": "Label",
|
||||
"admins": "Admins",
|
||||
"users": "Users",
|
||||
"guests": "Guests",
|
||||
"assignees": "Assignees",
|
||||
"assignee": "Assignee",
|
||||
"created_by": "Created by",
|
||||
@@ -612,6 +615,15 @@
|
||||
"quarter": "Quarter",
|
||||
"press_for_commands": "Press '/' for commands",
|
||||
"click_to_add_description": "Click to add description",
|
||||
"on_track": "On-Track",
|
||||
"off_track": "Off-Track",
|
||||
"timeline": "Timeline",
|
||||
"completion": "Completion",
|
||||
"upcoming": "Upcoming",
|
||||
"completed": "Completed",
|
||||
"in_progress": "In progress",
|
||||
"planned": "Planned",
|
||||
"paused": "Paused",
|
||||
"search": {
|
||||
"label": "Search",
|
||||
"placeholder": "Type to search",
|
||||
@@ -1158,29 +1170,11 @@
|
||||
"scope_and_demand": "Scope and Demand",
|
||||
"custom": "Custom Analytics"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster",
|
||||
"description": "See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.",
|
||||
"primary_button": {
|
||||
"text": "Start your first project",
|
||||
"comic": {
|
||||
"title": "Analytics works best with Cycles + Modules",
|
||||
"description": "First, timebox your work items into Cycles and, if you can, group work items that span more than a cycle into Modules. Check out both on the left nav."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"total_work_items": "Total work items",
|
||||
"started_work_items": "Started work items",
|
||||
"backlog_work_items": "Backlog work items",
|
||||
"un_started_work_items": "Unstarted work items",
|
||||
"completed_work_items": "Completed work items",
|
||||
"total_guests": "Total Guests",
|
||||
"total_intake": "Total Intake",
|
||||
"total_users": "Total Users",
|
||||
"total_admins": "Total Admins",
|
||||
"total_projects": "Total Projects",
|
||||
"total": "Total {entity}",
|
||||
"started_work_items": "Started {entity}",
|
||||
"backlog_work_items": "Backlog {entity}",
|
||||
"un_started_work_items": "Unstarted {entity}",
|
||||
"completed_work_items": "Completed {entity}",
|
||||
"project_insights": "Project Insights",
|
||||
"summary_of_projects": "Summary of Projects",
|
||||
"all_projects": "All Projects",
|
||||
@@ -1188,7 +1182,7 @@
|
||||
"active_projects": "Active Projects",
|
||||
"customized_insights": "Customized Insights",
|
||||
"created_vs_resolved": "Created vs Resolved",
|
||||
"empty_state_v2": {
|
||||
"empty_state": {
|
||||
"project_insights": {
|
||||
"title": "No data yet",
|
||||
"description": "Work items assigned to you, broken down by state, will show up here."
|
||||
@@ -1312,23 +1306,23 @@
|
||||
}
|
||||
},
|
||||
"account_settings": {
|
||||
"profile":{},
|
||||
"preferences":{
|
||||
"profile": {},
|
||||
"preferences": {
|
||||
"heading": "Preferences",
|
||||
"description": "Customize your app experience the way you work"
|
||||
},
|
||||
"notifications":{
|
||||
"notifications": {
|
||||
"heading": "Email notifications",
|
||||
"description": "Stay in the loop on Work items you are subscribed to. Enable this to get notified."
|
||||
"description": "Stay in the loop on Work items you are subscribed to. Enable this to get notified."
|
||||
},
|
||||
"security":{
|
||||
"security": {
|
||||
"heading": "Security"
|
||||
},
|
||||
"api_tokens":{
|
||||
"api_tokens": {
|
||||
"heading": "Personal Access Tokens",
|
||||
"description": "Generate secure API tokens to integrate your data with external systems and applications."
|
||||
},
|
||||
"activity":{
|
||||
"activity": {
|
||||
"heading": "Activity",
|
||||
"description": "Track your recent actions and changes across all projects and work items."
|
||||
}
|
||||
@@ -1400,7 +1394,7 @@
|
||||
},
|
||||
"billing_and_plans": {
|
||||
"heading": "Billing & Plans",
|
||||
"description":"Choose your plan, manage subscriptions, and easily upgrade as your needs grow.",
|
||||
"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",
|
||||
@@ -2344,5 +2338,9 @@
|
||||
"last_edited_by": "Last edited by",
|
||||
"previously_edited_by": "Previously edited by",
|
||||
"edited_by": "Edited by"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane didn't start up. This could be because one or more Plane services failed to start.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Choose View Logs from setup.sh and Docker logs to be sure."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -869,7 +869,19 @@
|
||||
"view": "Ver",
|
||||
"deactivated_user": "Usuario desactivado",
|
||||
"apply": "Aplicar",
|
||||
"applying": "Aplicando"
|
||||
"applying": "Aplicando",
|
||||
"users": "Usuarios",
|
||||
"admins": "Administradores",
|
||||
"guests": "Invitados",
|
||||
"on_track": "En camino",
|
||||
"off_track": "Fuera de camino",
|
||||
"timeline": "Cronograma",
|
||||
"completion": "Finalización",
|
||||
"upcoming": "Próximo",
|
||||
"completed": "Completado",
|
||||
"in_progress": "En progreso",
|
||||
"planned": "Planificado",
|
||||
"paused": "Pausado"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Eje X",
|
||||
@@ -1319,19 +1331,6 @@
|
||||
"custom": "Análisis Personalizado"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Rastrea el progreso, cargas de trabajo y asignaciones. Identifica tendencias, elimina bloqueos y mueve el trabajo más rápido",
|
||||
"description": "Observa el alcance versus la demanda, estimaciones y el aumento del alcance. Obtén el rendimiento por miembros del equipo y equipos, y asegúrate de que tu proyecto se ejecute a tiempo.",
|
||||
"primary_button": {
|
||||
"text": "Inicia tu primer proyecto",
|
||||
"comic": {
|
||||
"title": "El análisis funciona mejor con Ciclos + Módulos",
|
||||
"description": "Primero, organiza tus elementos de trabajo en Ciclos y, si puedes, agrupa los elementos de trabajo que abarcan más de un ciclo en Módulos. Revisa ambos en la navegación izquierda."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Los elementos de trabajo asignados a ti, desglosados por estado, aparecerán aquí.",
|
||||
"title": "Aún no hay datos"
|
||||
@@ -1347,21 +1346,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Creado vs Resuelto",
|
||||
"customized_insights": "Información personalizada",
|
||||
"backlog_work_items": "Elementos de trabajo en backlog",
|
||||
"backlog_work_items": "{entity} en backlog",
|
||||
"active_projects": "Proyectos activos",
|
||||
"trend_on_charts": "Tendencia en gráficos",
|
||||
"all_projects": "Todos los proyectos",
|
||||
"summary_of_projects": "Resumen de proyectos",
|
||||
"project_insights": "Información del proyecto",
|
||||
"started_work_items": "Elementos de trabajo iniciados",
|
||||
"total_work_items": "Total de elementos de trabajo",
|
||||
"started_work_items": "{entity} iniciados",
|
||||
"total_work_items": "Total de {entity}",
|
||||
"total_projects": "Total de proyectos",
|
||||
"total_admins": "Total de administradores",
|
||||
"total_users": "Total de usuarios",
|
||||
"total_intake": "Ingreso total",
|
||||
"un_started_work_items": "Elementos de trabajo no iniciados",
|
||||
"un_started_work_items": "{entity} no iniciados",
|
||||
"total_guests": "Total de invitados",
|
||||
"completed_work_items": "Elementos de trabajo completados"
|
||||
"completed_work_items": "{entity} completados",
|
||||
"total": "Total de {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {Proyecto} other {Proyectos}}",
|
||||
@@ -2464,5 +2464,9 @@
|
||||
"last_edited_by": "Última edición por",
|
||||
"previously_edited_by": "Editado anteriormente por",
|
||||
"edited_by": "Editado por"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane no se inició. Esto podría deberse a que uno o más servicios de Plane fallaron al iniciar.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Selecciona View Logs desde setup.sh y los logs de Docker para estar seguro."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -867,7 +867,19 @@
|
||||
"view": "Afficher",
|
||||
"deactivated_user": "Utilisateur désactivé",
|
||||
"apply": "Appliquer",
|
||||
"applying": "Application"
|
||||
"applying": "Application",
|
||||
"users": "Utilisateurs",
|
||||
"admins": "Administrateurs",
|
||||
"guests": "Invités",
|
||||
"on_track": "Sur la bonne voie",
|
||||
"off_track": "Hors de la bonne voie",
|
||||
"timeline": "Chronologie",
|
||||
"completion": "Achèvement",
|
||||
"upcoming": "À venir",
|
||||
"completed": "Terminé",
|
||||
"in_progress": "En cours",
|
||||
"planned": "Planifié",
|
||||
"paused": "En pause"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Axe X",
|
||||
@@ -1317,19 +1329,6 @@
|
||||
"custom": "Analytique Personnalisée"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Suivez les progrès, les charges de travail et les allocations. Repérez les tendances, supprimez les blocages et accélérez le travail",
|
||||
"description": "Visualisez la portée par rapport à la demande, les estimations et l'augmentation de la portée. Obtenez les performances par membres de l'équipe et équipes, et assurez-vous que votre projet se déroule dans les délais.",
|
||||
"primary_button": {
|
||||
"text": "Commencez votre premier projet",
|
||||
"comic": {
|
||||
"title": "L'analytique fonctionne mieux avec les Cycles + Modules",
|
||||
"description": "D'abord, planifiez vos éléments de travail dans des Cycles et, si possible, regroupez les éléments de travail qui s'étendent sur plus d'un cycle dans des Modules. Consultez les deux dans la navigation de gauche."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Les éléments de travail qui vous sont assignés, répartis par état, s'afficheront ici.",
|
||||
"title": "Pas encore de données"
|
||||
@@ -1345,21 +1344,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Créé vs Résolu",
|
||||
"customized_insights": "Informations personnalisées",
|
||||
"backlog_work_items": "Éléments de travail en backlog",
|
||||
"backlog_work_items": "{entity} en backlog",
|
||||
"active_projects": "Projets actifs",
|
||||
"trend_on_charts": "Tendance sur les graphiques",
|
||||
"all_projects": "Tous les projets",
|
||||
"summary_of_projects": "Résumé des projets",
|
||||
"project_insights": "Aperçus du projet",
|
||||
"started_work_items": "Éléments de travail commencés",
|
||||
"total_work_items": "Total des éléments de travail",
|
||||
"started_work_items": "{entity} commencés",
|
||||
"total_work_items": "Total des {entity}",
|
||||
"total_projects": "Total des projets",
|
||||
"total_admins": "Total des administrateurs",
|
||||
"total_users": "Nombre total d'utilisateurs",
|
||||
"total_intake": "Revenu total",
|
||||
"un_started_work_items": "Éléments de travail non commencés",
|
||||
"un_started_work_items": "{entity} non commencés",
|
||||
"total_guests": "Nombre total d'invités",
|
||||
"completed_work_items": "Éléments de travail terminés"
|
||||
"completed_work_items": "{entity} terminés",
|
||||
"total": "Total des {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {Projet} other {Projets}}",
|
||||
@@ -2462,5 +2462,9 @@
|
||||
"last_edited_by": "Dernière modification par",
|
||||
"previously_edited_by": "Précédemment modifié par",
|
||||
"edited_by": "Modifié par"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane n'a pas démarré. Cela pourrait être dû au fait qu'un ou plusieurs services Plane ont échoué à démarrer.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Choisissez View Logs depuis setup.sh et les logs Docker pour en être sûr."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -866,7 +866,19 @@
|
||||
"view": "Lihat",
|
||||
"deactivated_user": "Pengguna dinonaktifkan",
|
||||
"apply": "Terapkan",
|
||||
"applying": "Terapkan"
|
||||
"applying": "Terapkan",
|
||||
"users": "Pengguna",
|
||||
"admins": "Admin",
|
||||
"guests": "Tamu",
|
||||
"on_track": "Sesuai Jalur",
|
||||
"off_track": "Menyimpang",
|
||||
"timeline": "Linimasa",
|
||||
"completion": "Penyelesaian",
|
||||
"upcoming": "Mendatang",
|
||||
"completed": "Selesai",
|
||||
"in_progress": "Sedang berlangsung",
|
||||
"planned": "Direncanakan",
|
||||
"paused": "Dijedaikan"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Sumbu-X",
|
||||
@@ -1316,19 +1328,6 @@
|
||||
"custom": "Analitik Kustom"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Lacak kemajuan, beban kerja, dan alokasi. Temukan tren, hilangkan penghalang, dan percepat pekerjaan",
|
||||
"description": "Lihat lingkup dibandingkan permintaan, perkiraan, dan lingkup cree. Dapatkan kinerja oleh anggota tim dan tim, dan pastikan proyek Anda berjalan tepat waktu.",
|
||||
"primary_button": {
|
||||
"text": "Mulai proyek pertama Anda",
|
||||
"comic": {
|
||||
"title": "Analitik bekerja terbaik dengan Siklus + Modul",
|
||||
"description": "Pertama, bagi item kerja Anda ke dalam Siklus dan, jika memungkinkan, kelompokkan item kerja yang menjangkau lebih dari satu siklus ke dalam Modul. Lihat kedua fungsi pada navigasi kiri."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Item pekerjaan yang ditugaskan kepada Anda, dipecah berdasarkan status, akan muncul di sini.",
|
||||
"title": "Belum ada data"
|
||||
@@ -1344,21 +1343,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Dibuat vs Diselesaikan",
|
||||
"customized_insights": "Wawasan yang Disesuaikan",
|
||||
"backlog_work_items": "Item pekerjaan backlog",
|
||||
"backlog_work_items": "{entity} backlog",
|
||||
"active_projects": "Proyek Aktif",
|
||||
"trend_on_charts": "Tren pada grafik",
|
||||
"all_projects": "Semua Proyek",
|
||||
"summary_of_projects": "Ringkasan Proyek",
|
||||
"project_insights": "Wawasan Proyek",
|
||||
"started_work_items": "Item pekerjaan yang telah dimulai",
|
||||
"total_work_items": "Total item pekerjaan",
|
||||
"started_work_items": "{entity} yang telah dimulai",
|
||||
"total_work_items": "Total {entity}",
|
||||
"total_projects": "Total Proyek",
|
||||
"total_admins": "Total Admin",
|
||||
"total_users": "Total Pengguna",
|
||||
"total_intake": "Total Pemasukan",
|
||||
"un_started_work_items": "Item pekerjaan yang belum dimulai",
|
||||
"un_started_work_items": "{entity} yang belum dimulai",
|
||||
"total_guests": "Total Tamu",
|
||||
"completed_work_items": "Item pekerjaan yang telah selesai"
|
||||
"completed_work_items": "{entity} yang telah selesai",
|
||||
"total": "Total {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {Proyek} other {Proyek}}",
|
||||
@@ -2456,5 +2456,9 @@
|
||||
"last_edited_by": "Terakhir disunting oleh",
|
||||
"previously_edited_by": "Sebelumnya disunting oleh",
|
||||
"edited_by": "Disunting oleh"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane tidak berhasil dimulai. Ini bisa karena satu atau lebih layanan Plane gagal untuk dimulai.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Pilih View Logs dari setup.sh dan log Docker untuk memastikan."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -865,7 +865,19 @@
|
||||
"view": "Visualizza",
|
||||
"deactivated_user": "Utente disattivato",
|
||||
"apply": "Applica",
|
||||
"applying": "Applicazione"
|
||||
"applying": "Applicazione",
|
||||
"users": "Utenti",
|
||||
"admins": "Amministratori",
|
||||
"guests": "Ospiti",
|
||||
"on_track": "In linea",
|
||||
"off_track": "Fuori rotta",
|
||||
"timeline": "Cronologia",
|
||||
"completion": "Completamento",
|
||||
"upcoming": "In arrivo",
|
||||
"completed": "Completato",
|
||||
"in_progress": "In corso",
|
||||
"planned": "Pianificato",
|
||||
"paused": "In pausa"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Asse X",
|
||||
@@ -1315,19 +1327,6 @@
|
||||
"custom": "Analisi personalizzata"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Traccia il progresso, i carichi di lavoro e le assegnazioni. Individua tendenze, rimuovi gli ostacoli e accelera il lavoro",
|
||||
"description": "Visualizza l'ambito rispetto alla domanda, le stime e il fenomeno del scope creep. Ottieni le prestazioni dei membri del team e dei team, e assicurati che il tuo progetto rispetti le scadenze.",
|
||||
"primary_button": {
|
||||
"text": "Inizia il tuo primo progetto",
|
||||
"comic": {
|
||||
"title": "Le analisi funzionano meglio con Cicli + Moduli",
|
||||
"description": "Prima, definisci i tuoi elementi di lavoro in cicli e, se puoi, raggruppa quelli che si estendono per più di un ciclo in moduli. Dai un'occhiata ad entrambi nel menu di sinistra."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Gli elementi di lavoro assegnati a te, suddivisi per stato, verranno visualizzati qui.",
|
||||
"title": "Nessun dato disponibile"
|
||||
@@ -1343,21 +1342,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Creato vs Risolto",
|
||||
"customized_insights": "Approfondimenti personalizzati",
|
||||
"backlog_work_items": "Elementi di lavoro nel backlog",
|
||||
"backlog_work_items": "{entity} nel backlog",
|
||||
"active_projects": "Progetti attivi",
|
||||
"trend_on_charts": "Tendenza nei grafici",
|
||||
"all_projects": "Tutti i progetti",
|
||||
"summary_of_projects": "Riepilogo dei progetti",
|
||||
"project_insights": "Approfondimenti sul progetto",
|
||||
"started_work_items": "Elementi di lavoro iniziati",
|
||||
"total_work_items": "Totale elementi di lavoro",
|
||||
"started_work_items": "{entity} iniziati",
|
||||
"total_work_items": "Totale {entity}",
|
||||
"total_projects": "Progetti totali",
|
||||
"total_admins": "Totale amministratori",
|
||||
"total_users": "Totale utenti",
|
||||
"total_intake": "Entrate totali",
|
||||
"un_started_work_items": "Elementi di lavoro non avviati",
|
||||
"un_started_work_items": "{entity} non avviati",
|
||||
"total_guests": "Totale ospiti",
|
||||
"completed_work_items": "Elementi di lavoro completati"
|
||||
"completed_work_items": "{entity} completati",
|
||||
"total": "Totale {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {Progetto} other {Progetti}}",
|
||||
@@ -2461,5 +2461,9 @@
|
||||
"last_edited_by": "Ultima modifica di",
|
||||
"previously_edited_by": "Precedentemente modificato da",
|
||||
"edited_by": "Modificato da"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane non si è avviato. Questo potrebbe essere dovuto al fatto che uno o più servizi Plane non sono riusciti ad avviarsi.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Scegli View Logs da setup.sh e dai log Docker per essere sicuro."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -867,7 +867,19 @@
|
||||
"view": "ビュー",
|
||||
"deactivated_user": "無効化されたユーザー",
|
||||
"apply": "適用",
|
||||
"applying": "適用中"
|
||||
"applying": "適用中",
|
||||
"users": "ユーザー",
|
||||
"admins": "管理者",
|
||||
"guests": "ゲスト",
|
||||
"on_track": "順調",
|
||||
"off_track": "遅れ",
|
||||
"timeline": "タイムライン",
|
||||
"completion": "完了",
|
||||
"upcoming": "今後の予定",
|
||||
"completed": "完了",
|
||||
"in_progress": "進行中",
|
||||
"planned": "計画済み",
|
||||
"paused": "一時停止"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "エックス アクシス",
|
||||
@@ -1317,19 +1329,6 @@
|
||||
"custom": "カスタムアナリティクス"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "進捗、ワークロード、割り当てを追跡。傾向を把握し、ブロッカーを解消して、作業をより速く進めましょう",
|
||||
"description": "スコープと需要、見積もり、スコープクリープを確認できます。チームメンバーとチームのパフォーマンスを把握し、プロジェクトが予定通りに進むようにします。",
|
||||
"primary_button": {
|
||||
"text": "最初のプロジェクトを開始",
|
||||
"comic": {
|
||||
"title": "アナリティクスはサイクル + モジュールで最も効果を発揮",
|
||||
"description": "まず、作業項目をサイクルでタイムボックス化し、可能であれば、複数のサイクルにまたがる作業項目をモジュールにグループ化します。左のナビゲーションで両方を確認してください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "あなたに割り当てられた作業項目は、ステータスごとに分類されてここに表示されます。",
|
||||
"title": "まだデータがありません"
|
||||
@@ -1345,21 +1344,22 @@
|
||||
},
|
||||
"created_vs_resolved": "作成 vs 解決",
|
||||
"customized_insights": "カスタマイズされたインサイト",
|
||||
"backlog_work_items": "バックログの作業項目",
|
||||
"backlog_work_items": "バックログの{entity}",
|
||||
"active_projects": "アクティブなプロジェクト",
|
||||
"trend_on_charts": "グラフの傾向",
|
||||
"all_projects": "すべてのプロジェクト",
|
||||
"summary_of_projects": "プロジェクトの概要",
|
||||
"project_insights": "プロジェクトのインサイト",
|
||||
"started_work_items": "開始された作業項目",
|
||||
"total_work_items": "作業項目の合計",
|
||||
"started_work_items": "開始された{entity}",
|
||||
"total_work_items": "{entity}の合計",
|
||||
"total_projects": "プロジェクト合計",
|
||||
"total_admins": "管理者の合計",
|
||||
"total_users": "ユーザー総数",
|
||||
"total_intake": "総収入",
|
||||
"un_started_work_items": "未開始の作業項目",
|
||||
"un_started_work_items": "未開始の{entity}",
|
||||
"total_guests": "ゲストの合計",
|
||||
"completed_work_items": "完了した作業項目"
|
||||
"completed_work_items": "完了した{entity}",
|
||||
"total": "{entity}の合計"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {プロジェクト} other {プロジェクト}}",
|
||||
@@ -2462,5 +2462,9 @@
|
||||
"last_edited_by": "最終編集者",
|
||||
"previously_edited_by": "以前の編集者",
|
||||
"edited_by": "編集者"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Planeが起動しませんでした。これは1つまたは複数のPlaneサービスの起動に失敗したことが原因である可能性があります。",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "setup.shとDockerログからView Logsを選択して確認してください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -868,7 +868,19 @@
|
||||
"view": "보기",
|
||||
"deactivated_user": "비활성화된 사용자",
|
||||
"apply": "적용",
|
||||
"applying": "적용 중"
|
||||
"applying": "적용 중",
|
||||
"users": "사용자",
|
||||
"admins": "관리자",
|
||||
"guests": "게스트",
|
||||
"on_track": "계획대로 진행 중",
|
||||
"off_track": "계획 이탈",
|
||||
"timeline": "타임라인",
|
||||
"completion": "완료",
|
||||
"upcoming": "예정된",
|
||||
"completed": "완료됨",
|
||||
"in_progress": "진행 중",
|
||||
"planned": "계획된",
|
||||
"paused": "일시 중지됨"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "X축",
|
||||
@@ -1318,19 +1330,6 @@
|
||||
"custom": "맞춤형 분석"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "진행 상황, 작업량 및 할당을 추적하세요. 트렌드를 파악하고, 차단 요소를 제거하며, 작업을 더 빠르게 진행하세요",
|
||||
"description": "범위 대 수요, 추정치 및 범위 크리프를 확인하세요. 팀원과 팀의 성과를 확인하고 프로젝트가 제시간에 진행되도록 하세요.",
|
||||
"primary_button": {
|
||||
"text": "첫 번째 프로젝트 시작",
|
||||
"comic": {
|
||||
"title": "분석은 주기 + 모듈과 함께 작동합니다",
|
||||
"description": "먼저 작업 항목을 주기로 시간 상자화하고, 주기를 초과하는 작업 항목을 모듈로 그룹화하세요. 왼쪽 탐색에서 둘 다 확인하세요."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "귀하에게 할당된 작업 항목이 상태별로 나누어 여기에 표시됩니다.",
|
||||
"title": "아직 데이터가 없습니다"
|
||||
@@ -1346,21 +1345,22 @@
|
||||
},
|
||||
"created_vs_resolved": "생성됨 vs 해결됨",
|
||||
"customized_insights": "맞춤형 인사이트",
|
||||
"backlog_work_items": "백로그 작업 항목",
|
||||
"backlog_work_items": "백로그 {entity}",
|
||||
"active_projects": "활성 프로젝트",
|
||||
"trend_on_charts": "차트의 추세",
|
||||
"all_projects": "모든 프로젝트",
|
||||
"summary_of_projects": "프로젝트 요약",
|
||||
"project_insights": "프로젝트 인사이트",
|
||||
"started_work_items": "시작된 작업 항목",
|
||||
"total_work_items": "총 작업 항목",
|
||||
"started_work_items": "시작된 {entity}",
|
||||
"total_work_items": "총 {entity}",
|
||||
"total_projects": "총 프로젝트 수",
|
||||
"total_admins": "총 관리자 수",
|
||||
"total_users": "총 사용자 수",
|
||||
"total_intake": "총 수입",
|
||||
"un_started_work_items": "시작되지 않은 작업 항목",
|
||||
"un_started_work_items": "시작되지 않은 {entity}",
|
||||
"total_guests": "총 게스트 수",
|
||||
"completed_work_items": "완료된 작업 항목"
|
||||
"completed_work_items": "완료된 {entity}",
|
||||
"total": "총 {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {프로젝트} other {프로젝트}}",
|
||||
@@ -2464,5 +2464,9 @@
|
||||
"last_edited_by": "마지막 편집자",
|
||||
"previously_edited_by": "이전 편집자",
|
||||
"edited_by": "편집자"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane이 시작되지 않았습니다. 이는 하나 이상의 Plane 서비스가 시작에 실패했기 때문일 수 있습니다.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "확실히 하려면 setup.sh와 Docker 로그에서 View Logs를 선택하세요."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -868,7 +868,19 @@
|
||||
"view": "Widok",
|
||||
"deactivated_user": "Dezaktywowany użytkownik",
|
||||
"apply": "Zastosuj",
|
||||
"applying": "Zastosowanie"
|
||||
"applying": "Zastosowanie",
|
||||
"users": "Użytkownicy",
|
||||
"admins": "Administratorzy",
|
||||
"guests": "Goście",
|
||||
"on_track": "Na dobrej drodze",
|
||||
"off_track": "Poza planem",
|
||||
"timeline": "Oś czasu",
|
||||
"completion": "Zakończenie",
|
||||
"upcoming": "Nadchodzące",
|
||||
"completed": "Zakończone",
|
||||
"in_progress": "W trakcie",
|
||||
"planned": "Zaplanowane",
|
||||
"paused": "Wstrzymane"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Oś X",
|
||||
@@ -1318,19 +1330,6 @@
|
||||
"custom": "Analizy niestandardowe"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Śledź postępy, obciążenie i alokacje. Identyfikuj trendy, usuwaj przeszkody i przyspieszaj pracę",
|
||||
"description": "Obserwuj zakres vs. zapotrzebowanie, szacunki i zakres. Sprawdzaj wydajność członków i zespołów, upewnij się, że projekty kończą się na czas.",
|
||||
"primary_button": {
|
||||
"text": "Zacznij pierwszy projekt",
|
||||
"comic": {
|
||||
"title": "Analizy najlepiej działają z Cyklem + Modułami",
|
||||
"description": "Najpierw ogranicz pracę w cyklach i grupuj zadania w modułach obejmujących wiele cykli. Znajdziesz je w menu po lewej."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Przypisane do Ciebie elementy pracy, podzielone według stanu, pojawią się tutaj.",
|
||||
"title": "Brak danych"
|
||||
@@ -1346,21 +1345,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Utworzone vs Rozwiązane",
|
||||
"customized_insights": "Dostosowane informacje",
|
||||
"backlog_work_items": "Elementy pracy w backlogu",
|
||||
"backlog_work_items": "{entity} w backlogu",
|
||||
"active_projects": "Aktywne projekty",
|
||||
"trend_on_charts": "Trend na wykresach",
|
||||
"all_projects": "Wszystkie projekty",
|
||||
"summary_of_projects": "Podsumowanie projektów",
|
||||
"project_insights": "Wgląd w projekt",
|
||||
"started_work_items": "Rozpoczęte elementy pracy",
|
||||
"total_work_items": "Łączna liczba elementów pracy",
|
||||
"started_work_items": "Rozpoczęte {entity}",
|
||||
"total_work_items": "Łączna liczba {entity}",
|
||||
"total_projects": "Łączna liczba projektów",
|
||||
"total_admins": "Łączna liczba administratorów",
|
||||
"total_users": "Łączna liczba użytkowników",
|
||||
"total_intake": "Całkowity dochód",
|
||||
"un_started_work_items": "Nierozpoczęte elementy pracy",
|
||||
"un_started_work_items": "Nierozpoczęte {entity}",
|
||||
"total_guests": "Łączna liczba gości",
|
||||
"completed_work_items": "Ukończone elementy pracy"
|
||||
"completed_work_items": "Ukończone {entity}",
|
||||
"total": "Łączna liczba {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {Projekt} few {Projekty} other {Projektów}}",
|
||||
@@ -2463,5 +2463,9 @@
|
||||
"last_edited_by": "Ostatnio edytowane przez",
|
||||
"previously_edited_by": "Wcześniej edytowane przez",
|
||||
"edited_by": "Edytowane przez"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane nie uruchomił się. Może to być spowodowane tym, że jedna lub więcej usług Plane nie mogła się uruchomić.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Wybierz View Logs z setup.sh i logów Docker, aby mieć pewność."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -868,7 +868,19 @@
|
||||
"view": "Visualizar",
|
||||
"deactivated_user": "Usuário desativado",
|
||||
"apply": "Aplicar",
|
||||
"applying": "Aplicando"
|
||||
"applying": "Aplicando",
|
||||
"users": "Usuários",
|
||||
"admins": "Administradores",
|
||||
"guests": "Convidados",
|
||||
"on_track": "No caminho certo",
|
||||
"off_track": "Fora do caminho",
|
||||
"timeline": "Linha do tempo",
|
||||
"completion": "Conclusão",
|
||||
"upcoming": "Próximo",
|
||||
"completed": "Concluído",
|
||||
"in_progress": "Em andamento",
|
||||
"planned": "Planejado",
|
||||
"paused": "Pausado"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Eixo X",
|
||||
@@ -1318,19 +1330,6 @@
|
||||
"custom": "Análises Personalizadas"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Acompanhe o progresso, as cargas de trabalho e as alocações. Identifique tendências, remova bloqueadores e mova o trabalho mais rapidamente",
|
||||
"description": "Veja o escopo versus a demanda, as estimativas e o aumento do escopo. Obtenha o desempenho por membros da equipe e equipes, e certifique-se de que seu projeto seja executado no prazo.",
|
||||
"primary_button": {
|
||||
"text": "Comece seu primeiro projeto",
|
||||
"comic": {
|
||||
"title": "A análise funciona melhor com Ciclos + Módulos",
|
||||
"description": "Primeiro, coloque seus itens de trabalho em Ciclos e, se puder, agrupe os itens de trabalho que abrangem mais de um ciclo em Módulos. Confira ambos na navegação à esquerda."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Os itens de trabalho atribuídos a você, divididos por estado, aparecerão aqui.",
|
||||
"title": "Ainda não há dados"
|
||||
@@ -1346,21 +1345,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Criado vs Resolvido",
|
||||
"customized_insights": "Insights personalizados",
|
||||
"backlog_work_items": "Itens de trabalho no backlog",
|
||||
"backlog_work_items": "{entity} no backlog",
|
||||
"active_projects": "Projetos ativos",
|
||||
"trend_on_charts": "Tendência nos gráficos",
|
||||
"all_projects": "Todos os projetos",
|
||||
"summary_of_projects": "Resumo dos projetos",
|
||||
"project_insights": "Insights do projeto",
|
||||
"started_work_items": "Itens de trabalho iniciados",
|
||||
"total_work_items": "Total de itens de trabalho",
|
||||
"started_work_items": "{entity} iniciados",
|
||||
"total_work_items": "Total de {entity}",
|
||||
"total_projects": "Total de projetos",
|
||||
"total_admins": "Total de administradores",
|
||||
"total_users": "Total de usuários",
|
||||
"total_intake": "Receita total",
|
||||
"un_started_work_items": "Itens de trabalho não iniciados",
|
||||
"un_started_work_items": "{entity} não iniciados",
|
||||
"total_guests": "Total de convidados",
|
||||
"completed_work_items": "Itens de trabalho concluídos"
|
||||
"completed_work_items": "{entity} concluídos",
|
||||
"total": "Total de {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {Projeto} other {Projetos}}",
|
||||
@@ -2458,5 +2458,9 @@
|
||||
"last_edited_by": "Última edição por",
|
||||
"previously_edited_by": "Anteriormente editado por",
|
||||
"edited_by": "Editado por"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "O Plane não inicializou. Isso pode ser porque um ou mais serviços do Plane falharam ao iniciar.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Escolha View Logs do setup.sh e logs do Docker para ter certeza."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -866,7 +866,19 @@
|
||||
"view": "Vizualizează",
|
||||
"deactivated_user": "Utilizator dezactivat",
|
||||
"apply": "Aplică",
|
||||
"applying": "Aplicând"
|
||||
"applying": "Aplicând",
|
||||
"users": "Utilizatori",
|
||||
"admins": "Administratori",
|
||||
"guests": "Invitați",
|
||||
"on_track": "Pe drumul cel bun",
|
||||
"off_track": "În afara traiectoriei",
|
||||
"timeline": "Cronologie",
|
||||
"completion": "Finalizare",
|
||||
"upcoming": "Viitor",
|
||||
"completed": "Finalizat",
|
||||
"in_progress": "În desfășurare",
|
||||
"planned": "Planificat",
|
||||
"paused": "Pauzat"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "axa-X",
|
||||
@@ -1316,19 +1328,6 @@
|
||||
"custom": "Analitice personalizate"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Urmărește progresul, activitățile și alocările. Observă tendințele, elimină blocajele și accelerează munca",
|
||||
"description": "Vezi raportul dintre activitățile asumate și cerere, estimările și eventualele extinderi neplanificate ale activităților asumate. Obține performanța pe membri și echipe și asigură-te că proiectul tău se încadrează în timp.",
|
||||
"primary_button": {
|
||||
"text": "Începe primul tău proiect",
|
||||
"comic": {
|
||||
"title": "Statisticile funcționează cel mai bine cu Cicluri + Module",
|
||||
"description": "Mai întâi, încadrează-ți activitățile în Cicluri și, dacă poți, grupează-le pe cele care se întind pe mai multe cicluri în Module. Le găsești în meniul din stânga."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Elementele de lucru atribuite ție, împărțite pe stări, vor apărea aici.",
|
||||
"title": "Nu există date încă"
|
||||
@@ -1344,21 +1343,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Creat vs Rezolvat",
|
||||
"customized_insights": "Perspective personalizate",
|
||||
"backlog_work_items": "Elemente de lucru din backlog",
|
||||
"backlog_work_items": "{entity} din backlog",
|
||||
"active_projects": "Proiecte active",
|
||||
"trend_on_charts": "Tendință în grafice",
|
||||
"all_projects": "Toate proiectele",
|
||||
"summary_of_projects": "Sumarul proiectelor",
|
||||
"project_insights": "Informații despre proiect",
|
||||
"started_work_items": "Elemente de lucru începute",
|
||||
"total_work_items": "Totalul elementelor de lucru",
|
||||
"started_work_items": "{entity} începute",
|
||||
"total_work_items": "Totalul {entity}",
|
||||
"total_projects": "Total proiecte",
|
||||
"total_admins": "Total administratori",
|
||||
"total_users": "Total utilizatori",
|
||||
"total_intake": "Venit total",
|
||||
"un_started_work_items": "Elemente de lucru neîncepute",
|
||||
"un_started_work_items": "{entity} neîncepute",
|
||||
"total_guests": "Total invitați",
|
||||
"completed_work_items": "Elemente de lucru finalizate"
|
||||
"completed_work_items": "{entity} finalizate",
|
||||
"total": "Totalul {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {Proiect} other {Proiecte}}",
|
||||
@@ -2456,5 +2456,9 @@
|
||||
"last_edited_by": "Ultima editare de către",
|
||||
"previously_edited_by": "Editat anterior de către",
|
||||
"edited_by": "Editat de"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane nu a pornit. Aceasta ar putea fi din cauza că unul sau mai multe servicii Plane au eșuat să pornească.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Alegeți View Logs din setup.sh și logurile Docker pentru a fi siguri."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -868,7 +868,19 @@
|
||||
"view": "Просмотр",
|
||||
"deactivated_user": "Деактивированный пользователь",
|
||||
"apply": "Применить",
|
||||
"applying": "Применение"
|
||||
"applying": "Применение",
|
||||
"users": "Пользователи",
|
||||
"admins": "Администраторы",
|
||||
"guests": "Гости",
|
||||
"on_track": "По плану",
|
||||
"off_track": "Отклонение от плана",
|
||||
"timeline": "Хронология",
|
||||
"completion": "Завершение",
|
||||
"upcoming": "Предстоящие",
|
||||
"completed": "Завершено",
|
||||
"in_progress": "В процессе",
|
||||
"planned": "Запланировано",
|
||||
"paused": "На паузе"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Ось X",
|
||||
@@ -1318,19 +1330,6 @@
|
||||
"custom": "Пользовательская аналитика"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Отслеживайте прогресс, загрузку и распределение ресурсов",
|
||||
"description": "Анализируйте объёмы работ, оценивайте сроки и контролируйте выполнение проектов. Отслеживайте производительность команды и соблюдайте сроки.",
|
||||
"primary_button": {
|
||||
"text": "Начать первый проект",
|
||||
"comic": {
|
||||
"title": "Аналитика лучше всего работает с Циклами + Модулями",
|
||||
"description": "Сначала группируйте рабочие элементы в Циклы, а при возможности - объединяйте рабочие элементы в Модули. Найдите оба раздела в левом меню."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Назначенные вам рабочие элементы, разбитые по статусам, появятся здесь.",
|
||||
"title": "Данных пока нет"
|
||||
@@ -1346,21 +1345,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Создано vs Решено",
|
||||
"customized_insights": "Индивидуальные аналитические данные",
|
||||
"backlog_work_items": "Элементы работы в бэклоге",
|
||||
"backlog_work_items": "{entity} в бэклоге",
|
||||
"active_projects": "Активные проекты",
|
||||
"trend_on_charts": "Тренд на графиках",
|
||||
"all_projects": "Все проекты",
|
||||
"summary_of_projects": "Сводка по проектам",
|
||||
"project_insights": "Аналитика проекта",
|
||||
"started_work_items": "Начатые рабочие элементы",
|
||||
"total_work_items": "Общее количество рабочих элементов",
|
||||
"started_work_items": "Начатые {entity}",
|
||||
"total_work_items": "Общее количество {entity}",
|
||||
"total_projects": "Всего проектов",
|
||||
"total_admins": "Всего администраторов",
|
||||
"total_users": "Всего пользователей",
|
||||
"total_intake": "Общий доход",
|
||||
"un_started_work_items": "Не начатые рабочие элементы",
|
||||
"un_started_work_items": "Не начатые {entity}",
|
||||
"total_guests": "Всего гостей",
|
||||
"completed_work_items": "Завершённые рабочие элементы"
|
||||
"completed_work_items": "Завершённые {entity}",
|
||||
"total": "Общее количество {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {Проект} other {Проекты}}",
|
||||
@@ -2464,5 +2464,9 @@
|
||||
"last_edited_by": "Последнее редактирование",
|
||||
"previously_edited_by": "Ранее отредактировано",
|
||||
"edited_by": "Отредактировано"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane не запустился. Это может быть из-за того, что один или несколько сервисов Plane не смогли запуститься.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Выберите View Logs из setup.sh и логов Docker, чтобы убедиться."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -868,7 +868,19 @@
|
||||
"view": "Zobraziť",
|
||||
"deactivated_user": "Deaktivovaný používateľ",
|
||||
"apply": "Použiť",
|
||||
"applying": "Používanie"
|
||||
"applying": "Používanie",
|
||||
"users": "Používatelia",
|
||||
"admins": "Administrátori",
|
||||
"guests": "Hostia",
|
||||
"on_track": "Na správnej ceste",
|
||||
"off_track": "Mimo plán",
|
||||
"timeline": "Časová os",
|
||||
"completion": "Dokončenie",
|
||||
"upcoming": "Nadchádzajúce",
|
||||
"completed": "Dokončené",
|
||||
"in_progress": "Prebieha",
|
||||
"planned": "Plánované",
|
||||
"paused": "Pozastavené"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Os X",
|
||||
@@ -1318,19 +1330,6 @@
|
||||
"custom": "Vlastná analytika"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Sledujte pokrok, vyťaženie a alokácie. Identifikujte trendy, odstráňte prekážky a zrýchlite prácu",
|
||||
"description": "Sledujte rozsah vs. dopyt, odhady a rozsah. Zistite výkonnosť členov a tímov, zabezpečte včasné dokončenie projektov.",
|
||||
"primary_button": {
|
||||
"text": "Začnite prvý projekt",
|
||||
"comic": {
|
||||
"title": "Analytika funguje najlepšie s Cykly + Moduly",
|
||||
"description": "Najprv časovo ohraničte prácu do cyklov a zoskupte položky presahujúce cyklus do modulov. Nájdete ich v ľavom menu."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Pracovné položky priradené vám, rozdelené podľa stavu, sa zobrazia tu.",
|
||||
"title": "Zatiaľ žiadne údaje"
|
||||
@@ -1346,21 +1345,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Vytvorené vs Vyriešené",
|
||||
"customized_insights": "Prispôsobené prehľady",
|
||||
"backlog_work_items": "Pracovné položky v backlogu",
|
||||
"backlog_work_items": "{entity} v backlogu",
|
||||
"active_projects": "Aktívne projekty",
|
||||
"trend_on_charts": "Trend na grafoch",
|
||||
"all_projects": "Všetky projekty",
|
||||
"summary_of_projects": "Súhrn projektov",
|
||||
"project_insights": "Prehľad projektu",
|
||||
"started_work_items": "Spustené pracovné položky",
|
||||
"total_work_items": "Celkový počet pracovných položiek",
|
||||
"started_work_items": "Spustené {entity}",
|
||||
"total_work_items": "Celkový počet {entity}",
|
||||
"total_projects": "Celkový počet projektov",
|
||||
"total_admins": "Celkový počet administrátorov",
|
||||
"total_users": "Celkový počet používateľov",
|
||||
"total_intake": "Celkový príjem",
|
||||
"un_started_work_items": "Nespustené pracovné položky",
|
||||
"un_started_work_items": "Nespustené {entity}",
|
||||
"total_guests": "Celkový počet hostí",
|
||||
"completed_work_items": "Dokončené pracovné položky"
|
||||
"completed_work_items": "Dokončené {entity}",
|
||||
"total": "Celkový počet {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {Projekt} few {Projekty} other {Projektov}}",
|
||||
@@ -2463,5 +2463,9 @@
|
||||
"last_edited_by": "Naposledy upravené používateľom",
|
||||
"previously_edited_by": "Predtým upravené používateľom",
|
||||
"edited_by": "Upravené používateľom"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane sa nespustil. Toto môže byť spôsobené tým, že sa jedna alebo viac služieb Plane nepodarilo spustiť.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Vyberte View Logs z setup.sh a Docker logov, aby ste si boli istí."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -869,7 +869,19 @@
|
||||
"view": "Görünüm",
|
||||
"deactivated_user": "Devre dışı bırakılmış kullanıcı",
|
||||
"apply": "Uygula",
|
||||
"applying": "Uygulanıyor"
|
||||
"applying": "Uygulanıyor",
|
||||
"users": "Kullanıcılar",
|
||||
"admins": "Yöneticiler",
|
||||
"guests": "Misafirler",
|
||||
"on_track": "Yolunda",
|
||||
"off_track": "Yolunda değil",
|
||||
"timeline": "Zaman çizelgesi",
|
||||
"completion": "Tamamlama",
|
||||
"upcoming": "Yaklaşan",
|
||||
"completed": "Tamamlandı",
|
||||
"in_progress": "Devam ediyor",
|
||||
"planned": "Planlandı",
|
||||
"paused": "Durduruldu"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "X ekseni",
|
||||
@@ -1319,19 +1331,6 @@
|
||||
"custom": "Özel Analitik"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "İlerlemeyi, iş yükünü ve tahsisatları izleyin. Eğilimleri tespit edin, engelleri kaldırın ve işleri hızlandırın",
|
||||
"description": "Kapsam ve talep, tahminler ve kapsam genişlemesini görün. Takım üyeleri ve ekiplerin performansını izleyin ve projenizin zamanında ilerlemesini sağlayın.",
|
||||
"primary_button": {
|
||||
"text": "İlk projenizi başlatın",
|
||||
"comic": {
|
||||
"title": "Analitik Döngüler + Modüllerle en iyi şekilde çalışır",
|
||||
"description": "Öncelikle, iş öğelerinizi Döngülere zamanlayın ve mümkünse, bir döngüden uzun süren iş öğelerini Modüllerde gruplayın. Her ikisini de sol gezintide bulabilirsiniz."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Size atanan iş öğeleri, duruma göre ayrılarak burada gösterilecektir.",
|
||||
"title": "Henüz veri yok"
|
||||
@@ -1347,21 +1346,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Oluşturulan vs Çözülen",
|
||||
"customized_insights": "Özelleştirilmiş İçgörüler",
|
||||
"backlog_work_items": "Backlog iş öğeleri",
|
||||
"backlog_work_items": "Backlog {entity}",
|
||||
"active_projects": "Aktif Projeler",
|
||||
"trend_on_charts": "Grafiklerdeki eğilim",
|
||||
"all_projects": "Tüm Projeler",
|
||||
"summary_of_projects": "Projelerin Özeti",
|
||||
"project_insights": "Proje İçgörüleri",
|
||||
"started_work_items": "Başlatılan iş öğeleri",
|
||||
"total_work_items": "Toplam iş öğesi",
|
||||
"started_work_items": "Başlatılan {entity}",
|
||||
"total_work_items": "Toplam {entity}",
|
||||
"total_projects": "Toplam Proje",
|
||||
"total_admins": "Toplam Yönetici",
|
||||
"total_users": "Toplam Kullanıcı",
|
||||
"total_intake": "Toplam Gelir",
|
||||
"un_started_work_items": "Başlanmamış iş öğeleri",
|
||||
"un_started_work_items": "Başlanmamış {entity}",
|
||||
"total_guests": "Toplam Misafir",
|
||||
"completed_work_items": "Tamamlanmış iş öğeleri"
|
||||
"completed_work_items": "Tamamlanmış {entity}",
|
||||
"total": "Toplam {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {Proje} other {Projeler}}",
|
||||
@@ -2442,5 +2442,9 @@
|
||||
"last_edited_by": "Son düzenleyen",
|
||||
"previously_edited_by": "Önceki düzenleyen",
|
||||
"edited_by": "Tarafından düzenlendi"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane başlatılamadı. Bu, bir veya daha fazla Plane servisinin başlatılamaması nedeniyle olabilir.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Emin olmak için setup.sh ve Docker loglarından View Logs'u seçin."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -868,7 +868,19 @@
|
||||
"view": "Подання",
|
||||
"deactivated_user": "Деактивований користувач",
|
||||
"apply": "Застосувати",
|
||||
"applying": "Застосовується"
|
||||
"applying": "Застосовується",
|
||||
"users": "Користувачі",
|
||||
"admins": "Адміністратори",
|
||||
"guests": "Гості",
|
||||
"on_track": "У межах графіку",
|
||||
"off_track": "Поза графіком",
|
||||
"timeline": "Хронологія",
|
||||
"completion": "Завершення",
|
||||
"upcoming": "Майбутнє",
|
||||
"completed": "Завершено",
|
||||
"in_progress": "В процесі",
|
||||
"planned": "Заплановано",
|
||||
"paused": "Призупинено"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Вісь X",
|
||||
@@ -1318,19 +1330,6 @@
|
||||
"custom": "Користувацька аналітика"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Відстежуйте прогрес, навантаження й розподіл. Виявляйте тенденції, усувайте перешкоди й прискорюйте роботу",
|
||||
"description": "Стежте за обсягом проти попиту, оцінками та обсягом. Визначайте ефективність учасників і команд, аби вчасно виконувати проєкти.",
|
||||
"primary_button": {
|
||||
"text": "Розпочніть перший проєкт",
|
||||
"comic": {
|
||||
"title": "Аналітика найкраще працює з Циклами + Модулями",
|
||||
"description": "Спочатку обмежте роботу в часі через Цикли та згрупуйте робочі одиниці, які тривають довше, у Модулі. Все це в лівому меню."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Призначені вам робочі елементи, розбиті за станом, з’являться тут.",
|
||||
"title": "Ще немає даних"
|
||||
@@ -1346,21 +1345,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Створено vs Вирішено",
|
||||
"customized_insights": "Персоналізовані аналітичні дані",
|
||||
"backlog_work_items": "Робочі елементи у беклозі",
|
||||
"backlog_work_items": "{entity} у беклозі",
|
||||
"active_projects": "Активні проєкти",
|
||||
"trend_on_charts": "Тенденція на графіках",
|
||||
"all_projects": "Усі проєкти",
|
||||
"summary_of_projects": "Зведення проєктів",
|
||||
"project_insights": "Аналітика проєкту",
|
||||
"started_work_items": "Розпочаті робочі елементи",
|
||||
"total_work_items": "Усього робочих елементів",
|
||||
"started_work_items": "Розпочаті {entity}",
|
||||
"total_work_items": "Усього {entity}",
|
||||
"total_projects": "Усього проєктів",
|
||||
"total_admins": "Усього адміністраторів",
|
||||
"total_users": "Усього користувачів",
|
||||
"total_intake": "Загальний дохід",
|
||||
"un_started_work_items": "Нерозпочаті робочі елементи",
|
||||
"un_started_work_items": "Нерозпочаті {entity}",
|
||||
"total_guests": "Усього гостей",
|
||||
"completed_work_items": "Завершені робочі елементи"
|
||||
"completed_work_items": "Завершені {entity}",
|
||||
"total": "Усього {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {Проєкт} few {Проєкти} other {Проєктів}}",
|
||||
@@ -2463,5 +2463,9 @@
|
||||
"last_edited_by": "Останнє редагування",
|
||||
"previously_edited_by": "Раніше відредаговано",
|
||||
"edited_by": "Відредаговано"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane не запустився. Це може бути через те, що один або декілька сервісів Plane не змогли запуститися.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Виберіть View Logs з setup.sh та логів Docker, щоб переконатися."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -867,7 +867,19 @@
|
||||
"view": "Xem",
|
||||
"deactivated_user": "Người dùng bị vô hiệu hóa",
|
||||
"apply": "Áp dụng",
|
||||
"applying": "Đang áp dụng"
|
||||
"applying": "Đang áp dụng",
|
||||
"users": "Người dùng",
|
||||
"admins": "Quản trị viên",
|
||||
"guests": "Khách",
|
||||
"on_track": "Đúng tiến độ",
|
||||
"off_track": "Chệch hướng",
|
||||
"timeline": "Dòng thời gian",
|
||||
"completion": "Hoàn thành",
|
||||
"upcoming": "Sắp tới",
|
||||
"completed": "Đã hoàn thành",
|
||||
"in_progress": "Đang tiến hành",
|
||||
"planned": "Đã lên kế hoạch",
|
||||
"paused": "Tạm dừng"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Trục X",
|
||||
@@ -1317,19 +1329,6 @@
|
||||
"custom": "Phân tích tùy chỉnh"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "Theo dõi tiến độ, khối lượng công việc và phân công. Khám phá xu hướng, loại bỏ rào cản và đẩy nhanh công việc",
|
||||
"description": "Xem phạm vi so với nhu cầu, ước tính và mở rộng phạm vi. Nhận hiệu suất của thành viên nhóm và nhóm, đảm bảo dự án của bạn đúng tiến độ.",
|
||||
"primary_button": {
|
||||
"text": "Bắt đầu dự án đầu tiên của bạn",
|
||||
"comic": {
|
||||
"title": "Phân tích hoạt động tốt nhất trong chu kỳ + mô-đun",
|
||||
"description": "Đầu tiên, giới hạn mục công việc của bạn trong chu kỳ và nếu có thể, nhóm mục công việc kéo dài nhiều chu kỳ thành mô-đun. Xem cả hai trong thanh điều hướng bên trái."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "Các hạng mục công việc được giao cho bạn, phân loại theo trạng thái, sẽ hiển thị tại đây.",
|
||||
"title": "Chưa có dữ liệu"
|
||||
@@ -1345,21 +1344,22 @@
|
||||
},
|
||||
"created_vs_resolved": "Đã tạo vs Đã giải quyết",
|
||||
"customized_insights": "Thông tin chi tiết tùy chỉnh",
|
||||
"backlog_work_items": "Các hạng mục công việc tồn đọng",
|
||||
"backlog_work_items": "{entity} tồn đọng",
|
||||
"active_projects": "Dự án đang hoạt động",
|
||||
"trend_on_charts": "Xu hướng trên biểu đồ",
|
||||
"all_projects": "Tất cả dự án",
|
||||
"summary_of_projects": "Tóm tắt dự án",
|
||||
"project_insights": "Thông tin chi tiết dự án",
|
||||
"started_work_items": "Hạng mục công việc đã bắt đầu",
|
||||
"total_work_items": "Tổng số hạng mục công việc",
|
||||
"started_work_items": "{entity} đã bắt đầu",
|
||||
"total_work_items": "Tổng số {entity}",
|
||||
"total_projects": "Tổng số dự án",
|
||||
"total_admins": "Tổng số quản trị viên",
|
||||
"total_users": "Tổng số người dùng",
|
||||
"total_intake": "Tổng thu",
|
||||
"un_started_work_items": "Hạng mục công việc chưa bắt đầu",
|
||||
"un_started_work_items": "{entity} chưa bắt đầu",
|
||||
"total_guests": "Tổng số khách",
|
||||
"completed_work_items": "Hạng mục công việc đã hoàn thành"
|
||||
"completed_work_items": "{entity} đã hoàn thành",
|
||||
"total": "Tổng số {entity}"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {dự án} other {dự án}}",
|
||||
@@ -2461,5 +2461,9 @@
|
||||
"last_edited_by": "Chỉnh sửa lần cuối bởi",
|
||||
"previously_edited_by": "Trước đây được chỉnh sửa bởi",
|
||||
"edited_by": "Được chỉnh sửa bởi"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane không khởi động được. Điều này có thể do một hoặc nhiều dịch vụ Plane không khởi động được.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Chọn View Logs từ setup.sh và log Docker để chắc chắn."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -867,7 +867,19 @@
|
||||
"view": "查看",
|
||||
"deactivated_user": "已停用用户",
|
||||
"apply": "应用",
|
||||
"applying": "应用中"
|
||||
"applying": "应用中",
|
||||
"users": "用户",
|
||||
"admins": "管理员",
|
||||
"guests": "访客",
|
||||
"on_track": "进展顺利",
|
||||
"off_track": "偏离轨道",
|
||||
"timeline": "时间轴",
|
||||
"completion": "完成",
|
||||
"upcoming": "即将发生",
|
||||
"completed": "已完成",
|
||||
"in_progress": "进行中",
|
||||
"planned": "已计划",
|
||||
"paused": "暂停"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "X轴",
|
||||
@@ -1317,19 +1329,6 @@
|
||||
"custom": "自定义分析"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "跟踪进度、工作量和分配。发现趋势、消除障碍并加快工作进度",
|
||||
"description": "查看范围与需求、估算和范围蔓延。获取团队成员和团队的表现,确保您的项目按时运行。",
|
||||
"primary_button": {
|
||||
"text": "开始您的第一个项目",
|
||||
"comic": {
|
||||
"title": "分析在周期 + 模块中效果最佳",
|
||||
"description": "首先,将您的工作项限定在周期中,如果可能的话,将跨越多个周期的工作项分组到模块中。在左侧导航栏中查看这两项。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "分配给您的工作项将按状态分类显示在此处。",
|
||||
"title": "暂无数据"
|
||||
@@ -1345,21 +1344,22 @@
|
||||
},
|
||||
"created_vs_resolved": "已创建 vs 已解决",
|
||||
"customized_insights": "自定义洞察",
|
||||
"backlog_work_items": "待办工作项",
|
||||
"backlog_work_items": "待办的{entity}",
|
||||
"active_projects": "活跃项目",
|
||||
"trend_on_charts": "图表趋势",
|
||||
"all_projects": "所有项目",
|
||||
"summary_of_projects": "项目概览",
|
||||
"project_insights": "项目洞察",
|
||||
"started_work_items": "已开始的工作项",
|
||||
"total_work_items": "工作项总数",
|
||||
"started_work_items": "已开始的{entity}",
|
||||
"total_work_items": "{entity}总数",
|
||||
"total_projects": "项目总数",
|
||||
"total_admins": "管理员总数",
|
||||
"total_users": "用户总数",
|
||||
"total_intake": "总收入",
|
||||
"un_started_work_items": "未开始的工作项",
|
||||
"un_started_work_items": "未开始的{entity}",
|
||||
"total_guests": "访客总数",
|
||||
"completed_work_items": "已完成的工作项"
|
||||
"completed_work_items": "已完成的{entity}",
|
||||
"total": "{entity}总数"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {项目} other {项目}}",
|
||||
@@ -2443,5 +2443,9 @@
|
||||
"last_edited_by": "最后编辑者",
|
||||
"previously_edited_by": "之前编辑者",
|
||||
"edited_by": "编辑者"
|
||||
},
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane 未能启动。这可能是因为一个或多个 Plane 服务启动失败。",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "请选择“查看日志”来查看 setup.sh 和 Docker 日志,以确认问题。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -868,7 +868,19 @@
|
||||
"view": "檢視",
|
||||
"deactivated_user": "已停用用戶",
|
||||
"apply": "應用",
|
||||
"applying": "應用中"
|
||||
"applying": "應用中",
|
||||
"users": "使用者",
|
||||
"admins": "管理員",
|
||||
"guests": "訪客",
|
||||
"on_track": "進展順利",
|
||||
"off_track": "偏離軌道",
|
||||
"timeline": "時間軸",
|
||||
"completion": "完成",
|
||||
"upcoming": "即將發生",
|
||||
"completed": "已完成",
|
||||
"in_progress": "進行中",
|
||||
"planned": "已計劃",
|
||||
"paused": "暫停"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "X 軸",
|
||||
@@ -1318,19 +1330,6 @@
|
||||
"custom": "自訂分析"
|
||||
},
|
||||
"empty_state": {
|
||||
"general": {
|
||||
"title": "追蹤進度、工作量和分配。發現趨勢、移除阻礙,加快工作進展",
|
||||
"description": "檢視範圍與需求、評估和範圍擴展。取得團隊成員和團隊的績效,確保您的專案按時進行。",
|
||||
"primary_button": {
|
||||
"text": "開始您的第一個專案",
|
||||
"comic": {
|
||||
"title": "分析最適合搭配週期 + 模組使用",
|
||||
"description": "首先,將您的工作事項時間區段到週期中,如果可以的話,將跨週期的工作事項分組到模組中。請檢視左側導覽列中的兩個功能。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty_state_v2": {
|
||||
"customized_insights": {
|
||||
"description": "指派給您的工作項目將依狀態分類顯示在此處。",
|
||||
"title": "尚無資料"
|
||||
@@ -1346,21 +1345,22 @@
|
||||
},
|
||||
"created_vs_resolved": "已建立 vs 已解決",
|
||||
"customized_insights": "自訂化洞察",
|
||||
"backlog_work_items": "待辦工作項目",
|
||||
"backlog_work_items": "待辦的{entity}",
|
||||
"active_projects": "啟用中的專案",
|
||||
"trend_on_charts": "圖表趨勢",
|
||||
"all_projects": "所有專案",
|
||||
"summary_of_projects": "專案摘要",
|
||||
"project_insights": "專案洞察",
|
||||
"started_work_items": "已開始的工作項目",
|
||||
"total_work_items": "工作項目總數",
|
||||
"started_work_items": "已開始的{entity}",
|
||||
"total_work_items": "{entity}總數",
|
||||
"total_projects": "專案總數",
|
||||
"total_admins": "管理員總數",
|
||||
"total_users": "使用者總數",
|
||||
"total_intake": "總收入",
|
||||
"un_started_work_items": "未開始的工作項目",
|
||||
"un_started_work_items": "未開始的{entity}",
|
||||
"total_guests": "訪客總數",
|
||||
"completed_work_items": "已完成的工作項目"
|
||||
"completed_work_items": "已完成的{entity}",
|
||||
"total": "{entity}總數"
|
||||
},
|
||||
"workspace_projects": {
|
||||
"label": "{count, plural, one {專案} other {專案}}",
|
||||
@@ -2464,5 +2464,10 @@
|
||||
"last_edited_by": "最後編輯者",
|
||||
"previously_edited_by": "先前編輯者",
|
||||
"edited_by": "編輯者"
|
||||
},
|
||||
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane 未能啟動。這可能是因為一個或多個 Plane 服務啟動失敗。",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "從 setup.sh 和 Docker 日誌中選擇 View Logs 來確認。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/logger",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Logger shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/propel",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/services",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
// constants
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
// types
|
||||
import {
|
||||
IAnalyticsParams,
|
||||
IAnalyticsResponse,
|
||||
IDefaultAnalyticsResponse,
|
||||
IExportAnalyticsFormData,
|
||||
ISaveAnalyticsFormData,
|
||||
} from "@plane/types";
|
||||
// services
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
export class AnalyticsService extends APIService {
|
||||
constructor(BASE_URL?: string) {
|
||||
super(BASE_URL || API_BASE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves analytics data for a specific workspace
|
||||
* @param {string} workspaceSlug - The unique identifier for the workspace
|
||||
* @param {IAnalyticsParams} params - Parameters for filtering analytics data
|
||||
* @param {string|number} [params.project] - Optional project identifier that will be converted to string
|
||||
* @returns {Promise<IAnalyticsResponse>} The analytics data for the workspace
|
||||
* @throws {Error} Throws response data if the request fails
|
||||
*/
|
||||
async getAnalytics(workspaceSlug: string, params: IAnalyticsParams): Promise<IAnalyticsResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/analytics/`, {
|
||||
params: {
|
||||
...params,
|
||||
project: params?.project ? params.project.toString() : null,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves default analytics data for a workspace
|
||||
* @param {string} workspaceSlug - The unique identifier for the workspace
|
||||
* @param {Partial<IAnalyticsParams>} [params] - Optional parameters for filtering default analytics
|
||||
* @param {string|number} [params.project] - Optional project identifier that will be converted to string
|
||||
* @returns {Promise<IDefaultAnalyticsResponse>} The default analytics data
|
||||
* @throws {Error} Throws response data if the request fails
|
||||
*/
|
||||
async getDefaultAnalytics(
|
||||
workspaceSlug: string,
|
||||
params?: Partial<IAnalyticsParams>
|
||||
): Promise<IDefaultAnalyticsResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/default-analytics/`, {
|
||||
params: {
|
||||
...params,
|
||||
project: params?.project ? params.project.toString() : null,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves analytics view configuration for a workspace
|
||||
* @param {string} workspaceSlug - The unique identifier for the workspace
|
||||
* @param {ISaveAnalyticsFormData} data - The analytics configuration data to save
|
||||
* @returns {Promise<any>} The response from saving the analytics view
|
||||
* @throws {Error} Throws response data if the request fails
|
||||
*/
|
||||
async save(workspaceSlug: string, data: ISaveAnalyticsFormData): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/analytic-view/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports analytics data for a workspace
|
||||
* @param {string} workspaceSlug - The unique identifier for the workspace
|
||||
* @param {IExportAnalyticsFormData} data - Configuration for the analytics export
|
||||
* @returns {Promise<any>} The exported analytics data
|
||||
* @throws {Error} Throws response data if the request fails
|
||||
*/
|
||||
async export(workspaceSlug: string, data: IExportAnalyticsFormData): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/export-analytics/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./analytics.service";
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from "./ai";
|
||||
export * from "./analytics";
|
||||
export * from "./developer";
|
||||
export * from "./auth";
|
||||
export * from "./cycle";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/shared-state",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Shared state shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/tailwind-config",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "tailwind.config.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"types": "./src/index.d.ts",
|
||||
|
||||
Vendored
-55
@@ -1,55 +0,0 @@
|
||||
import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants";
|
||||
import { TChartData } from "./charts";
|
||||
|
||||
export type TAnalyticsTabsV2Base = "overview" | "work-items";
|
||||
export type TAnalyticsGraphsV2Base = "projects" | "work-items" | "custom-work-items";
|
||||
|
||||
// service types
|
||||
|
||||
export interface IAnalyticsResponseV2 {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface IAnalyticsResponseFieldsV2 {
|
||||
count: number;
|
||||
filter_count: number;
|
||||
}
|
||||
|
||||
export interface IAnalyticsRadarEntityV2 {
|
||||
key: string;
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
// chart types
|
||||
|
||||
export interface IChartResponseV2 {
|
||||
schema: Record<string, string>;
|
||||
data: TChartData<string, string>[];
|
||||
}
|
||||
|
||||
// table types
|
||||
|
||||
export interface WorkItemInsightColumns {
|
||||
project_id?: string;
|
||||
project__name?: string;
|
||||
cancelled_work_items: number;
|
||||
completed_work_items: number;
|
||||
backlog_work_items: number;
|
||||
un_started_work_items: number;
|
||||
started_work_items: number;
|
||||
// because of the peek view, we will display the name of the project instead of project__name
|
||||
display_name?: string;
|
||||
avatar_url?: string;
|
||||
assignee_id?: string;
|
||||
}
|
||||
|
||||
export type AnalyticsTableDataMap = {
|
||||
"work-items": WorkItemInsightColumns;
|
||||
};
|
||||
|
||||
export interface IAnalyticsV2Params {
|
||||
x_axis: ChartXAxisProperty;
|
||||
y_axis: ChartYAxisMetric;
|
||||
group_by?: ChartXAxisProperty;
|
||||
}
|
||||
Vendored
+50
-106
@@ -1,116 +1,60 @@
|
||||
import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants";
|
||||
import { TChartData } from "./charts";
|
||||
|
||||
export type TAnalyticsTabsBase = "overview" | "work-items";
|
||||
export type TAnalyticsGraphsBase = "projects" | "work-items" | "custom-work-items";
|
||||
export type TAnalyticsFilterParams = {
|
||||
project_ids?: string;
|
||||
cycle_id?: string;
|
||||
module_id?: string;
|
||||
};
|
||||
|
||||
// service types
|
||||
|
||||
export interface IAnalyticsResponse {
|
||||
total: number;
|
||||
distribution: IAnalyticsData;
|
||||
extras: {
|
||||
assignee_details: IAnalyticsAssigneeDetails[];
|
||||
cycle_details: IAnalyticsCycleDetails[];
|
||||
label_details: IAnalyticsLabelDetails[];
|
||||
module_details: IAnalyticsModuleDetails[];
|
||||
state_details: IAnalyticsStateDetails[];
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface IAnalyticsData {
|
||||
[key: string]: {
|
||||
dimension: string | null;
|
||||
segment?: string;
|
||||
count?: number;
|
||||
estimate?: number | null;
|
||||
}[];
|
||||
export interface IAnalyticsResponseFields {
|
||||
count: number;
|
||||
filter_count: number;
|
||||
}
|
||||
|
||||
export interface IAnalyticsAssigneeDetails {
|
||||
assignees__avatar_url: string | null;
|
||||
assignees__display_name: string | null;
|
||||
assignees__first_name: string;
|
||||
assignees__id: string | null;
|
||||
assignees__last_name: string;
|
||||
}
|
||||
|
||||
export interface IAnalyticsCycleDetails {
|
||||
issue_cycle__cycle__name: string | null;
|
||||
issue_cycle__cycle_id: string | null;
|
||||
}
|
||||
|
||||
export interface IAnalyticsLabelDetails {
|
||||
labels__color: string | null;
|
||||
labels__id: string | null;
|
||||
labels__name: string | null;
|
||||
}
|
||||
|
||||
export interface IAnalyticsModuleDetails {
|
||||
issue_module__module__name: string | null;
|
||||
issue_module__module_id: string | null;
|
||||
}
|
||||
|
||||
export interface IAnalyticsStateDetails {
|
||||
state__color: string;
|
||||
state__name: string;
|
||||
state_id: string;
|
||||
}
|
||||
|
||||
export type TXAxisValues =
|
||||
| "state_id"
|
||||
| "state__group"
|
||||
| "labels__id"
|
||||
| "assignees__id"
|
||||
| "estimate_point__value"
|
||||
| "issue_cycle__cycle_id"
|
||||
| "issue_module__module_id"
|
||||
| "priority"
|
||||
| "start_date"
|
||||
| "target_date"
|
||||
| "created_at"
|
||||
| "completed_at";
|
||||
|
||||
export type TYAxisValues = "issue_count" | "estimate";
|
||||
|
||||
export interface IAnalyticsParams {
|
||||
x_axis: TXAxisValues;
|
||||
y_axis: TYAxisValues;
|
||||
segment?: TXAxisValues | null;
|
||||
project?: string[] | null;
|
||||
cycle?: string | null;
|
||||
module?: string | null;
|
||||
}
|
||||
|
||||
export interface ISaveAnalyticsFormData {
|
||||
export interface IAnalyticsRadarEntity {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
query_dict: IExportAnalyticsFormData;
|
||||
}
|
||||
export interface IExportAnalyticsFormData {
|
||||
x_axis: TXAxisValues;
|
||||
y_axis: TYAxisValues;
|
||||
segment?: TXAxisValues | null;
|
||||
project?: string[];
|
||||
}
|
||||
|
||||
export interface IDefaultAnalyticsUser {
|
||||
assignees__avatar_url: string | null;
|
||||
assignees__first_name: string;
|
||||
assignees__last_name: string;
|
||||
assignees__display_name: string;
|
||||
assignees__id: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface IDefaultAnalyticsResponse {
|
||||
issue_completed_month_wise: { month: number; count: number }[];
|
||||
most_issue_closed_user: IDefaultAnalyticsUser[];
|
||||
most_issue_created_user: {
|
||||
created_by__avatar_url: string | null;
|
||||
created_by__first_name: string;
|
||||
created_by__last_name: string;
|
||||
created_by__display_name: string;
|
||||
created_by__id: string;
|
||||
count: number;
|
||||
}[];
|
||||
open_estimate_sum: number;
|
||||
open_issues: number;
|
||||
open_issues_classified: { state_group: string; state_count: number }[];
|
||||
pending_issue_user: IDefaultAnalyticsUser[];
|
||||
total_estimate_sum: number;
|
||||
total_issues: number;
|
||||
total_issues_classified: { state_group: string; state_count: number }[];
|
||||
// chart types
|
||||
|
||||
export interface IChartResponse {
|
||||
schema: Record<string, string>;
|
||||
data: TChartData<string, string>[];
|
||||
}
|
||||
|
||||
// table types
|
||||
|
||||
export interface WorkItemInsightColumns {
|
||||
project_id?: string;
|
||||
project__name?: string;
|
||||
cancelled_work_items: number;
|
||||
completed_work_items: number;
|
||||
backlog_work_items: number;
|
||||
un_started_work_items: number;
|
||||
started_work_items: number;
|
||||
// because of the peek view, we will display the name of the project instead of project__name
|
||||
display_name?: string;
|
||||
avatar_url?: string;
|
||||
assignee_id?: string;
|
||||
}
|
||||
|
||||
export type AnalyticsTableDataMap = {
|
||||
"work-items": WorkItemInsightColumns;
|
||||
};
|
||||
|
||||
export interface IAnalyticsParams {
|
||||
x_axis: ChartXAxisProperty;
|
||||
y_axis: ChartYAxisMetric;
|
||||
group_by?: ChartXAxisProperty;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -43,4 +43,4 @@ export * from "./home";
|
||||
export * from "./stickies";
|
||||
export * from "./utils";
|
||||
export * from "./payment";
|
||||
export * from "./analytics-v2";
|
||||
export * from "./analytics";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/typescript-config",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from "react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import React, { useState, useRef, useContext } from "react";
|
||||
import { usePopper } from "react-popper";
|
||||
// helpers
|
||||
import { cn } from "../../../helpers";
|
||||
// types
|
||||
import { TContextMenuItem } from "./root";
|
||||
import { TContextMenuItem, ContextMenuContext, Portal } from "./root";
|
||||
|
||||
type ContextMenuItemProps = {
|
||||
handleActiveItem: () => void;
|
||||
@@ -14,45 +16,230 @@ type ContextMenuItemProps = {
|
||||
export const ContextMenuItem: React.FC<ContextMenuItemProps> = (props) => {
|
||||
const { handleActiveItem, handleClose, isActive, item } = props;
|
||||
|
||||
// Nested menu state
|
||||
const [isNestedOpen, setIsNestedOpen] = useState(false);
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
const [activeNestedIndex, setActiveNestedIndex] = useState<number>(0);
|
||||
const nestedMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const contextMenuContext = useContext(ContextMenuContext);
|
||||
const hasNestedItems = item.nestedMenuItems && item.nestedMenuItems.length > 0;
|
||||
const renderedNestedItems = item.nestedMenuItems?.filter((nestedItem) => nestedItem.shouldRender !== false) || [];
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "right-start",
|
||||
strategy: "fixed",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 4],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "flip",
|
||||
options: {
|
||||
fallbackPlacements: ["left-start", "right-end", "left-end", "top-start", "bottom-start"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: 8,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const closeNestedMenu = React.useCallback(() => {
|
||||
setIsNestedOpen(false);
|
||||
setActiveNestedIndex(0);
|
||||
}, []);
|
||||
|
||||
// Register this nested menu with the main context
|
||||
React.useEffect(() => {
|
||||
if (contextMenuContext && hasNestedItems) {
|
||||
return contextMenuContext.registerSubmenu(closeNestedMenu);
|
||||
}
|
||||
}, [contextMenuContext, hasNestedItems, closeNestedMenu]);
|
||||
|
||||
const handleItemClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (hasNestedItems) {
|
||||
// Toggle nested menu
|
||||
if (!isNestedOpen && contextMenuContext) {
|
||||
contextMenuContext.closeAllSubmenus();
|
||||
}
|
||||
setIsNestedOpen(!isNestedOpen);
|
||||
} else {
|
||||
// Execute action for regular items
|
||||
item.action();
|
||||
if (item.closeOnClick !== false) handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
handleActiveItem();
|
||||
|
||||
if (hasNestedItems) {
|
||||
// Close other submenus and open this one
|
||||
if (contextMenuContext) {
|
||||
contextMenuContext.closeAllSubmenus();
|
||||
}
|
||||
setIsNestedOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNestedItemClick = (nestedItem: TContextMenuItem, e?: React.MouseEvent) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
nestedItem.action();
|
||||
if (nestedItem.closeOnClick !== false) {
|
||||
handleClose(); // Close the entire context menu
|
||||
}
|
||||
};
|
||||
|
||||
// Handle keyboard navigation for nested items
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!isNestedOpen || !hasNestedItems) return;
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setActiveNestedIndex((prev) => (prev + 1) % renderedNestedItems.length);
|
||||
}
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setActiveNestedIndex((prev) => (prev - 1 + renderedNestedItems.length) % renderedNestedItems.length);
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const nestedItem = renderedNestedItems[activeNestedIndex];
|
||||
if (!nestedItem.disabled) {
|
||||
handleNestedItemClick(nestedItem);
|
||||
}
|
||||
}
|
||||
if (e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
closeNestedMenu();
|
||||
}
|
||||
};
|
||||
|
||||
if (isNestedOpen && nestedMenuRef.current) {
|
||||
const menuElement = nestedMenuRef.current;
|
||||
menuElement.addEventListener("keydown", handleKeyDown);
|
||||
// Ensure the menu can receive keyboard events
|
||||
menuElement.setAttribute("tabindex", "-1");
|
||||
menuElement.focus();
|
||||
return () => {
|
||||
menuElement.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}
|
||||
}, [isNestedOpen, activeNestedIndex, renderedNestedItems, hasNestedItems, closeNestedMenu]);
|
||||
|
||||
if (item.shouldRender === false) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2 px-1 py-1.5 text-left text-custom-text-200 rounded text-xs select-none",
|
||||
{
|
||||
"bg-custom-background-90": isActive,
|
||||
"text-custom-text-400": item.disabled,
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
item.action();
|
||||
if (item.closeOnClick !== false) handleClose();
|
||||
}}
|
||||
onMouseEnter={handleActiveItem}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.customContent ?? (
|
||||
<>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
<h5>{item.title}</h5>
|
||||
{item.description && (
|
||||
<p
|
||||
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
<>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2 px-1 py-1.5 text-left text-custom-text-200 rounded text-xs select-none",
|
||||
{
|
||||
"bg-custom-background-90": isActive,
|
||||
"text-custom-text-400": item.disabled,
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
onClick={handleItemClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.customContent ?? (
|
||||
<>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div className="flex-1">
|
||||
<h5>{item.title}</h5>
|
||||
{item.description && (
|
||||
<p
|
||||
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{hasNestedItems && <ChevronRight className="h-3 w-3 flex-shrink-0" />}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Nested Menu */}
|
||||
{hasNestedItems && isNestedOpen && (
|
||||
<Portal container={contextMenuContext?.portalContainer}>
|
||||
<div
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
className={cn(
|
||||
"fixed z-[35] min-w-[12rem] overflow-hidden rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-lg",
|
||||
"ring-1 ring-black ring-opacity-5"
|
||||
)}
|
||||
data-context-submenu="true"
|
||||
>
|
||||
<div ref={nestedMenuRef} className="max-h-72 overflow-y-scroll vertical-scrollbar scrollbar-sm">
|
||||
{renderedNestedItems.map((nestedItem, index) => (
|
||||
<button
|
||||
key={nestedItem.key}
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2 px-1 py-1.5 text-left text-custom-text-200 rounded text-xs select-none",
|
||||
{
|
||||
"bg-custom-background-90": index === activeNestedIndex,
|
||||
"text-custom-text-400": nestedItem.disabled,
|
||||
},
|
||||
nestedItem.className
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleNestedItemClick(nestedItem, e);
|
||||
}}
|
||||
onMouseEnter={() => setActiveNestedIndex(index)}
|
||||
disabled={nestedItem.disabled}
|
||||
data-context-submenu="true"
|
||||
>
|
||||
{nestedItem.customContent ?? (
|
||||
<>
|
||||
{nestedItem.icon && <nestedItem.icon className={cn("h-3 w-3", nestedItem.iconClassName)} />}
|
||||
<div>
|
||||
<h5>{nestedItem.title}</h5>
|
||||
{nestedItem.description && (
|
||||
<p
|
||||
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||
"text-custom-text-400": nestedItem.disabled,
|
||||
})}
|
||||
>
|
||||
{nestedItem.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Portal>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,15 +21,46 @@ export type TContextMenuItem = {
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
nestedMenuItems?: TContextMenuItem[];
|
||||
};
|
||||
|
||||
// Portal component for nested menus
|
||||
interface PortalProps {
|
||||
children: React.ReactNode;
|
||||
container?: Element | null;
|
||||
}
|
||||
|
||||
export const Portal: React.FC<PortalProps> = ({ children, container }) => {
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true);
|
||||
return () => setMounted(false);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetContainer = container || document.body;
|
||||
return ReactDOM.createPortal(children, targetContainer);
|
||||
};
|
||||
|
||||
// Context for managing nested menus
|
||||
export const ContextMenuContext = React.createContext<{
|
||||
closeAllSubmenus: () => void;
|
||||
registerSubmenu: (closeSubmenu: () => void) => () => void;
|
||||
portalContainer?: Element | null;
|
||||
} | null>(null);
|
||||
|
||||
type ContextMenuProps = {
|
||||
parentRef: React.RefObject<HTMLElement>;
|
||||
items: TContextMenuItem[];
|
||||
portalContainer?: Element | null;
|
||||
};
|
||||
|
||||
const ContextMenuWithoutPortal: React.FC<ContextMenuProps> = (props) => {
|
||||
const { parentRef, items } = props;
|
||||
const { parentRef, items, portalContainer } = props;
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [position, setPosition] = useState({
|
||||
@@ -39,11 +70,24 @@ const ContextMenuWithoutPortal: React.FC<ContextMenuProps> = (props) => {
|
||||
const [activeItemIndex, setActiveItemIndex] = useState<number>(0);
|
||||
// refs
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||
const submenuClosersRef = useRef<Set<() => void>>(new Set());
|
||||
// derived values
|
||||
const renderedItems = items.filter((item) => item.shouldRender !== false);
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const closeAllSubmenus = React.useCallback(() => {
|
||||
submenuClosersRef.current.forEach((closeSubmenu) => closeSubmenu());
|
||||
}, []);
|
||||
|
||||
const registerSubmenu = React.useCallback((closeSubmenu: () => void) => {
|
||||
submenuClosersRef.current.add(closeSubmenu);
|
||||
return () => {
|
||||
submenuClosersRef.current.delete(closeSubmenu);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleClose = () => {
|
||||
closeAllSubmenus();
|
||||
setIsOpen(false);
|
||||
setActiveItemIndex(0);
|
||||
};
|
||||
@@ -121,13 +165,42 @@ const ContextMenuWithoutPortal: React.FC<ContextMenuProps> = (props) => {
|
||||
};
|
||||
}, [activeItemIndex, isOpen, renderedItems, setIsOpen]);
|
||||
|
||||
// close on clicking outside
|
||||
useOutsideClickDetector(contextMenuRef, handleClose);
|
||||
// Custom handler for nested menu portal clicks
|
||||
React.useEffect(() => {
|
||||
const handleDocumentClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// Check if the click is on a nested menu element
|
||||
const isNestedMenuClick = target.closest('[data-context-submenu="true"]');
|
||||
const isMainMenuClick = contextMenuRef.current?.contains(target);
|
||||
|
||||
// Also check if the target itself has the data attribute
|
||||
const isNestedMenuElement = target.hasAttribute("data-context-submenu");
|
||||
|
||||
// If it's a nested menu click, main menu click, or nested menu element, don't close
|
||||
if (isNestedMenuClick || isMainMenuClick || isNestedMenuElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If menu is open and it's an outside click, close it
|
||||
if (isOpen) {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
// Use capture phase to ensure we handle the event before other handlers
|
||||
document.addEventListener("mousedown", handleDocumentClick, true);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleDocumentClick, true);
|
||||
};
|
||||
}
|
||||
}, [isOpen, handleClose]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed h-screen w-screen top-0 left-0 cursor-default z-20 opacity-0 pointer-events-none transition-opacity",
|
||||
"fixed h-screen w-screen top-0 left-0 cursor-default z-30 opacity-0 pointer-events-none transition-opacity",
|
||||
{
|
||||
"opacity-100 pointer-events-auto": isOpen,
|
||||
}
|
||||
@@ -140,16 +213,19 @@ const ContextMenuWithoutPortal: React.FC<ContextMenuProps> = (props) => {
|
||||
top: position.y,
|
||||
left: position.x,
|
||||
}}
|
||||
data-context-menu="true"
|
||||
>
|
||||
{renderedItems.map((item, index) => (
|
||||
<ContextMenuItem
|
||||
key={item.key}
|
||||
handleActiveItem={() => setActiveItemIndex(index)}
|
||||
handleClose={handleClose}
|
||||
isActive={index === activeItemIndex}
|
||||
item={item}
|
||||
/>
|
||||
))}
|
||||
<ContextMenuContext.Provider value={{ closeAllSubmenus, registerSubmenu, portalContainer }}>
|
||||
{renderedItems.map((item, index) => (
|
||||
<ContextMenuItem
|
||||
key={item.key}
|
||||
handleActiveItem={() => setActiveItemIndex(index)}
|
||||
handleClose={handleClose}
|
||||
isActive={index === activeItemIndex}
|
||||
item={item}
|
||||
/>
|
||||
))}
|
||||
</ContextMenuContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Menu } from "@headlessui/react";
|
||||
import { ChevronDown, MoreHorizontal } from "lucide-react";
|
||||
import { ChevronDown, ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { usePopper } from "react-popper";
|
||||
@@ -10,7 +10,46 @@ import { cn } from "../../helpers";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
|
||||
// types
|
||||
import { ICustomMenuDropdownProps, ICustomMenuItemProps } from "./helper";
|
||||
import {
|
||||
ICustomMenuDropdownProps,
|
||||
ICustomMenuItemProps,
|
||||
ICustomSubMenuProps,
|
||||
ICustomSubMenuTriggerProps,
|
||||
ICustomSubMenuContentProps,
|
||||
} from "./helper";
|
||||
|
||||
interface PortalProps {
|
||||
children: React.ReactNode;
|
||||
container?: Element | null;
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Portal: React.FC<PortalProps> = ({ children, container, asChild = false }) => {
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true);
|
||||
return () => setMounted(false);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetContainer = container || document.body;
|
||||
|
||||
if (asChild) {
|
||||
return ReactDOM.createPortal(children, targetContainer);
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(<div data-radix-portal="">{children}</div>, targetContainer);
|
||||
};
|
||||
|
||||
// Context for main menu to communicate with submenus
|
||||
const MenuContext = React.createContext<{
|
||||
closeAllSubmenus: () => void;
|
||||
registerSubmenu: (closeSubmenu: () => void) => () => void;
|
||||
} | null>(null);
|
||||
|
||||
const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
const {
|
||||
@@ -45,19 +84,35 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
// refs
|
||||
const dropdownRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const submenuClosersRef = React.useRef<Set<() => void>>(new Set());
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "auto",
|
||||
});
|
||||
|
||||
const closeAllSubmenus = React.useCallback(() => {
|
||||
submenuClosersRef.current.forEach((closeSubmenu) => closeSubmenu());
|
||||
}, []);
|
||||
|
||||
const registerSubmenu = React.useCallback((closeSubmenu: () => void) => {
|
||||
submenuClosersRef.current.add(closeSubmenu);
|
||||
return () => {
|
||||
submenuClosersRef.current.delete(closeSubmenu);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => {
|
||||
if (isOpen) onMenuClose?.();
|
||||
|
||||
const closeDropdown = React.useCallback(() => {
|
||||
if (isOpen) {
|
||||
closeAllSubmenus();
|
||||
onMenuClose?.();
|
||||
}
|
||||
setIsOpen(false);
|
||||
};
|
||||
}, [isOpen, closeAllSubmenus, onMenuClose]);
|
||||
|
||||
const selectActiveItem = () => {
|
||||
const activeItem: HTMLElement | undefined | null = dropdownRef.current?.querySelector(
|
||||
@@ -75,8 +130,12 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
const handleMenuButtonClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
isOpen ? closeDropdown() : openDropdown();
|
||||
menuButtonOnClick?.();
|
||||
if (isOpen) {
|
||||
closeDropdown();
|
||||
} else {
|
||||
openDropdown();
|
||||
}
|
||||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
@@ -86,13 +145,43 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
const handleMouseLeave = () => {
|
||||
if (openOnHover && isOpen) {
|
||||
setTimeout(() => {
|
||||
closeDropdown();
|
||||
}, 500);
|
||||
// Only close if menu is still open
|
||||
if (isOpen) {
|
||||
closeDropdown();
|
||||
}
|
||||
}, 150); // Small delay to allow moving to submenu
|
||||
}
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown, useCaptureForOutsideClick);
|
||||
|
||||
// Custom handler for submenu portal clicks
|
||||
React.useEffect(() => {
|
||||
const handleDocumentClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const isSubmenuClick = target.closest('[data-prevent-outside-click="true"]');
|
||||
const isMainMenuClick = dropdownRef.current?.contains(target);
|
||||
|
||||
// If it's a submenu click or main menu click, don't close
|
||||
if (isSubmenuClick || isMainMenuClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If menu is open and it's an outside click, close it
|
||||
if (isOpen) {
|
||||
closeDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("mousedown", handleDocumentClick, useCaptureForOutsideClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleDocumentClick, useCaptureForOutsideClick);
|
||||
};
|
||||
}
|
||||
}, [isOpen, closeDropdown, useCaptureForOutsideClick]);
|
||||
|
||||
let menuItems = (
|
||||
<Menu.Items
|
||||
data-prevent-outside-click={!!portalElement}
|
||||
@@ -117,7 +206,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
{children}
|
||||
<MenuContext.Provider value={{ closeAllSubmenus, registerSubmenu }}>{children}</MenuContext.Provider>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
);
|
||||
@@ -136,6 +225,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
onClick={handleOnClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
data-main-menu="true"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
@@ -202,8 +292,161 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
// SubMenu context for closing submenu from nested items
|
||||
const SubMenuContext = React.createContext<{ closeSubmenu: () => void } | null>(null);
|
||||
|
||||
// Hook to use submenu context
|
||||
const useSubMenu = () => React.useContext(SubMenuContext);
|
||||
|
||||
// SubMenu implementation
|
||||
const SubMenu: React.FC<ICustomSubMenuProps> = (props) => {
|
||||
const {
|
||||
children,
|
||||
trigger,
|
||||
disabled = false,
|
||||
className = "",
|
||||
contentClassName = "",
|
||||
placement = "right-start",
|
||||
} = props;
|
||||
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [referenceElement, setReferenceElement] = React.useState<HTMLSpanElement | null>(null);
|
||||
const [popperElement, setPopperElement] = React.useState<HTMLDivElement | null>(null);
|
||||
const submenuRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const menuContext = React.useContext(MenuContext);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement,
|
||||
strategy: "fixed", // Use fixed positioning to escape overflow constraints
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 4],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "flip",
|
||||
options: {
|
||||
fallbackPlacements: ["left-start", "right-end", "left-end", "top-start", "bottom-start"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: 8,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const closeSubmenu = React.useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
// Register this submenu with the main menu context
|
||||
React.useEffect(() => {
|
||||
if (menuContext) {
|
||||
return menuContext.registerSubmenu(closeSubmenu);
|
||||
}
|
||||
}, [menuContext, closeSubmenu]);
|
||||
|
||||
const toggleSubmenu = () => {
|
||||
if (!disabled) {
|
||||
// Close other submenus when opening this one
|
||||
if (!isOpen && menuContext) {
|
||||
menuContext.closeAllSubmenus();
|
||||
}
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleSubmenu();
|
||||
};
|
||||
|
||||
// Close submenu when clicking on other menu items
|
||||
React.useEffect(() => {
|
||||
const handleMenuItemClick = (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
// Check if the click is on a menu item that's not part of this submenu
|
||||
if (target.closest('[role="menuitem"]') && !submenuRef.current?.contains(target)) {
|
||||
closeSubmenu();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", handleMenuItemClick);
|
||||
return () => {
|
||||
document.removeEventListener("click", handleMenuItemClick);
|
||||
};
|
||||
}, [closeSubmenu]);
|
||||
|
||||
return (
|
||||
<div ref={submenuRef} className={cn("relative", className)}>
|
||||
<span ref={setReferenceElement} className="w-full">
|
||||
<Menu.Item as="div" disabled={disabled}>
|
||||
{({ active }) => (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full select-none rounded px-1 py-1.5 text-left text-custom-text-200 flex items-center justify-between cursor-pointer",
|
||||
{
|
||||
"bg-custom-background-80": active && !disabled,
|
||||
"text-custom-text-400": disabled,
|
||||
"cursor-not-allowed": disabled,
|
||||
}
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span className="flex-1">{trigger}</span>
|
||||
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
</div>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</span>
|
||||
|
||||
{isOpen && (
|
||||
<Portal>
|
||||
<div
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
className={cn(
|
||||
"fixed z-[20] min-w-[12rem] overflow-hidden rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 p-1 text-xs shadow-custom-shadow-lg",
|
||||
"ring-1 ring-black ring-opacity-5", // Additional styling to make it stand out
|
||||
contentClassName
|
||||
)}
|
||||
data-prevent-outside-click="true"
|
||||
onMouseEnter={() => {
|
||||
// Notify parent menu that we're hovering over submenu
|
||||
const mainMenuElement = document.querySelector('[data-main-menu="true"]');
|
||||
if (mainMenuElement) {
|
||||
const mouseEnterEvent = new MouseEvent("mouseenter", { bubbles: true });
|
||||
mainMenuElement.dispatchEvent(mouseEnterEvent);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
// Notify parent menu that we're leaving submenu
|
||||
const mainMenuElement = document.querySelector('[data-main-menu="true"]');
|
||||
if (mainMenuElement) {
|
||||
const mouseLeaveEvent = new MouseEvent("mouseleave", { bubbles: true });
|
||||
mainMenuElement.dispatchEvent(mouseLeaveEvent);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SubMenuContext.Provider value={{ closeSubmenu }}>{children}</SubMenuContext.Provider>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
|
||||
const { children, disabled = false, onClick, className } = props;
|
||||
const submenuContext = useSubMenu();
|
||||
|
||||
return (
|
||||
<Menu.Item as="div" disabled={disabled}>
|
||||
@@ -221,6 +464,8 @@ const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
|
||||
onClick={(e) => {
|
||||
close();
|
||||
onClick?.(e);
|
||||
// Close submenu if this item is inside a submenu
|
||||
submenuContext?.closeSubmenu();
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
@@ -231,6 +476,52 @@ const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SubMenuTrigger: React.FC<ICustomSubMenuTriggerProps> = (props) => {
|
||||
const { children, disabled = false, className } = props;
|
||||
|
||||
return (
|
||||
<Menu.Item as="div" disabled={disabled}>
|
||||
{({ active }) => (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full select-none rounded px-1 py-1.5 text-left text-custom-text-200 flex items-center justify-between",
|
||||
{
|
||||
"bg-custom-background-80": active && !disabled,
|
||||
"text-custom-text-400": disabled,
|
||||
"cursor-pointer": !disabled,
|
||||
"cursor-not-allowed": disabled,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="flex-1">{children}</span>
|
||||
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
</div>
|
||||
)}
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const SubMenuContent: React.FC<ICustomSubMenuContentProps> = (props) => {
|
||||
const { children, className } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"z-[15] min-w-[12rem] overflow-hidden rounded-md border border-custom-border-300 bg-custom-background-100 p-1 text-xs shadow-custom-shadow-rg",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Add all components as static properties for external use
|
||||
CustomMenu.Portal = Portal;
|
||||
CustomMenu.MenuItem = MenuItem;
|
||||
CustomMenu.SubMenu = SubMenu;
|
||||
CustomMenu.SubMenuTrigger = SubMenuTrigger;
|
||||
CustomMenu.SubMenuContent = SubMenuContent;
|
||||
|
||||
export { CustomMenu };
|
||||
|
||||
@@ -21,6 +21,12 @@ export interface IDropdownProps {
|
||||
useCaptureForOutsideClick?: boolean;
|
||||
}
|
||||
|
||||
export interface IPortalProps {
|
||||
children: React.ReactNode;
|
||||
container?: Element | null;
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export interface ICustomMenuDropdownProps extends IDropdownProps {
|
||||
children: React.ReactNode;
|
||||
ellipsis?: boolean;
|
||||
@@ -75,3 +81,27 @@ export interface ICustomSelectItemProps {
|
||||
value: any;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Submenu interfaces
|
||||
export interface ICustomSubMenuProps {
|
||||
children: React.ReactNode;
|
||||
trigger: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
placement?: Placement;
|
||||
}
|
||||
|
||||
export interface ICustomSubMenuTriggerProps {
|
||||
children: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface ICustomSubMenuContentProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
placement?: Placement;
|
||||
sideOffset?: number;
|
||||
alignOffset?: number;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export const setToast = (props: SetToastProps) => {
|
||||
)}
|
||||
>
|
||||
<X
|
||||
className="fixed top-2 right-2.5 text-toast-text-secondary hover:text-toast-text-tertiary cursor-pointer"
|
||||
className="absolute top-2 right-2.5 text-toast-text-secondary hover:text-toast-text-tertiary cursor-pointer"
|
||||
strokeWidth={1.5}
|
||||
width={14}
|
||||
height={14}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/utils",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"description": "Helper functions shared across multiple apps internally",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
EMAIL=
|
||||
PASSWORD_BASE64=
|
||||
BASE_URL=
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
# Playwright
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
playwright/.auth
|
||||
@@ -0,0 +1,110 @@
|
||||
# Integration Tests
|
||||
|
||||
This directory contains end-to-end integration tests for the application using Playwright.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js (v18 or higher)
|
||||
- npm or yarn
|
||||
- Playwright browsers installed
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn install
|
||||
```
|
||||
|
||||
2. Install Playwright browsers:
|
||||
```bash
|
||||
npx playwright install
|
||||
```
|
||||
|
||||
3. Create a `.env` file in the `tests/integration-tests` directory with the following variables:
|
||||
```env
|
||||
BASE_URL=http://localhost:3000 # Your application URL
|
||||
EMAIL=your-email@example.com # Your test user email
|
||||
PASSWORD_BASE64=base64-encoded-password # Your test user password in base64
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Authentication Setup
|
||||
|
||||
First, you need to set up the authentication state:
|
||||
|
||||
```bash
|
||||
npx playwright test auth.setup.ts
|
||||
```
|
||||
|
||||
This will create a `playwright/.auth/user.json` file containing the authentication state.
|
||||
|
||||
### Running All Tests
|
||||
|
||||
```bash
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
### Running Specific Test Files
|
||||
|
||||
```bash
|
||||
# Run a specific test file
|
||||
npx playwright test projects.spec.ts
|
||||
|
||||
# Run tests in a specific browser
|
||||
npx playwright test --project=chromium
|
||||
```
|
||||
|
||||
### Running Tests in UI Mode
|
||||
|
||||
```bash
|
||||
npx playwright test --ui
|
||||
```
|
||||
|
||||
### Debugging Tests
|
||||
|
||||
```bash
|
||||
# Run tests in debug mode
|
||||
npx playwright test --debug
|
||||
|
||||
# Run a specific test in debug mode
|
||||
npx playwright test projects.spec.ts --debug
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
- `auth.setup.ts`: Handles user authentication and creates a persistent auth state
|
||||
- `projects.spec.ts`: Contains the actual test cases for the projects functionality
|
||||
|
||||
## Test Reports
|
||||
|
||||
After running tests, you can view the HTML report:
|
||||
|
||||
```bash
|
||||
npx playwright show-report
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. If tests fail due to authentication:
|
||||
- Delete the `playwright/.auth/user.json` file
|
||||
- Run `npx playwright test auth.setup.ts` again
|
||||
- Try running your tests
|
||||
|
||||
2. If you need to see what's happening during test execution:
|
||||
- Use `--debug` flag
|
||||
- Or run with `--headed` flag to see the browser
|
||||
|
||||
3. If you need to update the auth state:
|
||||
- Delete the existing auth file
|
||||
- Run the auth setup again
|
||||
- Run your tests
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
For CI/CD environments, make sure to:
|
||||
1. Set the appropriate environment variables
|
||||
2. Run `npx playwright install-deps` before running tests
|
||||
3. Use `--reporter=html` for test reports
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "integration-tests",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@types/node": "^22.13.14"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
import dotenv from "dotenv";
|
||||
import path from "path";
|
||||
dotenv.config({ path: path.resolve(__dirname, ".env") });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: process.env.BASE_URL || "http://localhost:3000",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "setup",
|
||||
testMatch: /auth.setup.ts$/,
|
||||
},
|
||||
{
|
||||
name: "chromium",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
storageState: "./playwright/.auth/user.json",
|
||||
},
|
||||
dependencies: ["setup"],
|
||||
testMatch: /.*\.spec\.ts$/,
|
||||
},
|
||||
|
||||
{
|
||||
name: "firefox",
|
||||
use: {
|
||||
...devices["Desktop Firefox"],
|
||||
storageState: "playwright/.auth/user.json",
|
||||
},
|
||||
dependencies: ["setup"],
|
||||
testMatch: /.*\.spec\.ts/,
|
||||
},
|
||||
|
||||
{
|
||||
name: "webkit",
|
||||
use: {
|
||||
...devices["Desktop Safari"],
|
||||
storageState: "playwright/.auth/user.json",
|
||||
},
|
||||
dependencies: ["setup"],
|
||||
testMatch: /.*\.spec\.ts/,
|
||||
},
|
||||
],
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user