Compare commits

...

37 Commits

Author SHA1 Message Date
Aaron Heckmann 7ba77abe61 test(integration): fix password step 2025-06-06 10:43:42 -07:00
Surya Prashanth 065bf85a25 update minimum node version to 18 2025-06-06 10:14:44 -07:00
Surya Prashanth fcda9c5343 tests: add integration tests using playwright 2025-06-06 10:14:44 -07:00
sriram veeraghanta 9ff238816b sync: canary changes to preview 2025-06-06 18:06:51 +05:30
sriram veeraghanta 6bd5caf008 chore: upgrade package version 2025-06-06 17:50:31 +05:30
sriram veeraghanta c021aff58f chore: django version upgrade 2025-06-06 16:04:34 +05:30
sriram veeraghanta 683be55883 chore: upgrade nextjs version 2025-06-06 16:02:56 +05:30
Manish Gupta 970ce8cf26 [INFRA-183] feat: add restore-airgapped script to build workflow (#7170)
* [WEB-4260] chore: add restore-airgapped script to build workflow

* docs: update restore instructions in README for self-hosted and commercial air-gapped versions

* fix: update restore script filename and improve error handling in restore-airgapped script
2025-06-06 15:24:43 +05:30
Manish Gupta cbbe1a4e4d refactor: Enhance backup and restore scripts for container data (#7055)
* refactor: enhance backup and restore scripts for container data management

* fix: ensure proper quoting in backup script to handle paths with spaces

* fix: ensure backup directory is only removed if tar command succeeds

* CodeRabbit fixes
2025-06-06 15:24:43 +05:30
Manish Gupta 6a74677cc9 fix: update API service startup check to use HTTP request instead of logs (#7054) 2025-06-06 15:24:43 +05:30
sriram veeraghanta f6ea4f931d Merge branch 'canary' of github.com:makeplane/plane into preview 2025-06-06 15:23:10 +05:30
Aaryan Khandelwal 950fcfdb40 [WIKI-391] chore: handle deactivated user display name in version history #7171 2025-06-06 15:04:00 +05:30
Bavisetti Narayan 053c895120 [WEB 4252] chore: updated the favicon request for work item link (#7173)
* chore: added the favicon to link

* chore: added none validation for soup
2025-06-06 15:02:00 +05:30
Aaryan Khandelwal 245167e8aa refactor: unused components, hooks, constants (#7157)
* refactor: remove unused dashboard components and fetch keys

* refactor: remove unused hooks and wrappers

* chore: remove unused function
2025-06-06 14:09:56 +05:30
Vamsi Krishna 6be3f0ea73 [WEB-4208]chore: refactored work item quick actions (#7136)
* chore: refactored work item quick actions

* chore: update event handling for menu

* chore: reverted unwanted changes

* fix: update archive copy link

* chore: handled undefined function implementation
2025-06-06 13:21:00 +05:30
JayashTripathy 14d2d69120 [WEB-4230] refactor: Analytics code refacor, Removal of nivo charts dependencies and translations (#7131)
* chore: added code split for the analytics store

* chore: done some refactor

* refactor: update entity keys in analytics and translations

* chore: updated the translations

* refactor: simplify AnalyticsStoreV2 class by removing unnecessary constructor

* feat: add AnalyticsStoreV2 class and interface for enhanced analytics functionality

* feat: enhance WorkItemsModal and analytics store with isEpic functionality

* feat: integrate isEpic state into TotalInsights and WorkItemsModal components

* refactor: remove isEpic state from WorkItemsModalMainContent component

* refactor: removed old  analytics components and related services

* refactor: new analytics

* refactor: removed all nivo chart dependencies

* chore: resolved coderabbit comments

* fix: update processUrl to handle custom-work-items in peek view

* feat: implement CSV export functionality in InsightTable component

* feat: enhance analytics service with filter parameters and improve data handling in InsightTable

* feat: add new translation keys for various statuses across multiple languages

* [WEB-4246] fix: enhance analytics components to include 'isEpic' parameter for improved data fetching

* chore: update yarn.lock to remove deprecated @nivo packages and clean up unused dependencies
2025-06-06 01:53:38 +05:30
Anmol Singh Bhatia 570a9e319e [WEB-4257] chore: user profile setting options updated #7166 2025-06-06 01:47:31 +05:30
Anmol Singh Bhatia 469a027bb6 [WEB-4274] fix: metadata base url warning #7175 2025-06-05 22:51:56 +05:30
Prateek Shourya 8c99a7df88 [WEB-4273] fix: plans comparison scroll issue (#7176) 2025-06-05 22:51:05 +05:30
Prateek Shourya f34f078bd2 [WEB-4272] fix: remove duplicate CommandPalette instances from settings layouts to prevent modal conflicts (#7174) 2025-06-05 20:48:36 +05:30
Anmol Singh Bhatia 0fe2549bc6 [WEB-4256] chore: add og image and update meta tags for social media compatibility (#7165)
* chore: og image added

* chore: meta config for cross-platform support
2025-06-05 19:32:11 +05:30
Prateek Shourya 118964de01 [WEB-4254] fix: ensure user details are available in project member details computation (#7162) 2025-06-05 19:31:07 +05:30
Manish Gupta 9f37f1ef0e [INFRA-183] feat: add restore-airgapped script to build workflow (#7170)
* [WEB-4260] chore: add restore-airgapped script to build workflow

* docs: update restore instructions in README for self-hosted and commercial air-gapped versions

* fix: update restore script filename and improve error handling in restore-airgapped script
2025-06-05 17:27:57 +05:30
Prateek Shourya 986f29d1f2 [WEB-4253] improvement: plan card enhancements (#7168)
* [WEB-4253] improvement: plan card enhancements

* improvement: pricing changes
2025-06-05 14:37:26 +05:30
Aaryan Khandelwal 1113f9fc19 [WIKI-412] regression: drop plugin logic #7161 2025-06-04 19:07:49 +05:30
Prateek Shourya ef3ec7274c [WEB-4253] improvement: minor enhancements to billing page (#7160) 2025-06-04 17:29:45 +05:30
Akshita Goyal a0a45b7916 [WEB-4249] fix: settings header css + cta on error page + project member list (#7159)
* fix: settings header css + cta on error page

* [WEB-4249] fix: filter out inactive workspace members from project member list

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2025-06-04 16:38:35 +05:30
Aaryan Khandelwal 2792d48288 [WIKI-412] chore: improved rich text editor extensions handling (#7158)
* chore: code split for rich text editor extensions

* chore: update type

* chore: add missing prop
2025-06-04 15:32:54 +05:30
Anmol Singh Bhatia b2ccca0567 [WEB-3931] chore: maintenance page ux copy (#7135)
* chore: maintenance ux copy translation added

* chore: maintenance ux copy updated

* chore: code refactor
2025-06-04 13:37:58 +05:30
Prateek Shourya 2e822b38e4 [WEB-4240] chore: bump local db version to 1.3 #7154 2025-06-04 13:01:29 +05:30
JayashTripathy e570fe404f [WEB-4182] Fix work item links error messages (#7122)
* fix: backend error message toast when getting error

* fix: toast in small screens
2025-06-03 22:18:26 +05:30
Aaryan Khandelwal 48b613ae66 [WIKI-410] chore: editor translation files #7156 2025-06-03 22:13:56 +05:30
sriram veeraghanta 461e099bbc release: v0.26.0 #6962 2025-04-28 18:24:37 +05:30
sriram veeraghanta 45e25ce18b release: v0.25.3 #6788 2025-03-21 17:26:55 +05:30
sriram veeraghanta 4d88dbaf49 release: v0.25.2 (#6736) 2025-03-11 16:01:20 +05:30
sriram veeraghanta e61ff879c4 release: v0.25.1
* fix: issue activity for project id validation (#6668)

* fix: work item attachment count mutation (#6670)

* updated the action to modify the release build assets (#6669)

* feat: russian translation (#6666)

* chore: ru translation updated (#6672)

* fix: state drop down refactor

* fix: intake work item creation refactor

* fix: cleanup for deprecated functions

* fix: date range picker on cycles and modules list (#6676)

* fix: Handled workspace switcher closing on click

* fix: replaced date range picker with date picker at some places

* chore: add common translation keys (#6688)

* chore: add missing translation keys

* chore: add russian translation keys

* fix: issue activity task (#6689)

* changed github workflow action ubuntu version to `ubuntu-22.04` (#6683)

* chore: update russian translation (#6682)

* chore: update russian translation

* chore: rename issues to work items in russian translation

* [PE-275] chore: editor line spacing variables (#6678)

* chore: variable editor line spacing

* chore: variable list spacing

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* [WEB-3475] fix: cycle dates dropdown (#6690)

* fix: Handled workspace switcher closing on click

* fix: Cycle date picker

* fix: Made onSelect optional in range range component

* fix: module date picker (#6691)

* fix: Handled workspace switcher closing on click

* fix: reverted module date picker changes

* chore: extended sidebar improvement (#6693)

* feat: italian translations (#6692)

* Create translations.json - ITALIAN translation (#6667)

* chore: italian translation updated

* feat: italian translation added

* fix: module end date translation

---------

Co-authored-by: Nicolas Bossi <nicolasbossi@gmail.com>
Co-authored-by: gakshita <akshitagoyal1516@gmail.com>

* fix: attachment item created by (#6695)

* fix: module flicker issue on property updation (#6699)

* [WEB-3477] fix: mutation issue on moving work items for a manually ended cycle (#6696)

* fix: package version update

* fix: esbuild version fix

* fix: package license repliation

* [WEB-3488] improvement: assignee validation for work item creation (#6701)

* fix: work item assignee update validation (#6704)

---------

Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Co-authored-by: Nikita Mitasov <32384814+ch4og@users.noreply.github.com>
Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: Akshat Jain <akshatjain9782@gmail.com>
Co-authored-by: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Nicolas Bossi <nicolasbossi@gmail.com>
Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2025-03-05 19:15:33 +05:30
sriram veeraghanta adeb7d977d Merge pull request #6665 from makeplane/canary
fix: package version update
2025-02-24 20:40:25 +05:30
301 changed files with 3844 additions and 8131 deletions
+1
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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"
+7 -1
View File
@@ -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,
+2 -2
View File
@@ -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")
)
+7 -1
View File
@@ -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
+36 -44
View File
@@ -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()
+26 -1
View File
@@ -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>
+144
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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"];
-81
View File
@@ -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",
];
+178
View File
@@ -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 -2
View File
@@ -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";
+7 -7
View File
@@ -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 -1
View File
@@ -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,
}),
+16 -5
View File
@@ -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,
+1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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,
+1
View File
@@ -31,6 +31,7 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
export enum ETranslationFiles {
TRANSLATIONS = "translations",
ACCESSIBILITY = "accessibility",
EDITOR = "editor",
}
export const LANGUAGE_STORAGE_KEY = "userLanguage";
+1
View File
@@ -0,0 +1 @@
{}
+24 -20
View File
@@ -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."
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
+24 -20
View File
@@ -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."
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
+31 -33
View File
@@ -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."
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
+24 -20
View File
@@ -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."
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
+24 -20
View File
@@ -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&apos;a pas démarré. Cela pourrait être dû au fait qu&apos;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."
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
+24 -20
View File
@@ -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."
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
+24 -20
View File
@@ -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."
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
+24 -20
View File
@@ -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を選択して確認してください。"
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
+24 -20
View File
@@ -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를 선택하세요."
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
+24 -20
View File
@@ -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."
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
+24 -20
View File
@@ -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."
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
+24 -20
View File
@@ -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, чтобы убедиться."
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
+24 -20
View File
@@ -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&apos;u seçin."
}
}
}
+1
View File
@@ -0,0 +1 @@
{}
+24 -20
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/propel",
"version": "0.26.0",
"version": "0.26.1",
"private": true,
"license": "AGPL-3.0",
"scripts": {
+1 -1
View File
@@ -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
View File
@@ -1 +0,0 @@
export * from "./analytics.service";
-1
View File
@@ -1,5 +1,4 @@
export * from "./ai";
export * from "./analytics";
export * from "./developer";
export * from "./auth";
export * from "./cycle";
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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",
-55
View File
@@ -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;
}
+50 -106
View File
@@ -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;
}
+1 -1
View File
@@ -43,4 +43,4 @@ export * from "./home";
export * from "./stickies";
export * from "./utils";
export * from "./payment";
export * from "./analytics-v2";
export * from "./analytics";
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/typescript-config",
"version": "0.26.0",
"version": "0.26.1",
"license": "AGPL-3.0",
"private": true,
"files": [
+1 -1
View File
@@ -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",
+223 -36
View File
@@ -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>
</>
);
};
+89 -13
View File
@@ -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>
);
+301 -10
View File
@@ -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 };
+30
View File
@@ -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;
}
+1 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.26.0",
"version": "0.26.1",
"private": true,
"license": "AGPL-3.0",
"scripts": {
+3
View File
@@ -0,0 +1,3 @@
EMAIL=
PASSWORD_BASE64=
BASE_URL=
+8
View File
@@ -0,0 +1,8 @@
# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
playwright/.auth
+110
View File
@@ -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
+14
View File
@@ -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