Compare commits

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

* chore: revert prop changes

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

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

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

* feat: remove sort order calculation

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

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

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

* style: table cell padding

* style: table cell padding

* fix: insert resizable tables

---------

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

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

* style fixes

* style: css and autofocus improved

* fix: fixed weeks for datepicker to ensure static height

---------

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

* chore: remove unused drop indicators
2025-01-28 15:33:25 +05:30
Bavisetti NarayanandGitHub 7fe58e0ea9 chore: added estimate point value in expand issues (#6483) 2025-01-28 15:11:33 +05:30
Prateek ShouryaandGitHub 7f22cd1ac1 [WEB-3229] fix: issue creation from modal (#6460) 2025-01-27 13:13:32 +05:30
Prateek ShouryaandGitHub e2550e0b2d [WEB-3201] improvement: minor enhancements for tree map text size and color (#6458) 2025-01-24 19:20:44 +05:30
Aaryan KhandelwalandGitHub b016ed78cf [PE-248] regression: recent widgets refactor for scalability (#6456)
* fix: recent widgets types and filters

* fix: recent widgets types
2025-01-24 17:23:15 +05:30
c429ca7b36 refactor: recents widget for scalability (#6453)
Co-authored-by: pushya22 <130810100+pushya22@users.noreply.github.com>
2025-01-24 15:34:28 +05:30
Bavisetti NarayanandGitHub ee22dbba1b chore: added a condition to restrict duplicate user creation (#6447) 2025-01-24 15:33:08 +05:30
Vamsi KrishnaandGitHub f4a208bd44 fix: handled label overflow in modules (#6451) 2025-01-24 15:32:44 +05:30
Aaryan KhandelwalandGitHub 8edff26ccd fix: space app editor colors (#6446) 2025-01-24 15:27:57 +05:30
Aaryan KhandelwalandGitHub d08c03f557 [WEB-3203] fix: dashboard widgets' empty state content and assets (#6450)
* fix: empty state content

* chore: replace margin with padding
2025-01-24 15:23:41 +05:30
Prateek ShouryaandGitHub 0b53912295 [WEB-3207] chore: add state_id, priority and assignee_ids to create issue relation response (#6448) 2025-01-23 14:16:06 +05:30
Prateek ShouryaandGitHub 586a320d86 fix: minor fixes for issue relations list and retrival (#6444) 2025-01-23 12:42:35 +05:30
Vamsi KrishnaandGitHub 8f3a0be177 [WEB-3194]fix: stickies modal close while redirection (#6440)
* fix: stickies modal close while redirection

* chore: added deafult for optional prop
2025-01-22 14:22:24 +05:30
Prateek ShouryaandGitHub 0679e140a2 [WEB-3192] fix: issue relation redirection (#6441) 2025-01-22 14:01:21 +05:30
guru_sainathandGitHub b611f5110f chore: issue and issue description version endpoints (#6434) 2025-01-21 20:34:43 +05:30
Akshita GoyalandGitHub 0f7bc6979f fix: new sticky color + recent sticky api call + sticky max height (#6438) 2025-01-21 20:34:00 +05:30
Prateek ShouryaandGitHub 12501d0597 fix: add optional chaning check for actor details (#6437) 2025-01-21 20:33:38 +05:30
Aaryan KhandelwalandGitHub 3a86fff7c1 [WEB-3045] fix: sticky placeholder, gray color value (#6436)
* fix: sticky placeholder

* chore: update gray color
2025-01-21 20:32:45 +05:30
58a4b45463 [WEB-3045] fix: stickies bugs (#6433)
* fix: stickies bugs

* fix: sticky height fixed

---------

Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
2025-01-21 16:37:27 +05:30
Aaryan KhandelwalandGitHub 22836ea03e fix: editor placeholder color (#6430) 2025-01-20 15:52:23 +05:30
Anmol Singh BhatiaandGitHub 13cc8b0e96 chore: workspace view loading state improvement (#6423) 2025-01-17 19:50:56 +05:30
Vamsi KrishnaandGitHub 9addcde553 fix: padding issue cycle active cycles menu (#6424) 2025-01-17 19:46:19 +05:30
Bavisetti NarayanandGitHub 26a9b7fced fix: dashboard completed issues count (#6422) 2025-01-17 18:03:28 +05:30
Vamsi KrishnaandGitHub 62dd80874f fix: issue store condition (#6420) 2025-01-17 16:53:33 +05:30
Anmol Singh BhatiaandGitHub 9ae1ce0a9a chore: helper function added and code refactor (#6419) 2025-01-17 15:41:34 +05:30
Bavisetti NarayanandGitHub 00cc338c07 [WEB-3039] fix: assignee count in dashboard (#6418)
* fix: assignee count in dashboard

* fix: removed the extra filter
2025-01-17 15:24:03 +05:30
Anmol Singh BhatiaandGitHub 4432be15e4 [WEB-3166] chore: global empty state components (#6414)
* chore: detailed and simple empty state component added

* chore: section empty state component added

* chore: asset path helper hook added
2025-01-17 13:52:08 +05:30
Anmol Singh BhatiaandGitHub 20893c6017 fix: draft issue fetch (#6416) 2025-01-17 13:51:17 +05:30
NikhilandGitHub 95f43a7bb6 fix: admin login when the user is not present (#6399) 2025-01-16 19:59:04 +05:30
fd7eedc343 [WEB-3096] feat: stickies page (#6380)
* feat: added independent stickies page

* chore: randomized sticky color

* chore: search in stickies

* feat: dnd

* fix: quick links

* fix: stickies abrupt rendering

* fix: handled edge cases for dnd

* fix: empty states

* fix: build and lint

* fix: handled new sticky when last sticky is emoty

* fix: new sticky condition

* refactor: stickies empty states, store

* chore: update stickies empty states

* fix: random sticky color

* fix: header

* refactor: better error handling

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2025-01-16 19:57:51 +05:30
Aaryan KhandelwalandGitHub d2c9b437f4 [WEB-3164] chore: update tailwindcss version (#6413)
* [WEB-3164] chore: update tailwind version

* chore: add tailwindcss to editor package

* chore: remove tailwindcss package from separate apps
2025-01-16 19:45:05 +05:30
Prateek ShouryaandGitHub 2f57d0e138 fix: add client validation and find similar language from browser config if exact match is not available (#6412) 2025-01-16 17:34:41 +05:30
Prateek ShouryaandGitHub 59ddc02a31 [WEB-3153] improvement: add support for nested translations and ICU formatting (#6411)
* improvement: add support for nested translations and ICU formatting

* chore: comment update
2025-01-16 17:29:57 +05:30
Anmol Singh BhatiaandGitHub 3ac20741d9 fix: issue visibility (#6410) 2025-01-16 15:08:33 +05:30
Aaryan KhandelwalandGitHub 8ea0772a1b fix: page open in new tab action (#6408) 2025-01-16 13:41:02 +05:30
sriram veeraghantaandGitHub bddad8932b feat: Chinese language support (#6407)
* feat: chinese language support

* fix: following iso standards
2025-01-16 13:29:57 +05:30
Vamsi KrishnaandGitHub 8acea7f599 chore: cycle store restructuring (#6405) 2025-01-15 21:05:05 +05:30
a908bf9edd [PE-232] chore: management of disabled extensions (#6317)
* chore: added mobile editor required changes

* fix: turbo.json

---------

Co-authored-by: Lakhan <Lakhanbaheti9@gmail.com>
2025-01-15 20:23:09 +05:30
Vamsi KrishnaandGitHub 79fff4744a chore: modified functionality and placement for live button (#6400) 2025-01-15 16:21:00 +05:30
369d927321 [WEB-3102]fix: transfer issues count (#6384)
* fix: updated cancelled issues count into pending issues

* chore: code refactor

* chore: added pending issues count

* chore: added pending issues count

* chore: added pending_issues to api response

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2025-01-15 16:19:22 +05:30
M. PalanikannanandGitHub 996d11de12 [PE-210] feat: editor performance (#6269)
* bump: upgrade editor

* fix: remove editor ref in use

* fix: added editor state to reduce rerenders

* fix: add editor rerendering optimization

* fix: wrong condition in scroll summary

* fix: removing ref usage internally in read only editor as well

* fix: remove unused methods from read only editor

* fix: add editable prop again

* regression: added the types for onHeadingChange

* fix: types

* fix: improve the check condition
2025-01-15 16:18:49 +05:30
Vamsi KrishnaandGitHub 0345336d90 chore: removed guests from assignees filters (#6402) 2025-01-15 16:05:56 +05:30
Vamsi KrishnaandGitHub 75d14e7c3a fix: removed redundant custom menu (#6388) 2025-01-15 16:00:26 +05:30
Vamsi KrishnaandGitHub 76fdb81249 chore: added current cycle to cycle dropdown (#6376) 2025-01-15 15:59:57 +05:30
Prateek ShouryaandGitHub 88669af141 fix: hide transfer issues option from cycles list when used outside project scope (#6401) 2025-01-15 15:56:35 +05:30
71dcbd938e [WEB-3078] chore: empty state config (#6397)
* chore: empty state config updated

* chore: code refactor

* chore: date range picker icon updated

* fix: tree map content css

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2025-01-15 15:23:30 +05:30
sriram veeraghanta 4060412b18 fix: upgrade django version to fix vulneribility 2025-01-15 14:42:36 +05:30
Zohir TamdaandGitHub 9d715683f7 fix: correcting french translations. (#6383) 2025-01-15 13:56:46 +05:30
sriram veeraghanta 0eb97e32cd fix: ui kit export updated 2025-01-14 18:12:58 +05:30
sriram veeraghanta 3f708e451c fix: update tailwind config package 2025-01-14 17:09:40 +05:30
sriram veeraghanta e962770a5f fix: adding propel ui kit to packages 2025-01-14 16:50:41 +05:30
sriram veeraghanta 4bd6ee5014 fix: setting up shared state package 2025-01-14 16:37:42 +05:30
sriram veeraghanta e9372adcf4 chore: moving web constants to packages 2025-01-14 16:30:43 +05:30
sriram veeraghanta ae3b588081 fix: minor redirection issue on interceptor 2025-01-13 19:54:19 +05:30
Prateek ShouryaandGitHub 7183cffdb7 [WEB-3128] improvement: made tab list items modular for independent use and added few icons (#6387)
* chore: added bar and tree map icons

* improvement: made tab list items modular for independent use
2025-01-13 19:21:01 +05:30
277 changed files with 7444 additions and 5259 deletions
-3
View File
@@ -2,7 +2,4 @@ module.exports = {
root: true,
extends: ["@plane/eslint-config/next.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
+1 -2
View File
@@ -35,19 +35,18 @@
"react-dom": "^18.3.1",
"react-hook-form": "7.51.5",
"swr": "^2.2.4",
"tailwindcss": "3.3.2",
"uuid": "^9.0.1",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@types/node": "18.16.1",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
"@types/uuid": "^9.0.8",
"@types/zxcvbn": "^4.4.4",
"tailwind-config-custom": "*",
"typescript": "5.3.3"
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
const sharedConfig = require("tailwind-config-custom/tailwind.config.js");
/* eslint-disable @typescript-eslint/no-require-imports */
const sharedConfig = require("@plane/tailwind-config/tailwind.config.js");
module.exports = {
presets: [sharedConfig],
@@ -15,3 +15,4 @@ from .state import StateLiteSerializer, StateSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
from .intake import IntakeIssueSerializer
from .estimate import EstimatePointSerializer
+2
View File
@@ -72,6 +72,7 @@ class BaseSerializer(serializers.ModelSerializer):
StateLiteSerializer,
UserLiteSerializer,
WorkspaceLiteSerializer,
EstimatePointSerializer,
)
# Expansion mapper
@@ -88,6 +89,7 @@ class BaseSerializer(serializers.ModelSerializer):
"owned_by": UserLiteSerializer,
"members": UserLiteSerializer,
"parent": IssueLiteSerializer,
"estimate_point": EstimatePointSerializer,
}
# Check if field in expansion then expand the field
if expand in expansion:
@@ -0,0 +1,10 @@
# Module imports
from plane.db.models import EstimatePoint
from .base import BaseSerializer
class EstimatePointSerializer(BaseSerializer):
class Meta:
model = EstimatePoint
fields = ["id", "value"]
read_only_fields = fields
+2
View File
@@ -207,6 +207,7 @@ class IssueSerializer(BaseSerializer):
for assignee_id in assignees
],
batch_size=10,
ignore_conflicts=True,
)
if labels is not None:
@@ -224,6 +225,7 @@ class IssueSerializer(BaseSerializer):
for label_id in labels
],
batch_size=10,
ignore_conflicts=True,
)
# Time updation occues even when other related models are updated
+5
View File
@@ -71,4 +71,9 @@ urlpatterns = [
IssueAttachmentEndpoint.as_view(),
name="attachment",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
IssueAttachmentEndpoint.as_view(),
name="issue-attachment",
),
]
@@ -72,6 +72,8 @@ from .issue import (
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
IssueVersionDetailSerializer,
IssueDescriptionVersionDetailSerializer,
)
from .module import (
+99 -2
View File
@@ -33,6 +33,8 @@ from plane.db.models import (
IssueVote,
IssueRelation,
State,
IssueVersion,
IssueDescriptionVersion,
)
@@ -201,6 +203,7 @@ class IssueCreateSerializer(BaseSerializer):
for user in assignees
],
batch_size=10,
ignore_conflicts=True,
)
if labels is not None:
@@ -218,6 +221,7 @@ class IssueCreateSerializer(BaseSerializer):
for label in labels
],
batch_size=10,
ignore_conflicts=True,
)
# Time updation occues even when other related models are updated
@@ -281,10 +285,26 @@ class IssueRelationSerializer(BaseSerializer):
)
name = serializers.CharField(source="related_issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
state_id = serializers.UUIDField(source="related_issue.state.id", read_only=True)
priority = serializers.CharField(source="related_issue.priority", read_only=True)
assignee_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = IssueRelation
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
"state_id",
"priority",
"assignee_ids",
]
read_only_fields = ["workspace", "project"]
@@ -296,10 +316,26 @@ class RelatedIssueSerializer(BaseSerializer):
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
name = serializers.CharField(source="issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
state_id = serializers.UUIDField(source="issue.state.id", read_only=True)
priority = serializers.CharField(source="issue.priority", read_only=True)
assignee_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = IssueRelation
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
"state_id",
"priority",
"assignee_ids",
]
read_only_fields = ["workspace", "project"]
@@ -667,3 +703,64 @@ class IssueSubscriberSerializer(BaseSerializer):
model = IssueSubscriber
fields = "__all__"
read_only_fields = ["workspace", "project", "issue"]
class IssueVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueVersion
fields = [
"id",
"workspace",
"project",
"issue",
"parent",
"state",
"estimate_point",
"name",
"priority",
"start_date",
"target_date",
"assignees",
"sequence_id",
"labels",
"sort_order",
"completed_at",
"archived_at",
"is_draft",
"external_source",
"external_id",
"type",
"cycle",
"modules",
"meta",
"name",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]
class IssueDescriptionVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueDescriptionVersion
fields = [
"id",
"workspace",
"project",
"issue",
"description_binary",
"description_html",
"description_stripped",
"description_json",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]
@@ -22,6 +22,7 @@ from plane.db.models import (
ProjectMember,
WorkspaceHomePreference,
Sticky,
WorkspaceUserPreference,
)
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
@@ -258,3 +259,10 @@ class StickySerializer(BaseSerializer):
fields = "__all__"
read_only_fields = ["workspace", "owner"]
extra_kwargs = {"name": {"required": False}}
class WorkspaceUserPreferenceSerializer(BaseSerializer):
class Meta:
model = WorkspaceUserPreference
fields = ["key", "is_pinned", "sort_order"]
read_only_fields = ["workspace", "created_by", "updated_by"]
+22
View File
@@ -24,6 +24,8 @@ from plane.app.views import (
IssueDetailEndpoint,
IssueAttachmentV2Endpoint,
IssueBulkUpdateDateEndpoint,
IssueVersionEndpoint,
IssueDescriptionVersionEndpoint,
)
urlpatterns = [
@@ -256,4 +258,24 @@ urlpatterns = [
IssueBulkUpdateDateEndpoint.as_view(),
name="project-issue-dates",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
IssueVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
IssueVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/",
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/<uuid:pk>/",
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
]
+12
View File
@@ -31,6 +31,7 @@ from plane.app.views import (
UserRecentVisitViewSet,
WorkspaceHomePreferenceViewSet,
WorkspaceStickyViewSet,
WorkspaceUserPreferenceViewSet,
)
@@ -258,4 +259,15 @@ urlpatterns = [
),
name="workspace-sticky",
),
# User Preference
path(
"workspaces/<str:slug>/sidebar-preferences/",
WorkspaceUserPreferenceViewSet.as_view(),
name="workspace-user-preference",
),
path(
"workspaces/<str:slug>/sidebar-preferences/<str:key>/",
WorkspaceUserPreferenceViewSet.as_view(),
name="workspace-user-preference",
),
]
+3
View File
@@ -48,6 +48,7 @@ from .workspace.favorite import (
WorkspaceFavoriteGroupEndpoint,
)
from .workspace.recent_visit import UserRecentVisitViewSet
from .workspace.user_preference import WorkspaceUserPreferenceViewSet
from .workspace.member import (
WorkSpaceMemberViewSet,
@@ -141,6 +142,8 @@ from .issue.sub_issue import SubIssuesEndpoint
from .issue.subscriber import IssueSubscriberViewSet
from .issue.version import IssueVersionEndpoint, IssueDescriptionVersionEndpoint
from .module.base import (
ModuleViewSet,
ModuleLinkViewSet,
+22
View File
@@ -47,6 +47,7 @@ from plane.db.models import (
User,
Project,
ProjectMember,
UserRecentVisit,
)
from plane.utils.analytics_plot import burndown_plot
from plane.bgtasks.recent_visited_task import recent_visited_task
@@ -132,6 +133,18 @@ class CycleViewSet(BaseViewSet):
),
)
)
.annotate(
pending_issues=Count(
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group__in=["backlog", "unstarted", "started"],
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
.annotate(
status=Case(
When(
@@ -214,6 +227,7 @@ class CycleViewSet(BaseViewSet):
"is_favorite",
"total_issues",
"completed_issues",
"pending_issues",
"assignee_ids",
"status",
"version",
@@ -245,6 +259,7 @@ class CycleViewSet(BaseViewSet):
# meta fields
"is_favorite",
"total_issues",
"pending_issues",
"completed_issues",
"assignee_ids",
"status",
@@ -529,6 +544,13 @@ class CycleViewSet(BaseViewSet):
entity_identifier=pk,
project_id=project_id,
).delete()
# Delete the cycle from recent visits
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="cycle",
).delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)
+9 -3
View File
@@ -53,10 +53,10 @@ from .. import BaseAPIView
def dashboard_overview_stats(self, request, slug):
assigned_issues = (
Issue.issue_objects.filter(
(Q(assignees__in=[request.user]) & Q(issue_assignee__deleted_at__isnull=True)),
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
workspace__slug=slug,
assignees__in=[request.user],
)
.filter(
Q(
@@ -133,10 +133,13 @@ def dashboard_overview_stats(self, request, slug):
completed_issues_count = (
Issue.issue_objects.filter(
(
Q(assignees__in=[request.user])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
assignees__in=[request.user],
state__group="completed",
)
.filter(
@@ -176,10 +179,13 @@ def dashboard_assigned_issues(self, request, slug):
# get all the assigned issues
assigned_issues = (
Issue.issue_objects.filter(
(
Q(assignees__in=[request.user])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
assignees__in=[request.user],
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
+8
View File
@@ -44,6 +44,7 @@ from plane.db.models import (
Project,
ProjectMember,
CycleIssue,
UserRecentVisit,
)
from plane.utils.grouper import (
issue_group_values,
@@ -671,6 +672,13 @@ class IssueViewSet(BaseViewSet):
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
issue.delete()
# delete the issue from recent visits
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="issue",
).delete(soft=False)
issue_activity.delay(
type="issue.activity.deleted",
requested_data=json.dumps({"issue_id": str(pk)}),
+118
View File
@@ -0,0 +1,118 @@
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.db.models import IssueVersion, IssueDescriptionVersion
from ..base import BaseAPIView
from plane.app.serializers import (
IssueVersionDetailSerializer,
IssueDescriptionVersionDetailSerializer,
)
from plane.app.permissions import allow_permission, ROLE
from plane.utils.global_paginator import paginate
from plane.utils.timezone_converter import user_timezone_converter
class IssueVersionEndpoint(BaseAPIView):
def process_paginated_result(self, fields, results, timezone):
paginated_data = results.values(*fields)
datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter(
paginated_data, datetime_fields, timezone
)
return paginated_data
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
issue_version = IssueVersion.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
serializer = IssueVersionDetailSerializer(issue_version)
return Response(serializer.data, status=status.HTTP_200_OK)
cursor = request.GET.get("cursor", None)
required_fields = [
"id",
"workspace",
"project",
"issue",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
issue_versions_queryset = IssueVersion.objects.filter(
workspace__slug=slug, project_id=project_id, issue_id=issue_id
)
paginated_data = paginate(
base_queryset=issue_versions_queryset,
queryset=issue_versions_queryset,
cursor=cursor,
on_result=lambda results: self.process_paginated_result(
required_fields, results, request.user.user_timezone
),
)
return Response(paginated_data, status=status.HTTP_200_OK)
class IssueDescriptionVersionEndpoint(BaseAPIView):
def process_paginated_result(self, fields, results, timezone):
paginated_data = results.values(*fields)
datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter(
paginated_data, datetime_fields, timezone
)
return paginated_data
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
issue_description_version = IssueDescriptionVersion.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
serializer = IssueDescriptionVersionDetailSerializer(
issue_description_version
)
return Response(serializer.data, status=status.HTTP_200_OK)
cursor = request.GET.get("cursor", None)
required_fields = [
"id",
"workspace",
"project",
"issue",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
issue_description_versions_queryset = IssueDescriptionVersion.objects.filter(
workspace__slug=slug, project_id=project_id, issue_id=issue_id
)
paginated_data = paginate(
base_queryset=issue_description_versions_queryset,
queryset=issue_description_versions_queryset,
cursor=cursor,
on_result=lambda results: self.process_paginated_result(
required_fields, results, request.user.user_timezone
),
)
return Response(paginated_data, status=status.HTTP_200_OK)
+8
View File
@@ -54,6 +54,7 @@ from plane.db.models import (
ModuleLink,
ModuleUserProperties,
Project,
UserRecentVisit,
)
from plane.utils.analytics_plot import burndown_plot
from plane.utils.timezone_converter import user_timezone_converter
@@ -808,6 +809,13 @@ class ModuleViewSet(BaseViewSet):
entity_identifier=pk,
project_id=project_id,
).delete()
# delete the module from recent visits
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="module",
).delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)
+8
View File
@@ -33,6 +33,7 @@ from plane.db.models import (
ProjectMember,
ProjectPage,
Project,
UserRecentVisit,
)
from plane.utils.error_codes import ERROR_CODES
from ..base import BaseAPIView, BaseViewSet
@@ -387,6 +388,13 @@ class PageViewSet(BaseViewSet):
entity_identifier=pk,
entity_type="page",
).delete()
# Delete the page from recent visit
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="page",
).delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)
+17
View File
@@ -53,6 +53,23 @@ class StateViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def partial_update(self, request, slug, project_id, pk):
try:
state = State.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
serializer = StateSerializer(state, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"name": "The state name is already taken"},
status=status.HTTP_400_BAD_REQUEST,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):
+8
View File
@@ -24,6 +24,7 @@ from plane.db.models import (
ProjectMember,
Project,
CycleIssue,
UserRecentVisit,
)
from plane.utils.grouper import (
issue_group_values,
@@ -495,6 +496,13 @@ class IssueViewViewSet(BaseViewSet):
entity_identifier=pk,
entity_type="view",
).delete()
# Delete the page from recent visit
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="view",
).delete(soft=False)
else:
return Response(
{"error": "Only admin or owner can delete the view"},
@@ -39,9 +39,9 @@ class WorkspaceStickyViewSet(BaseViewSet):
)
def list(self, request, slug):
query = request.query_params.get("query", False)
stickies = self.get_queryset()
stickies = self.get_queryset().order_by("-sort_order")
if query:
stickies = stickies.filter(name__icontains=query)
stickies = stickies.filter(description_stripped__icontains=query)
return self.paginate(
request=request,
@@ -49,7 +49,7 @@ class WorkspaceStickyViewSet(BaseViewSet):
on_results=lambda stickies: StickySerializer(stickies, many=True).data,
default_per_page=20,
)
@allow_permission(allowed_roles=[], creator=True, model=Sticky, level="WORKSPACE")
def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)
+20 -5
View File
@@ -375,8 +375,11 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
state_distribution = (
Issue.issue_objects.filter(
(
Q(assignees__in=[user_id])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
@@ -391,8 +394,11 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
priority_distribution = (
Issue.issue_objects.filter(
(
Q(assignees__in=[user_id])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
@@ -426,8 +432,11 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
assigned_issues_count = (
Issue.issue_objects.filter(
(
Q(assignees__in=[user_id])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
@@ -438,8 +447,11 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
pending_issues_count = (
Issue.issue_objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
(
Q(assignees__in=[user_id])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
@@ -449,8 +461,11 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
completed_issues_count = (
Issue.issue_objects.filter(
(
Q(assignees__in=[user_id])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
assignees__in=[user_id],
state__group="completed",
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
@@ -0,0 +1,78 @@
# Module imports
from ..base import BaseAPIView
from plane.db.models.workspace import WorkspaceUserPreference
from plane.app.serializers.workspace import WorkspaceUserPreferenceSerializer
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import Workspace
# Third party imports
from rest_framework.response import Response
from rest_framework import status
class WorkspaceUserPreferenceViewSet(BaseAPIView):
model = WorkspaceUserPreference
def get_serializer_class(self):
return WorkspaceUserPreferenceSerializer
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
get_preference = WorkspaceUserPreference.objects.filter(
user=request.user, workspace_id=workspace.id
)
create_preference_keys = []
keys = [
key
for key, _ in WorkspaceUserPreference.UserPreferenceKeys.choices
if key not in ["projects"]
]
for preference in keys:
if preference not in get_preference.values_list("key", flat=True):
create_preference_keys.append(preference)
preference = WorkspaceUserPreference.objects.bulk_create(
[
WorkspaceUserPreference(
key=key, user=request.user, workspace=workspace
)
for key in create_preference_keys
],
batch_size=10,
ignore_conflicts=True,
)
preference = WorkspaceUserPreference.objects.filter(
user=request.user, workspace_id=workspace.id
)
return Response(
preference.values("key", "is_pinned", "sort_order"),
status=status.HTTP_200_OK,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def patch(self, request, slug, key):
preference = WorkspaceUserPreference.objects.filter(
key=key, workspace__slug=slug, user=request.user
).first()
if preference:
serializer = WorkspaceUserPreferenceSerializer(
preference, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
{"detail": "Preference not found"}, status=status.HTTP_404_NOT_FOUND
)
-1
View File
@@ -53,7 +53,6 @@ urlpatterns = [
path("magic-generate/", MagicGenerateEndpoint.as_view(), name="magic-generate"),
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
path("magic-sign-up/", MagicSignUpEndpoint.as_view(), name="magic-sign-up"),
path("get-csrf-token/", CSRFTokenEndpoint.as_view(), name="get_csrf_token"),
path(
"spaces/magic-generate/",
MagicGenerateSpaceEndpoint.as_view(),
+2 -1
View File
@@ -69,7 +69,8 @@ from .workspace import (
WorkspaceTheme,
WorkspaceUserProperties,
WorkspaceUserLink,
WorkspaceHomePreference
WorkspaceHomePreference,
WorkspaceUserPreference,
)
from .favorite import UserFavorite
+9
View File
@@ -5,6 +5,9 @@ from django.db import models
# Module imports
from .base import BaseModel
# Third party imports
from plane.utils.html_processor import strip_tags
class Sticky(BaseModel):
name = models.TextField(null=True, blank=True)
@@ -33,6 +36,12 @@ class Sticky(BaseModel):
ordering = ("-created_at",)
def save(self, *args, **kwargs):
# Strip the html tags using html parser
self.description_stripped = (
None
if (self.description_html == "" or self.description_html is None)
else strip_tags(self.description_html)
)
if self._state.adding:
# Get the maximum sequence value from the database
last_id = Sticky.objects.filter(workspace=self.workspace).aggregate(
+2 -3
View File
@@ -388,15 +388,14 @@ class WorkspaceHomePreference(BaseModel):
return f"{self.workspace.name} {self.user.email} {self.key}"
class WorkspaceUserPreference(BaseModel):
"""Preference for the workspace for a user"""
class UserPreferenceKeys(models.TextChoices):
PROJECTS = "projects", "Projects"
ANALYTICS = "analytics", "Analytics"
CYCLES = "cycles", "Cycles"
VIEWS = "views", "Views"
ANALYTICS = "analytics", "Analytics"
PROJECTS = "projects", "Projects"
workspace = models.ForeignKey(
"db.Workspace",
+9 -9
View File
@@ -290,11 +290,12 @@ class InstanceAdminSignInEndpoint(View):
# Fetch the user
user = User.objects.filter(email=email).first()
# is_active
if not user.is_active:
# Error out if the user is not present
if not user:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DEACTIVATED"],
error_message="ADMIN_USER_DEACTIVATED",
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DOES_NOT_EXIST"],
error_message="ADMIN_USER_DOES_NOT_EXIST",
payload={"email": email},
)
url = urljoin(
base_host(request=request, is_admin=True),
@@ -302,12 +303,11 @@ class InstanceAdminSignInEndpoint(View):
)
return HttpResponseRedirect(url)
# Error out if the user is not present
if not user:
# is_active
if not user.is_active:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DOES_NOT_EXIST"],
error_message="ADMIN_USER_DOES_NOT_EXIST",
payload={"email": email},
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DEACTIVATED"],
error_message="ADMIN_USER_DEACTIVATED",
)
url = urljoin(
base_host(request=request, is_admin=True),
+26 -9
View File
@@ -1,8 +1,14 @@
# Python imports
import pytz
from plane.db.models import Project
from datetime import datetime, time
from datetime import timedelta
# Django imports
from django.utils import timezone
# Module imports
from plane.db.models import Project
def user_timezone_converter(queryset, datetime_fields, user_timezone):
# Create a timezone object for the user's timezone
@@ -65,16 +71,27 @@ def convert_to_utc(
if is_start_date:
localized_datetime += timedelta(minutes=0, seconds=1)
# If it's start an end date are equal, add 23 hours, 59 minutes, and 59 seconds
# to make it the end of the day
if is_start_date_end_date_equal:
localized_datetime += timedelta(hours=23, minutes=59, seconds=59)
# Convert the localized datetime to UTC
utc_datetime = localized_datetime.astimezone(pytz.utc)
# Convert the localized datetime to UTC
utc_datetime = localized_datetime.astimezone(pytz.utc)
current_datetime_in_project_tz = timezone.now().astimezone(local_tz)
current_datetime_in_utc = current_datetime_in_project_tz.astimezone(pytz.utc)
# Return the UTC datetime for storage
return utc_datetime
if utc_datetime.date() == current_datetime_in_utc.date():
return current_datetime_in_utc
return utc_datetime
else:
# If it's start an end date are equal, add 23 hours, 59 minutes, and 59 seconds
# to make it the end of the day
if is_start_date_end_date_equal:
localized_datetime += timedelta(hours=23, minutes=59, seconds=59)
# Convert the localized datetime to UTC
utc_datetime = localized_datetime.astimezone(pytz.utc)
# Return the UTC datetime for storage
return utc_datetime
def convert_utc_to_project_timezone(utc_datetime, project_id):
+1 -1
View File
@@ -1,7 +1,7 @@
# base requirements
# django
Django==4.2.17
Django==4.2.18
# rest framework
djangorestframework==3.15.2
# postgres
+8 -7
View File
@@ -131,17 +131,18 @@ function updateEnvFile() {
fi
if [ -f "$file" ]; then
# check if key exists in the file
grep -q "^$key=" "$file"
if [ $? -ne 0 ]; then
# Check if key exists in the file
if ! grep -q "^$key=" "$file"; then
echo "$key=$value" >> "$file"
return
else
else
# Escape special characters in value for sed
value=$(printf '%s\n' "$value" | sed -e 's/[\/&]/\\&/g')
if [ "$OS_NAME" == "Darwin" ]; then
value=$(echo "$value" | sed 's/|/\\|/g')
sed -i '' "s|^$key=.*|$key=$value|g" "$file"
sed -i '' "s|^$key=.*|$key=$value|" "$file"
else
sed -i "s/^$key=.*/$key=$value/g" "$file"
sed -i "s|^$key=.*|$key=$value|" "$file"
fi
fi
else
+6 -6
View File
@@ -16,10 +16,10 @@
"author": "",
"license": "ISC",
"dependencies": {
"@hocuspocus/extension-database": "^2.11.3",
"@hocuspocus/extension-logger": "^2.11.3",
"@hocuspocus/extension-redis": "^2.13.5",
"@hocuspocus/server": "^2.11.3",
"@hocuspocus/extension-database": "^2.15.0",
"@hocuspocus/extension-logger": "^2.15.0",
"@hocuspocus/extension-redis": "^2.15.0",
"@hocuspocus/server": "^2.15.0",
"@plane/constants": "*",
"@plane/editor": "*",
"@plane/types": "*",
@@ -40,9 +40,9 @@
"pino-http": "^10.3.0",
"pino-pretty": "^11.2.2",
"uuid": "^10.0.0",
"y-prosemirror": "^1.2.9",
"y-prosemirror": "^1.2.15",
"y-protocols": "^1.0.6",
"yjs": "^13.6.14"
"yjs": "^13.6.20"
},
"devDependencies": {
"@babel/cli": "^7.25.6",
+78
View File
@@ -0,0 +1,78 @@
// types
import { TXAxisValues, TYAxisValues } from "@plane/types";
export const ANALYTICS_TABS = [
{ key: "scope_and_demand", title: "Scope and Demand" },
{ key: "custom", title: "Custom Analytics" },
];
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: "Issue Count",
},
{
value: "estimate",
label: "Estimate",
},
];
export const ANALYTICS_DATE_KEYS = [
"completed_at",
"target_date",
"start_date",
"created_at",
];
+10
View File
@@ -0,0 +1,10 @@
export const ISSUE_REACTION_EMOJI_CODES = [
"128077",
"128078",
"128516",
"128165",
"128533",
"129505",
"9992",
"128064",
];
+4
View File
@@ -23,3 +23,7 @@ export const WEBSITE_URL =
// support email
export const SUPPORT_EMAIL =
process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "support@plane.so";
// marketing links
export const MARKETING_PRICING_PAGE_LINK = "https://plane.so/pricing";
export const MARKETING_CONTACT_US_PAGE_LINK = "https://plane.so/contact";
export const MARKETING_PLANE_ONE_PAGE_LINK = "https://plane.so/one";
+5
View File
@@ -0,0 +1,5 @@
export enum E_ARCHIVE_ERROR_CODES {
"INVALID_ARCHIVE_STATE_GROUP" = 4091,
"INVALID_ISSUE_START_DATE" = 4101,
"INVALID_ISSUE_TARGET_DATE" = 4102,
}
+4
View File
@@ -0,0 +1,4 @@
export enum E_SORT_ORDER {
ASC = "asc",
DESC = "desc",
}
@@ -1,7 +1,4 @@
// nivo
import { Theme } from "@nivo/core";
export const CHARTS_THEME: Theme = {
export const CHARTS_THEME = {
background: "transparent",
text: {
color: "rgb(var(--color-text-200))",
@@ -29,7 +26,7 @@ export const CHARTS_THEME: Theme = {
},
};
export const DEFAULT_MARGIN = {
export const CHART_DEFAULT_MARGIN = {
top: 50,
right: 50,
bottom: 50,
+4
View File
@@ -1,8 +1,11 @@
export * from "./ai";
export * from "./analytics";
export * from "./auth";
export * from "./endpoints";
export * from "./event";
export * from "./file";
export * from "./filter";
export * from "./graph";
export * from "./instance";
export * from "./issue";
export * from "./metadata";
@@ -10,3 +13,4 @@ export * from "./state";
export * from "./swr";
export * from "./user";
export * from "./workspace";
export * from "./stickies";
+23
View File
@@ -43,3 +43,26 @@ export const ARCHIVABLE_STATE_GROUPS = [
STATE_GROUPS.completed.key,
STATE_GROUPS.cancelled.key,
];
export const PROGRESS_STATE_GROUPS_DETAILS = [
{
key: "completed_issues",
title: "Completed",
color: "#16A34A",
},
{
key: "started_issues",
title: "Started",
color: "#F59E0B",
},
{
key: "unstarted_issues",
title: "Unstarted",
color: "#3A3A3A",
},
{
key: "backlog_issues",
title: "Backlog",
color: "#A3A3A3",
},
];
+1
View File
@@ -0,0 +1 @@
export const STICKIES_PER_PAGE = 30;
+7 -9
View File
@@ -12,14 +12,12 @@
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"module": "./dist/index.mjs"
"import": "./dist/index.mjs"
},
"./lib": {
"require": "./dist/lib.js",
"types": "./dist/lib.d.mts",
"import": "./dist/lib.mjs",
"module": "./dist/lib.mjs"
"import": "./dist/lib.mjs"
}
},
"scripts": {
@@ -36,7 +34,7 @@
},
"dependencies": {
"@floating-ui/react": "^0.26.4",
"@hocuspocus/provider": "^2.13.5",
"@hocuspocus/provider": "^2.15.0",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/utils": "*",
@@ -67,21 +65,21 @@
"prosemirror-codemark": "^0.4.2",
"prosemirror-utils": "^1.2.2",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.9",
"tiptap-markdown": "^0.8.10",
"uuid": "^10.0.0",
"y-indexeddb": "^9.0.12",
"y-prosemirror": "^1.2.5",
"y-prosemirror": "^1.2.15",
"y-protocols": "^1.0.6",
"yjs": "^13.6.15"
"yjs": "^13.6.20"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@types/node": "18.15.3",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
"postcss": "^8.4.38",
"tailwind-config-custom": "*",
"tsup": "^7.2.0",
"typescript": "5.3.3"
},
@@ -1,5 +1,6 @@
import { HocuspocusProvider } from "@hocuspocus/provider";
import { Extensions } from "@tiptap/core";
import { AnyExtension } from "@tiptap/core";
import { SlashCommands } from "@/extensions";
// plane editor types
import { TIssueEmbedConfig } from "@/plane-editor/types";
@@ -13,15 +14,24 @@ type Props = {
userDetails: TUserDetails;
};
type ExtensionConfig = {
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
getExtension: (props: Props) => AnyExtension;
};
const extensionRegistry: ExtensionConfig[] = [
{
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
getExtension: () => SlashCommands({}),
},
];
export const DocumentEditorAdditionalExtensions = (_props: Props) => {
const { disabledExtensions } = _props;
const extensions: Extensions = disabledExtensions?.includes("slash-commands")
? []
: [
SlashCommands({
disabledExtensions,
}),
];
return extensions;
const { disabledExtensions = [] } = _props;
const documentExtensions = extensionRegistry
.filter((config) => config.isEnabled(disabledExtensions))
.map((config) => config.getExtension(_props));
return documentExtensions;
};
@@ -4,7 +4,7 @@ import { TSlashCommandAdditionalOption } from "@/extensions";
import { TExtensions } from "@/types";
type Props = {
disabledExtensions: TExtensions[];
disabledExtensions?: TExtensions[];
};
export const coreEditorAdditionalSlashCommandOptions = (props: Props): TSlashCommandAdditionalOption[] => {
@@ -16,6 +16,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
const {
onTransaction,
aiHandler,
bubbleMenuEnabled = true,
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
@@ -75,8 +76,9 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
return (
<PageRenderer
displayConfig={displayConfig}
aiHandler={aiHandler}
bubbleMenuEnabled={bubbleMenuEnabled}
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassNames}
id={id}
@@ -15,12 +15,13 @@ import { Editor, ReactRenderer } from "@tiptap/react";
// components
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
import { LinkView, LinkViewProps } from "@/components/links";
import { AIFeaturesMenu, BlockMenu } from "@/components/menus";
import { AIFeaturesMenu, BlockMenu, EditorBubbleMenu } from "@/components/menus";
// types
import { TAIHandler, TDisplayConfig } from "@/types";
type IPageRenderer = {
aiHandler?: TAIHandler;
bubbleMenuEnabled: boolean;
displayConfig: TDisplayConfig;
editor: Editor;
editorContainerClassName: string;
@@ -29,7 +30,7 @@ type IPageRenderer = {
};
export const PageRenderer = (props: IPageRenderer) => {
const { aiHandler, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
const { aiHandler, bubbleMenuEnabled, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
// states
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
const [isOpen, setIsOpen] = useState(false);
@@ -141,6 +142,7 @@ export const PageRenderer = (props: IPageRenderer) => {
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
{editor.isEditable && (
<div>
{bubbleMenuEnabled && <EditorBubbleMenu editor={editor} />}
<BlockMenu editor={editor} />
<AIFeaturesMenu menu={aiHandler?.menu} />
</div>
@@ -69,6 +69,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
return (
<PageRenderer
bubbleMenuEnabled={false}
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassName}
@@ -1,6 +1,6 @@
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react";
import { Editor } from "@tiptap/core";
import { Check, Link, Trash } from "lucide-react";
import { Check, Link, Trash2 } from "lucide-react";
import { Dispatch, FC, SetStateAction, useCallback, useRef, useState } from "react";
// plane utils
import { cn } from "@plane/utils";
// helpers
@@ -15,22 +15,26 @@ type Props = {
export const BubbleMenuLinkSelector: FC<Props> = (props) => {
const { editor, isOpen, setIsOpen } = props;
// states
const [error, setError] = useState(false);
// refs
const inputRef = useRef<HTMLInputElement>(null);
const onLinkSubmit = useCallback(() => {
const handleLinkSubmit = useCallback(() => {
const input = inputRef.current;
const url = input?.value;
if (url && isValidHttpUrl(url)) {
if (!input) return;
let url = input.value;
if (!url) return;
if (!url.startsWith("http")) url = `http://${url}`;
if (isValidHttpUrl(url)) {
setLinkEditor(editor, url);
setIsOpen(false);
setError(false);
} else {
setError(true);
}
}, [editor, inputRef, setIsOpen]);
useEffect(() => {
inputRef.current && inputRef.current?.focus();
});
return (
<div className="relative h-full">
<button
@@ -47,52 +51,62 @@ export const BubbleMenuLinkSelector: FC<Props> = (props) => {
e.stopPropagation();
}}
>
<span>Link</span>
Link
<Link className="flex-shrink-0 size-3" />
</button>
{isOpen && (
<div
className="dow-xl fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 animate-in fade-in slide-in-from-top-1"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onLinkSubmit();
}
}}
>
<input
ref={inputRef}
type="url"
placeholder="Paste a link"
onClick={(e) => {
e.stopPropagation();
}}
className="flex-1 border-r border-custom-border-300 bg-custom-background-100 p-1 text-sm outline-none placeholder:text-custom-text-400"
defaultValue={editor.getAttributes("link").href || ""}
/>
{editor.getAttributes("link").href ? (
<button
type="button"
className="flex items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800"
onClick={(e) => {
unsetLinkEditor(editor);
setIsOpen(false);
e.stopPropagation();
<div className="fixed top-full z-[99999] mt-1 w-60 animate-in fade-in slide-in-from-top-1 rounded bg-custom-background-100 shadow-custom-shadow-rg">
<div
className={cn("flex rounded border border-custom-border-300 transition-colors", {
"border-red-500": error,
})}
>
<input
ref={inputRef}
type="url"
placeholder="Enter or paste a link"
onClick={(e) => e.stopPropagation()}
className="flex-1 border-r border-custom-border-300 bg-custom-background-100 py-2 px-1.5 text-xs outline-none placeholder:text-custom-text-400 rounded"
defaultValue={editor.getAttributes("link").href || ""}
onKeyDown={(e) => {
setError(false);
if (e.key === "Enter") {
e.preventDefault();
handleLinkSubmit();
}
}}
>
<Trash className="h-4 w-4" />
</button>
) : (
<button
className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90"
type="button"
onClick={(e) => {
onLinkSubmit();
e.stopPropagation();
}}
>
<Check className="h-4 w-4" />
</button>
onFocus={() => setError(false)}
autoFocus
/>
{editor.getAttributes("link").href ? (
<button
type="button"
className="grid place-items-center rounded-sm p-1 text-red-500 hover:bg-red-500/20 transition-all"
onClick={(e) => {
unsetLinkEditor(editor);
setIsOpen(false);
e.stopPropagation();
}}
>
<Trash2 className="size-4" />
</button>
) : (
<button
type="button"
className="h-full aspect-square grid place-items-center p-1 rounded-sm text-custom-text-300 hover:bg-custom-background-80 transition-all"
onClick={(e) => {
e.stopPropagation();
handleLinkSubmit();
}}
>
<Check className="size-4" />
</button>
)}
</div>
{error && (
<p className="text-xs text-red-500 my-1 px-2 pointer-events-none animate-in fade-in slide-in-from-top-0">
Please enter a valid URL
</p>
)}
</div>
)}
@@ -202,8 +202,7 @@ export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({
key: "image",
name: "Image",
isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"),
command: ({ savedSelection }) =>
insertImage({ editor, event: "insert", pos: savedSelection?.from ?? editor.state.selection.from }),
command: () => insertImage({ editor, event: "insert", pos: editor.state.selection.from }),
icon: ImageIcon,
});
@@ -93,6 +93,19 @@ export const CustomColorExtension = Mark.create({
};
},
addStorage() {
return {
markdown: {
serialize: {
open: "",
close: "",
mixable: true,
expelEnclosingWhitespace: true,
},
},
};
},
parseHTML() {
return [
{
@@ -134,10 +134,6 @@ const SideMenu = (options: SideMenuPluginProps) => {
rect.left -= 8;
}
if (node.parentElement?.matches("td") || node.parentElement?.matches("th")) {
rect.left += 8;
}
rect.width = options.dragHandleWidth;
if (!editorSideMenu) return;
@@ -39,11 +39,11 @@ import {
setText,
} from "@/helpers/editor-commands";
// types
import { CommandProps, ISlashCommandItem, TExtensions, TSlashCommandSectionKeys } from "@/types";
import { CommandProps, ISlashCommandItem, TSlashCommandSectionKeys } from "@/types";
// plane editor extensions
import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions";
// local types
import { TSlashCommandAdditionalOption } from "./root";
import { TExtensionProps } from "./root";
export type TSlashCommandSection = {
key: TSlashCommandSectionKeys;
@@ -51,13 +51,8 @@ export type TSlashCommandSection = {
items: ISlashCommandItem[];
};
type TArgs = {
additionalOptions?: TSlashCommandAdditionalOption[];
disabledExtensions: TExtensions[];
};
export const getSlashCommandFilteredSections =
(args: TArgs) =>
(args: TExtensionProps) =>
({ query }: { query: string }): TSlashCommandSection[] => {
const { additionalOptions, disabledExtensions } = args;
const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [
@@ -103,9 +103,9 @@ const renderItems = () => {
};
};
type TExtensionProps = {
export type TExtensionProps = {
additionalOptions?: TSlashCommandAdditionalOption[];
disabledExtensions: TExtensions[];
disabledExtensions?: TExtensions[];
};
export const SlashCommands = (props: TExtensionProps) =>
@@ -39,7 +39,12 @@ export interface TableOptions {
declare module "@tiptap/core" {
interface Commands<ReturnType> {
table: {
insertTable: (options?: { rows?: number; cols?: number; withHeaderRow?: boolean }) => ReturnType;
insertTable: (options?: {
rows?: number;
cols?: number;
withHeaderRow?: boolean;
columnWidth?: number;
}) => ReturnType;
addColumnBefore: () => ReturnType;
addColumnAfter: () => ReturnType;
deleteColumn: () => ReturnType;
@@ -108,9 +113,9 @@ export const Table = Node.create({
addCommands() {
return {
insertTable:
({ rows = 3, cols = 3, withHeaderRow = false } = {}) =>
({ rows = 3, cols = 3, withHeaderRow = false, columnWidth = 150 } = {}) =>
({ tr, dispatch, editor }) => {
const node = createTable(editor.schema, rows, cols, withHeaderRow);
const node = createTable(editor.schema, rows, cols, withHeaderRow, undefined, columnWidth);
if (dispatch) {
const offset = tr.selection.anchor + 1;
@@ -2,11 +2,12 @@ import { Fragment, Node as ProsemirrorNode, NodeType } from "@tiptap/pm/model";
export function createCell(
cellType: NodeType,
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
attrs?: Record<string, any>
): ProsemirrorNode | null | undefined {
if (cellContent) {
return cellType.createChecked(null, cellContent);
return cellType.createChecked(attrs, cellContent);
}
return cellType.createAndFill();
return cellType.createAndFill(attrs);
}
@@ -8,21 +8,22 @@ export function createTable(
rowsCount: number,
colsCount: number,
withHeaderRow: boolean,
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
columnWidth: number = 100
): ProsemirrorNode {
const types = getTableNodeTypes(schema);
const headerCells: ProsemirrorNode[] = [];
const cells: ProsemirrorNode[] = [];
for (let index = 0; index < colsCount; index += 1) {
const cell = createCell(types.cell, cellContent);
const cell = createCell(types.cell, cellContent, { colwidth: [columnWidth] });
if (cell) {
cells.push(cell);
}
if (withHeaderRow) {
const headerCell = createCell(types.header_cell, cellContent);
const headerCell = createCell(types.header_cell, cellContent, { colwidth: [columnWidth] });
if (headerCell) {
headerCells.push(headerCell);
@@ -138,8 +138,9 @@ export const insertTableCommand = (editor: Editor, range?: Range) => {
}
}
}
if (range) editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3 }).run();
else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3 }).run();
if (range)
editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3, columnWidth: 150 }).run();
else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3, columnWidth: 150 }).run();
};
export const insertImage = ({
@@ -1,27 +1,21 @@
import { MutableRefObject } from "react";
import { Selection } from "@tiptap/pm/state";
import { Editor } from "@tiptap/react";
export const insertContentAtSavedSelection = (
editorRef: MutableRefObject<Editor | null>,
content: string,
savedSelection: Selection
) => {
if (!editorRef.current || editorRef.current.isDestroyed) {
export const insertContentAtSavedSelection = (editor: Editor, content: string) => {
if (!editor || editor.isDestroyed) {
console.error("Editor reference is not available or has been destroyed.");
return;
}
if (!savedSelection) {
if (!editor.state.selection) {
console.error("Saved selection is invalid.");
return;
}
const docSize = editorRef.current.state.doc.content.size;
const safePosition = Math.max(0, Math.min(savedSelection.anchor, docSize));
const docSize = editor.state.doc.content.size;
const safePosition = Math.max(0, Math.min(editor.state.selection.anchor, docSize));
try {
editorRef.current.chain().focus().insertContentAt(safePosition, content).run();
editor.chain().focus().insertContentAt(safePosition, content).run();
} catch (error) {
console.error("An error occurred while inserting content at saved selection:", error);
}
+49 -76
View File
@@ -1,12 +1,11 @@
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider";
import { DOMSerializer } from "@tiptap/pm/model";
import { Selection } from "@tiptap/pm/state";
import { EditorProps } from "@tiptap/pm/view";
import { useEditor as useTiptapEditor, Editor, Extensions } from "@tiptap/react";
import { useEditor as useTiptapEditor, Extensions } from "@tiptap/react";
import { useImperativeHandle, MutableRefObject, useEffect } from "react";
import * as Y from "yjs";
// components
import { EditorMenuItem, getEditorMenuItems } from "@/components/menus";
import { getEditorMenuItems } from "@/components/menus";
// extensions
import { CoreEditorExtensions } from "@/extensions";
// helpers
@@ -71,14 +70,12 @@ export const useEditor = (props: CustomEditorProps) => {
provider,
autofocus = false,
} = props;
// states
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
// refs
const editorRef: MutableRefObject<Editor | null> = useRef(null);
const savedSelectionRef = useRef(savedSelection);
const editor = useTiptapEditor(
{
editable,
immediatelyRender: false,
shouldRerenderOnTransaction: false,
autofocus,
editorProps: {
...CoreEditorProps({
@@ -100,8 +97,7 @@ export const useEditor = (props: CustomEditorProps) => {
],
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
onCreate: () => handleEditorReady?.(true),
onTransaction: ({ editor }) => {
setSavedSelection(editor.state.selection);
onTransaction: () => {
onTransaction?.();
},
onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()),
@@ -110,23 +106,17 @@ export const useEditor = (props: CustomEditorProps) => {
[editable]
);
// Update the ref whenever savedSelection changes
useEffect(() => {
savedSelectionRef.current = savedSelection;
}, [savedSelection]);
// Effect for syncing SWR data
useEffect(() => {
// value is null when intentionally passed where syncing is not yet
// supported and value is undefined when the data from swr is not populated
if (value === null || value === undefined) return;
if (value == null) return;
if (editor && !editor.isDestroyed && !editor.storage.imageComponent.uploadInProgress) {
try {
editor.commands.setContent(value, false, { preserveWhitespace: "full" });
const currentSavedSelection = savedSelectionRef.current;
if (currentSavedSelection) {
if (editor.state.selection) {
const docLength = editor.state.doc.content.size;
const relativePosition = Math.min(currentSavedSelection.from, docLength - 1);
const relativePosition = Math.min(editor.state.selection.from, docLength - 1);
editor.commands.setTextSelection(relativePosition);
}
} catch (error) {
@@ -138,46 +128,40 @@ export const useEditor = (props: CustomEditorProps) => {
useImperativeHandle(
forwardedRef,
() => ({
blur: () => editorRef.current?.commands.blur(),
blur: () => editor.commands.blur(),
scrollToNodeViaDOMCoordinates(behavior?: ScrollBehavior, pos?: number) {
const resolvedPos = pos ?? savedSelection?.from;
if (!editorRef.current || !resolvedPos) return;
scrollToNodeViaDOMCoordinates(editorRef.current, resolvedPos, behavior);
const resolvedPos = pos ?? editor.state.selection.from;
if (!editor || !resolvedPos) return;
scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior);
},
getCurrentCursorPosition: () => savedSelection?.from,
getCurrentCursorPosition: () => editor.state.selection.from,
clearEditor: (emitUpdate = false) => {
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
},
setEditorValue: (content: string) => {
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" });
editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
},
setEditorValueAtCursorPosition: (content: string) => {
if (savedSelection) {
insertContentAtSavedSelection(editorRef, content, savedSelection);
if (editor.state.selection) {
insertContentAtSavedSelection(editor, content);
}
},
executeMenuItemCommand: (props) => {
const { itemKey } = props;
const editorItems = getEditorMenuItems(editorRef.current);
const editorItems = getEditorMenuItems(editor);
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
const item = getEditorMenuItem(itemKey);
if (item) {
if (item.key === "image") {
(item as EditorMenuItem<"image">).command({
savedSelection: savedSelectionRef.current,
});
} else {
item.command(props);
}
item.command(props);
} else {
console.warn(`No command found for item: ${itemKey}`);
}
},
isMenuItemActive: (props) => {
const { itemKey } = props;
const editorItems = getEditorMenuItems(editorRef.current);
const editorItems = getEditorMenuItems(editor);
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
const item = getEditorMenuItem(itemKey);
@@ -187,20 +171,20 @@ export const useEditor = (props: CustomEditorProps) => {
},
onHeadingChange: (callback: (headings: IMarking[]) => void) => {
// Subscribe to update event emitted from headers extension
editorRef.current?.on("update", () => {
callback(editorRef.current?.storage.headingList.headings);
editor?.on("update", () => {
callback(editor?.storage.headingList.headings);
});
// Return a function to unsubscribe to the continuous transactions of
// the editor on unmounting the component that has subscribed to this
// method
return () => {
editorRef.current?.off("update");
editor?.off("update");
};
},
getHeadings: () => editorRef?.current?.storage.headingList.headings,
getHeadings: () => editor?.storage.headingList.headings,
onStateChange: (callback: () => void) => {
// Subscribe to editor state changes
editorRef.current?.on("transaction", () => {
editor?.on("transaction", () => {
callback();
});
@@ -208,17 +192,17 @@ export const useEditor = (props: CustomEditorProps) => {
// the editor on unmounting the component that has subscribed to this
// method
return () => {
editorRef.current?.off("transaction");
editor?.off("transaction");
};
},
getMarkDown: (): string => {
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown();
const markdownOutput = editor?.storage.markdown.getMarkdown();
return markdownOutput;
},
getDocument: () => {
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
const documentJSON = editorRef.current?.getJSON() ?? null;
const documentHTML = editor?.getHTML() ?? "<p></p>";
const documentJSON = editor.getJSON() ?? null;
return {
binary: documentBinary,
@@ -227,19 +211,19 @@ export const useEditor = (props: CustomEditorProps) => {
};
},
scrollSummary: (marking: IMarking): void => {
if (!editorRef.current) return;
scrollSummary(editorRef.current, marking);
if (!editor) return;
scrollSummary(editor, marking);
},
isEditorReadyToDiscard: () => editorRef.current?.storage.imageComponent.uploadInProgress === false,
isEditorReadyToDiscard: () => editor?.storage.imageComponent.uploadInProgress === false,
setFocusAtPosition: (position: number) => {
if (!editorRef.current || editorRef.current.isDestroyed) {
if (!editor || editor.isDestroyed) {
console.error("Editor reference is not available or has been destroyed.");
return;
}
try {
const docSize = editorRef.current.state.doc.content.size;
const docSize = editor.state.doc.content.size;
const safePosition = Math.max(0, Math.min(position, docSize));
editorRef.current
editor
.chain()
.insertContentAt(safePosition, [{ type: "paragraph" }])
.focus()
@@ -249,17 +233,17 @@ export const useEditor = (props: CustomEditorProps) => {
}
},
getSelectedText: () => {
if (!editorRef.current) return null;
if (!editor) return null;
const { state } = editorRef.current;
const { state } = editor;
const { from, to, empty } = state.selection;
if (empty) return null;
const nodesArray: string[] = [];
state.doc.nodesBetween(from, to, (node, _pos, parent) => {
if (parent === state.doc && editorRef.current) {
const serializer = DOMSerializer.fromSchema(editorRef.current?.schema);
if (parent === state.doc && editor) {
const serializer = DOMSerializer.fromSchema(editor.schema);
const dom = serializer.serializeNode(node);
const tempDiv = document.createElement("div");
tempDiv.appendChild(dom);
@@ -270,28 +254,21 @@ export const useEditor = (props: CustomEditorProps) => {
return selection;
},
insertText: (contentHTML, insertOnNextLine) => {
if (!editorRef.current) return;
// get selection
const { from, to, empty } = editorRef.current.state.selection;
if (!editor) return;
const { from, to, empty } = editor.state.selection;
if (empty) return;
if (insertOnNextLine) {
// move cursor to the end of the selection and insert a new line
editorRef.current
.chain()
.focus()
.setTextSelection(to)
.insertContent("<br />")
.insertContent(contentHTML)
.run();
editor.chain().focus().setTextSelection(to).insertContent("<br />").insertContent(contentHTML).run();
} else {
// replace selected text with the content provided
editorRef.current.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
}
},
getDocumentInfo: () => ({
characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editorRef?.current?.state),
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
characters: editor?.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editor?.state),
words: editor?.storage?.characterCount?.words?.() ?? 0,
}),
setProviderDocument: (value) => {
const document = provider?.document;
@@ -301,16 +278,12 @@ export const useEditor = (props: CustomEditorProps) => {
emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message),
listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
}),
[editorRef, savedSelection]
[editor]
);
if (!editor) {
return null;
}
// the editorRef is used to access the editor instance from outside the hook
// and should only be used after editor is initialized
editorRef.current = editor;
return editor;
};
@@ -1,7 +1,7 @@
import { useImperativeHandle, useRef, MutableRefObject, useEffect } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider";
import { EditorProps } from "@tiptap/pm/view";
import { useEditor as useCustomEditor, Editor, Extensions } from "@tiptap/react";
import { useEditor as useTiptapEditor, Extensions } from "@tiptap/react";
import { useImperativeHandle, MutableRefObject, useEffect } from "react";
import * as Y from "yjs";
// extensions
import { CoreReadOnlyEditorExtensions } from "@/extensions";
@@ -11,13 +11,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
// props
import { CoreReadOnlyEditorProps } from "@/props";
// types
import type {
EditorReadOnlyRefApi,
TExtensions,
TDocumentEventsServer,
TFileHandler,
TReadOnlyMentionHandler,
} from "@/types";
import type { EditorReadOnlyRefApi, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
interface CustomReadOnlyEditorProps {
disabledExtensions: TExtensions[];
@@ -46,8 +40,10 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
provider,
} = props;
const editor = useCustomEditor({
const editor = useTiptapEditor({
editable: false,
immediatelyRender: true,
shouldRerenderOnTransaction: false,
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
editorProps: {
...CoreReadOnlyEditorProps({
@@ -77,23 +73,21 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: "full" });
}, [editor, initialValue]);
const editorRef: MutableRefObject<Editor | null> = useRef(null);
useImperativeHandle(forwardedRef, () => ({
clearEditor: (emitUpdate = false) => {
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
},
setEditorValue: (content: string) => {
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" });
editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
},
getMarkDown: (): string => {
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown();
const markdownOutput = editor?.storage.markdown.getMarkdown();
return markdownOutput;
},
getDocument: () => {
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
const documentJSON = editorRef.current?.getJSON() ?? null;
const documentHTML = editor?.getHTML() ?? "<p></p>";
const documentJSON = editor?.getJSON() ?? null;
return {
binary: documentBinary,
@@ -102,35 +96,22 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
};
},
scrollSummary: (marking: IMarking): void => {
if (!editorRef.current) return;
scrollSummary(editorRef.current, marking);
if (!editor) return;
scrollSummary(editor, marking);
},
getDocumentInfo: () => ({
characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editorRef?.current?.state),
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
}),
onHeadingChange: (callback: (headings: IMarking[]) => void) => {
// Subscribe to update event emitted from headers extension
editorRef.current?.on("update", () => {
callback(editorRef.current?.storage.headingList.headings);
});
// Return a function to unsubscribe to the continuous transactions of
// the editor on unmounting the component that has subscribed to this
// method
return () => {
editorRef.current?.off("update");
getDocumentInfo: () => {
if (!editor) return;
return {
characters: editor.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editor.state),
words: editor.storage?.characterCount?.words?.() ?? 0,
};
},
emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message),
listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
getHeadings: () => editorRef?.current?.storage.headingList.headings,
}));
if (!editor) {
return null;
}
editorRef.current = editor;
return editor;
};
@@ -88,16 +88,18 @@ export const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
const elements = document.elementsFromPoint(coords.x, coords.y);
for (const elem of elements) {
// Check for table wrapper first
if (elem.matches(".table-wrapper")) {
return elem;
}
if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) {
return elem;
}
// if the element is a <p> tag that is the first child of a td or th
if (
(elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) &&
elem?.textContent?.trim() !== ""
) {
return elem; // Return only if p tag is not empty in td or th
// Skip table cells
if (elem.closest(".table-wrapper")) {
continue;
}
// apply general selector
+6 -5
View File
@@ -86,10 +86,6 @@ export type EditorReadOnlyRefApi = {
paragraphs: number;
words: number;
};
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
getHeadings: () => IMarking[];
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;
};
export interface EditorRefApi extends EditorReadOnlyRefApi {
@@ -105,6 +101,10 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
getSelectedText: () => string | null;
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
setProviderDocument: (value: Uint8Array) => void;
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
getHeadings: () => IMarking[];
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;
}
// editor props
@@ -138,8 +138,9 @@ export interface IRichTextEditor extends IEditorProps {
export interface ICollaborativeDocumentEditor
extends Omit<IEditorProps, "initialValue" | "onChange" | "onEnterKeyPress" | "value"> {
editable: boolean;
aiHandler?: TAIHandler;
bubbleMenuEnabled?: boolean;
editable: boolean;
embedHandler: TEmbedConfig;
handleEditorReady?: (value: boolean) => void;
id: string;
+13 -11
View File
@@ -25,7 +25,7 @@
.ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: rgb(var(--color-text-400));
color: var(--color-placeholder);
pointer-events: none;
height: 0;
}
@@ -34,7 +34,7 @@
.ProseMirror p.is-empty::before {
content: attr(data-placeholder);
float: left;
color: rgb(var(--color-text-400));
color: var(--color-placeholder);
pointer-events: none;
height: 0;
}
@@ -105,14 +105,14 @@ ul[data-type="taskList"] li > div {
}
ul[data-type="taskList"] li > label input[type="checkbox"] {
border: 1px solid rgba(var(--color-border-300)) !important;
border: 1px solid rgba(var(--color-text-100), 0.2) !important;
outline: none;
border-radius: 2px;
transform: scale(1.05);
}
.ProseMirror[contenteditable="true"] input[type="checkbox"]:hover {
background-color: rgba(var(--color-background-80));
background-color: rgba(var(--color-text-100), 0.1);
}
.ProseMirror[contenteditable="false"] input[type="checkbox"] {
@@ -145,7 +145,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
position: relative;
-webkit-appearance: none;
appearance: none;
background-color: rgb(var(--color-background-100));
background-color: transparent;
margin: 0;
cursor: pointer;
width: 0.8rem;
@@ -192,7 +192,7 @@ ul[data-type="taskList"] li > div {
ul[data-type="taskList"] li[data-checked="true"] {
& > div > p.editor-paragraph-block {
color: rgb(var(--color-text-400));
color: var(--color-placeholder);
}
[data-text-color] {
@@ -408,12 +408,14 @@ p.editor-paragraph-block {
padding-top: 4px;
}
&:last-child {
padding-bottom: 4px;
}
&:not(td p.editor-paragraph-block, th p.editor-paragraph-block) {
&:last-child {
padding-bottom: 4px;
}
&:not(:last-child) {
padding-bottom: 8px;
&:not(:last-child) {
padding-bottom: 8px;
}
}
font-size: var(--font-size-regular);
+2 -2
View File
@@ -16,7 +16,7 @@
.table-wrapper table th {
min-width: 1em;
border: 1px solid rgba(var(--color-border-200));
padding: 10px 20px;
padding: 7px 10px;
vertical-align: top;
box-sizing: border-box;
position: relative;
@@ -48,7 +48,7 @@
/* table dropdown */
.table-wrapper table .column-resize-handle {
position: absolute;
right: -2px;
right: 0;
top: 0;
width: 2px;
height: 100%;
+2 -38
View File
@@ -1,42 +1,6 @@
:root {
/* text colors */
--editor-colors-gray-text: #5c5e63;
--editor-colors-peach-text: #ff5b59;
--editor-colors-pink-text: #f65385;
--editor-colors-orange-text: #fd9038;
--editor-colors-green-text: #0fc27b;
--editor-colors-light-blue-text: #17bee9;
--editor-colors-dark-blue-text: #266df0;
--editor-colors-purple-text: #9162f9;
/* end text colors */
}
/* text background colors */
[data-theme="light"],
[data-theme="light-contrast"] {
--editor-colors-gray-background: #d6d6d8;
--editor-colors-peach-background: #ffd5d7;
--editor-colors-pink-background: #fdd4e3;
--editor-colors-orange-background: #ffe3cd;
--editor-colors-green-background: #c3f0de;
--editor-colors-light-blue-background: #c5eff9;
--editor-colors-dark-blue-background: #c9dafb;
--editor-colors-purple-background: #e3d8fd;
}
[data-theme="dark"],
[data-theme="dark-contrast"] {
--editor-colors-gray-background: #404144;
--editor-colors-peach-background: #593032;
--editor-colors-pink-background: #562e3d;
--editor-colors-orange-background: #583e2a;
--editor-colors-green-background: #1d4a3b;
--editor-colors-light-blue-background: #1f495c;
--editor-colors-dark-blue-background: #223558;
--editor-colors-purple-background: #3d325a;
}
/* end text background colors */
.editor-container {
--color-placeholder: rgba(var(--color-text-100), 0.5);
/* font sizes and line heights */
&.large-font {
--font-size-h1: 1.75rem;
+1 -1
View File
@@ -1,4 +1,4 @@
const sharedConfig = require("tailwind-config-custom/tailwind.config.js");
const sharedConfig = require("@plane/tailwind-config/tailwind.config.js");
module.exports = {
// prefix ui lib classes to avoid conflicting with the app
+2 -1
View File
@@ -10,7 +10,8 @@
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"@plane/utils": "*"
"@plane/utils": "*",
"intl-messageformat": "^10.7.11"
},
"devDependencies": {
"@plane/eslint-config": "*",
-29
View File
@@ -1,29 +0,0 @@
import { observer } from "mobx-react";
import React, { createContext, useEffect } from "react";
import { Language, languages } from "../config";
import { TranslationStore } from "./store";
// Create the store instance
const translationStore = new TranslationStore();
// Create Context
export const TranslationContext = createContext<TranslationStore>(translationStore);
export const TranslationProvider = observer(({ children }: { children: React.ReactNode }) => {
// Handle storage events for cross-tab synchronization
useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === "userLanguage" && event.newValue) {
const newLang = event.newValue as Language;
if (languages.includes(newLang)) {
translationStore.setLanguage(newLang);
}
}
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, []);
return <TranslationContext.Provider value={translationStore}>{children}</TranslationContext.Provider>;
});
-42
View File
@@ -1,42 +0,0 @@
import { makeObservable, observable } from "mobx";
import { Language, fallbackLng, languages, translations } from "../config";
export class TranslationStore {
currentLocale: Language = fallbackLng;
constructor() {
makeObservable(this, {
currentLocale: observable.ref,
});
this.initializeLanguage();
}
get availableLanguages() {
return languages;
}
t(key: string) {
return translations[this.currentLocale]?.[key] || translations[fallbackLng][key] || key;
}
setLanguage(lng: Language) {
try {
localStorage.setItem("userLanguage", lng);
this.currentLocale = lng;
} catch (error) {
console.error(error);
}
}
initializeLanguage() {
if (typeof window === "undefined") return;
const savedLocale = localStorage.getItem("userLanguage") as Language;
if (savedLocale && languages.includes(savedLocale)) {
this.setLanguage(savedLocale);
} else {
const browserLang = navigator.language.split("-")[0] as Language;
const newLocale = languages.includes(browserLang as Language) ? (browserLang as Language) : fallbackLng;
this.setLanguage(newLocale);
}
}
}
-39
View File
@@ -1,39 +0,0 @@
import en from "../locales/en/translations.json";
import fr from "../locales/fr/translations.json";
import es from "../locales/es/translations.json";
import ja from "../locales/ja/translations.json";
export type Language = (typeof languages)[number];
export type Translations = {
[key: string]: {
[key: string]: string;
};
};
export const fallbackLng = "en";
export const languages = ["en", "fr", "es", "ja"] as const;
export const translations: Translations = {
en,
fr,
es,
ja,
};
export const SUPPORTED_LANGUAGES = [
{
label: "English",
value: "en",
},
{
label: "French",
value: "fr",
},
{
label: "Spanish",
value: "es",
},
{
label: "Japanese",
value: "ja",
},
];
+1
View File
@@ -0,0 +1 @@
export * from "./language";
+13
View File
@@ -0,0 +1,13 @@
import { TLanguage, ILanguageOption } from "../types";
export const FALLBACK_LANGUAGE: TLanguage = "en";
export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "English", value: "en" },
{ label: "Français", value: "fr" },
{ label: "Español", value: "es" },
{ label: "日本語", value: "ja" },
{ label: "中文", value: "zh-CN" },
];
export const STORAGE_KEY = "userLanguage";
+19
View File
@@ -0,0 +1,19 @@
import { observer } from "mobx-react";
import React, { createContext } from "react";
// store
import { TranslationStore } from "../store";
export const TranslationContext = createContext<TranslationStore | null>(null);
interface TranslationProviderProps {
children: React.ReactNode;
}
/**
* Provides the translation store to the application
*/
export const TranslationProvider: React.FC<TranslationProviderProps> = observer(({ children }) => {
const [store] = React.useState(() => new TranslationStore());
return <TranslationContext.Provider value={store}>{children}</TranslationContext.Provider>;
});
+25 -7
View File
@@ -1,17 +1,35 @@
import { useContext } from "react";
import { TranslationContext } from "../components";
import { Language } from "../config";
import { useContext } from 'react';
// context
import { TranslationContext } from '../context';
// types
import { ILanguageOption, TLanguage } from '../types';
export function useTranslation() {
export type TTranslationStore = {
t: (key: string, params?: Record<string, any>) => string;
currentLocale: TLanguage;
changeLanguage: (lng: TLanguage) => void;
languages: ILanguageOption[];
};
/**
* Provides the translation store to the application
* @returns {TTranslationStore}
* @returns {(key: string, params?: Record<string, any>) => string} t: method to translate the key with params
* @returns {TLanguage} currentLocale - current locale language
* @returns {(lng: TLanguage) => void} changeLanguage - method to change the language
* @returns {ILanguageOption[]} languages - available languages
* @throws {Error} if the TranslationProvider is not used
*/
export function useTranslation(): TTranslationStore {
const store = useContext(TranslationContext);
if (!store) {
throw new Error("useTranslation must be used within a TranslationProvider");
throw new Error('useTranslation must be used within a TranslationProvider');
}
return {
t: (key: string) => store.t(key),
t: store.t.bind(store),
currentLocale: store.currentLocale,
changeLanguage: (lng: Language) => store.setLanguage(lng),
changeLanguage: (lng: TLanguage) => store.setLanguage(lng),
languages: store.availableLanguages,
};
}
+3 -2
View File
@@ -1,3 +1,4 @@
export * from "./config";
export * from "./components";
export * from "./constants";
export * from "./context";
export * from "./hooks";
export * from "./types";
+1 -284
View File
@@ -316,288 +316,5 @@
"change_parent_issue": "Change parent issue",
"remove_parent_issue": "Remove parent issue",
"add_parent": "Add parent",
"loading_members": "Loading members...",
"empty_state_workspace_dashboard_title": "Overview of your projects, activity, and metrics",
"empty_state_workspace_dashboard_description": "Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this page will transform into a space that helps you progress. Admins will also see items which help their team progress.",
"empty_state_workspace_dashboard_primary_button": "Build your first project",
"empty_state_workspace_dashboard_comic_title": "Everything starts with a project in Plane",
"empty_state_workspace_dashboard_comic_description": "A project could be a product's roadmap, a marketing campaign, or launching a new car.",
"empty_state_workspace_analytics_title": "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster",
"empty_state_workspace_analytics_description": "See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.",
"empty_state_workspace_analytics_primary_button": "Start your first project",
"empty_state_workspace_analytics_comic_title": "Analytics works best with Cycles + Modules",
"empty_state_workspace_analytics_comic_description": "First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.",
"empty_state_workspace_projects_title": "No active projects",
"empty_state_workspace_projects_description": "Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal. Create a new project or filter for archived projects.",
"empty_state_workspace_projects_primary_button": "Start your first project",
"empty_state_workspace_projects_comic_title": "Everything starts with a project in Plane",
"empty_state_workspace_projects_comic_description": "A project could be a product's roadmap, a marketing campaign, or launching a new car.",
"empty_state_workspace_project_not_found_title": "No such project exists",
"empty_state_workspace_project_not_found_description": "To create issues or manage your work, you need to create a project or be a part of one.",
"empty_state_workspace_project_not_found_primary_button": "Create Project",
"empty_state_workspace_project_not_found_comic_title": "Everything starts with a project in Plane",
"empty_state_workspace_project_not_found_comic_description": "A project could be a product's roadmap, a marketing campaign, or launching a new car.",
"empty_state_workspace_no_projects_title": "No project",
"empty_state_workspace_no_projects_description": "To create issues or manage your work, you need to create a project or be a part of one.",
"empty_state_workspace_no_projects_primary_button": "Start your first project",
"empty_state_workspace_no_projects_comic_title": "Everything starts with a project in Plane",
"empty_state_workspace_no_projects_comic_description": "A project could be a product's roadmap, a marketing campaign, or launching a new car.",
"empty_state_workspace_teams_title": "Teams",
"empty_state_workspace_teams_description": "Teams are groups of people who collaborate on projects. Create a team to get started.",
"empty_state_workspace_teams_primary_button": "Create new team",
"empty_state_workspace_initiatives_title": "Organize work at the highest level with Initiatives",
"empty_state_workspace_initiatives_description": "When you need to organize work spanning several projects and teams, Initiatives come in handy. Connect projects and epics to initiatives, see automatically rolled up updates, and see the forests before you get to the trees.",
"empty_state_workspace_initiatives_primary_button": "Create an initiative",
"empty_state_workspace_initiatives_empty_search_title": "No matching initiatives",
"empty_state_workspace_initiatives_empty_search_description": "No initiatives detected with the matching criteria. \n Create a new initiative instead.",
"empty_state_workspace_all_issues_title": "No issues in the project",
"empty_state_workspace_all_issues_description": "First project done! Now, slice your work into trackable pieces with issues. Let's go!",
"empty_state_workspace_all_issues_primary_button": "Create new issue",
"empty_state_workspace_assigned_title": "No issues yet",
"empty_state_workspace_assigned_description": "Issues assigned to you can be tracked from here.",
"empty_state_workspace_assigned_primary_button": "Create new issue",
"empty_state_workspace_created_title": "No issues yet",
"empty_state_workspace_created_description": "All issues created by you come here, track them here directly.",
"empty_state_workspace_created_primary_button": "Create new issue",
"empty_state_workspace_subscribed_title": "No issues yet",
"empty_state_workspace_subscribed_description": "Subscribe to issues you are interested in, track all of them here.",
"empty_state_workspace_custom_view_title": "No issues yet",
"empty_state_workspace_custom_view_description": "Issues that applies to the filters, track all of them here.",
"empty_state_project_no_epics_title": "Create an epic and assign it to someone, even yourself",
"empty_state_project_no_epics_description": "For larger bodies of work that span several cycles and can live across modules, create an epic. Link issues and sub-issues in a project to an epic and jump into an issue from the overview.",
"empty_state_project_no_epics_primary_button": "Create an Epic",
"empty_state_team_no_issues_title": "Create an issue in your team projects and assign it to someone, even yourself",
"empty_state_team_no_issues_description": "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.",
"empty_state_team_no_issues_primary_button": "Create your first issue",
"empty_state_team_no_issues_comic_title": "Issues are building blocks in Plane.",
"empty_state_team_no_issues_comic_description": "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.",
"empty_state_team_empty_filter_title": "No issues found matching the filters applied",
"empty_state_team_empty_filter_secondary_button": "Clear all filters",
"empty_state_team_view_title": "Save filtered views for your team. Create as many as you need",
"empty_state_team_view_description": "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a team can see everyone's views and choose whichever suits their needs best.",
"empty_state_team_view_primary_button": "Create your first view",
"empty_state_team_view_comic_title": "Views work atop Issue properties.",
"empty_state_team_view_comic_description": "You can create a view from here with as many properties as filters as you see fit.",
"empty_state_workspace_settings_api_tokens_title": "No API tokens created",
"empty_state_workspace_settings_api_tokens_description": "Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started.",
"empty_state_workspace_settings_webhooks_title": "No webhooks added",
"empty_state_workspace_settings_webhooks_description": "Create webhooks to receive real-time updates and automate actions.",
"empty_state_workspace_settings_export_title": "No previous exports yet",
"empty_state_workspace_settings_export_description": "Anytime you export, you will also have a copy here for reference.",
"empty_state_workspace_settings_import_title": "No previous imports yet",
"empty_state_workspace_settings_import_description": "Find all your previous imports here and download them.",
"empty_state_profile_activity_title": "No activities yet",
"empty_state_profile_activity_description": "Get started by creating a new issue! Add details and properties to it. Explore more in Plane to see your activity.",
"empty_state_project_settings_labels_title": "No labels yet",
"empty_state_project_settings_labels_description": "Create labels to help organize and filter issues in you project.",
"empty_state_project_settings_integrations_title": "No integrations configured",
"empty_state_project_settings_integrations_description": "Configure GitHub and other integrations to sync your project issues.",
"empty_state_project_settings_estimate_title": "No estimates added",
"empty_state_project_settings_estimate_description": "Create a set of estimates to communicate the amount of work per issue.",
"empty_state_project_cycles_title": "Group and timebox your work in Cycles.",
"empty_state_project_cycles_description": "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.",
"empty_state_project_cycles_primary_button": "Set your first cycle",
"empty_state_project_cycles_comic_title": "Cycles are repetitive time-boxes.",
"empty_state_project_cycles_comic_description": "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.",
"empty_state_project_cycle_no_issues_title": "No issues added to the cycle",
"empty_state_project_cycle_no_issues_description": "Add or create issues you wish to timebox and deliver within this cycle",
"empty_state_project_cycle_no_issues_primary_button": "Create new issue",
"empty_state_project_cycle_no_issues_secondary_button": "Add an existing issue",
"empty_state_project_cycle_active_title": "No active cycle",
"empty_state_project_cycle_active_description": "An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.",
"empty_state_project_cycle_completed_no_issues_title": "No issues in the cycle",
"empty_state_project_cycle_completed_no_issues_description": "No issues in the cycle. Issues are either transferred or hidden. To see hidden issues if any, update your display properties accordingly.",
"empty_state_command_k_search_empty_state_title": "No results found",
"empty_state_issue_relation_search_empty_state_title": "No matching issues found",
"empty_state_issue_relation_empty_state_title": "No issues found",
"empty_state_issue_comment_empty_state_title": "No comments yet",
"empty_state_issue_comment_empty_state_description": "Comments can be used as a discussion and follow-up space for the issues",
"empty_state_notification_detail_empty_state_title": "Select to view details.",
"empty_state_notification_all_empty_state_title": "No issues assigned",
"empty_state_notification_all_empty_state_description": "Updates for issues assigned to you can be seen here",
"empty_state_notification_mentions_empty_state_title": "No issues assigned",
"empty_state_notification_mentions_empty_state_description": "Updates for issues assigned to you can be seen here",
"empty_state_notification_my_issue_empty_state_title": "No issues assigned",
"empty_state_notification_my_issue_empty_state_description": "Updates for issues assigned to you can be seen here",
"empty_state_notification_created_empty_state_title": "No updates to issues",
"empty_state_notification_created_empty_state_description": "Updates to issues created by you can be seen here",
"empty_state_notification_subscribed_empty_state_title": "No updates to issues",
"empty_state_notification_subscribed_empty_state_description": "Updates to any issue you are subscribed to can be seen here",
"empty_state_notification_unread_empty_state_title": "No unread notifications",
"empty_state_notification_unread_empty_state_description": "Congratulations, you are up-to-date with everything happening in the issues you care about",
"empty_state_notification_snoozed_empty_state_title": "No snoozed notifications yet",
"empty_state_notification_snoozed_empty_state_description": "Any notification you snooze for later will be available here to act upon",
"empty_state_notification_archived_empty_state_title": "No archived notifications yet",
"empty_state_notification_archived_empty_state_description": "Any notification you archive will be available here to help you focus",
"empty_state_active_cycle_progress_empty_state_title": "Add issues to the cycle to view it's progress",
"empty_state_active_cycle_chart_empty_state_title": "Add issues to the cycle to view the burndown chart.",
"empty_state_active_cycle_priority_issue_empty_state_title": "Observe high priority issues tackled in the cycle at a glance.",
"empty_state_active_cycle_assignee_empty_state_title": "Add assignees to issues to see a breakdown of work by assignees.",
"empty_state_active_cycle_label_empty_state_title": "Add labels to issues to see the breakdown of work by labels.",
"empty_state_workspace_active_cycles_title": "No active cycles",
"empty_state_workspace_active_cycles_description": "Cycles of your projects that includes any period that encompasses today's date within its range. Find the progress and details of all your active cycle here.",
"empty_state_disabled_project_inbox_title": "Intake is not enabled for the project.",
"empty_state_disabled_project_inbox_description": "Intake helps you manage incoming requests to your project and add them as issues in your workflow. Enable intake from project settings to manage requests.",
"empty_state_disabled_project_inbox_primary_button": "Manage features",
"empty_state_disabled_project_cycle_title": "Cycles is not enabled for this project.",
"empty_state_disabled_project_cycle_description": "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team. Enable the cycles feature for your project to start using them.",
"empty_state_disabled_project_cycle_primary_button": "Manage features",
"empty_state_disabled_project_module_title": "Modules are not enabled for the project.",
"empty_state_disabled_project_module_description": "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. Enable modules from project settings.",
"empty_state_disabled_project_module_primary_button": "Manage features",
"empty_state_disabled_project_page_title": "Pages are not enabled for the project.",
"empty_state_disabled_project_page_description": "Pages are thought spotting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project's context. Enable the pages feature to start creating them in your project.",
"empty_state_disabled_project_page_primary_button": "Manage features",
"empty_state_disabled_project_view_title": "Views is not enabled for this project.",
"empty_state_disabled_project_view_description": "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone's views and choose whichever suits their needs best. Enable views in the project settings to start using them.",
"empty_state_disabled_project_view_primary_button": "Manage features",
"empty_state_inbox_sidebar_open_tab_title": "No open issues",
"empty_state_inbox_sidebar_open_tab_description": "Find open issues here. Create new issue.",
"empty_state_inbox_sidebar_closed_tab_title": "No closed issues",
"empty_state_inbox_sidebar_closed_tab_description": "All the issues whether accepted or declined can be found here.",
"empty_state_inbox_sidebar_filter_empty_state_title": "No matching issues",
"empty_state_inbox_sidebar_filter_empty_state_description": "No issue matches filter applied in intake. Create a new issue.",
"empty_state_inbox_detail_empty_state_title": "Select an issue to view its details.",
"empty_state_views_empty_search_title": "No matching views",
"empty_state_views_empty_search_description": "No views match the search criteria. Create a new view instead.",
"empty_state_projects_empty_search_title": "No matching projects",
"empty_state_projects_empty_search_description": "No projects detected with the matching criteria. Create a new project instead.",
"empty_state_members_empty_search_title": "No matching members",
"empty_state_members_empty_search_description": "Add them to the project if they are already a part of the workspace",
"empty_state_project_module_issues_title": "No issues in the module",
"empty_state_project_module_issues_description": "Create or add issues which you want to accomplish as part of this module",
"empty_state_project_module_issues_primary_button": "Create new issue",
"empty_state_project_module_issues_secondary_button": "Add an existing issue",
"empty_state_project_module_title": "Map your project milestones to Modules and track aggregated work easily.",
"empty_state_project_module_description": "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone.",
"empty_state_project_module_primary_button": "Build your first module",
"empty_state_project_module_comic_title": "Modules help group work by hierarchy.",
"empty_state_project_module_comic_description": "A cart module, a chassis module, and a warehouse module are all good example of this grouping.",
"empty_state_project_archived_no_modules_title": "No archived Modules yet",
"empty_state_project_archived_no_modules_description": "To tidy up your project, archive completed or cancelled modules. Find them here once archived.",
"empty_state_project_empty_filter_title": "No issues found matching the filters applied",
"empty_state_project_empty_filter_secondary_button": "Clear all filters",
"empty_state_project_archived_empty_filter_title": "No issues found matching the filters applied",
"empty_state_project_archived_empty_filter_secondary_button": "Clear all filters",
"empty_state_project_draft_empty_filter_title": "No issues found matching the filters applied",
"empty_state_project_draft_empty_filter_secondary_button": "Clear all filters",
"empty_state_project_no_issues_title": "Create an issue and assign it to someone, even yourself",
"empty_state_project_no_issues_description": "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.",
"empty_state_project_no_issues_primary_button": "Create your first issue",
"empty_state_project_no_issues_comic_title": "Issues are building blocks in Plane.",
"empty_state_project_no_issues_comic_description": "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.",
"empty_state_project_archived_no_issues_title": "No archived issues yet",
"empty_state_project_archived_no_issues_description": "Manually or through automation, you can archive issues that are completed or cancelled. Find them here once archived.",
"empty_state_project_archived_no_issues_primary_button": "Set automation",
"empty_state_project_draft_no_issues_title": "No draft issues yet",
"empty_state_project_draft_no_issues_description": "Quickly stepping away but want to keep your place? No worries save a draft now. Your issues will be right here waiting for you.",
"empty_state_project_view_title": "Save filtered views for your project. Create as many as you need",
"empty_state_project_view_description": "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone's views and choose whichever suits their needs best.",
"empty_state_project_view_primary_button": "Create your first view",
"empty_state_project_view_comic_title": "Views work atop Issue properties.",
"empty_state_project_view_comic_description": "You can create a view from here with as many properties as filters as you see fit.",
"empty_state_project_page_title": "Write a note, a doc, or a full knowledge base. Get Galileo, Plane's AI assistant, to help you get started",
"empty_state_project_page_description": "Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project's context. To make short work of any doc, invoke Galileo, Plane's AI, with a shortcut or the click of a button.",
"empty_state_project_page_primary_button": "Create your first page",
"empty_state_project_page_private_title": "No private pages yet",
"empty_state_project_page_private_description": "Keep your private thoughts here. When you're ready to share, the team's just a click away.",
"empty_state_project_page_private_primary_button": "Create your first page",
"empty_state_project_page_public_title": "No public pages yet",
"empty_state_project_page_public_description": "See pages shared with everyone in your project right here.",
"empty_state_project_page_public_primary_button": "Create your first page",
"empty_state_project_page_archived_title": "No archived pages yet",
"empty_state_project_page_archived_description": "Archive pages not on your radar. Access them here when needed.",
"empty_state_workspace_page_title": "Write a note, a doc, or a full knowledge base. Get Galileo, Plane's AI assistant, to help you get started",
"empty_state_workspace_page_description": "Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project's context. To make short work of any doc, invoke Galileo, Plane's AI, with a shortcut or the click of a button.",
"empty_state_workspace_page_primary_button": "Create your first page",
"empty_state_workspace_page_private_title": "No private pages yet",
"empty_state_workspace_page_private_description": "Keep your private thoughts here. When you're ready to share, the team's just a click away.",
"empty_state_workspace_page_private_primary_button": "Create your first page",
"empty_state_workspace_page_public_title": "No public pages yet",
"empty_state_workspace_page_public_description": "See pages shared with everyone in your workspace right here.",
"empty_state_workspace_page_public_primary_button": "Create your first page",
"empty_state_workspace_page_archived_title": "No archived pages yet",
"empty_state_workspace_page_archived_description": "Archive pages not on your radar. Access them here when needed.",
"empty_state_workspace_draft_issues_title": "Half-written issues, and soon, comments will show up here.",
"empty_state_workspace_draft_issues_description": "To try this out, start adding an issue and leave it mid-way or create your first draft below. 😉",
"empty_state_workspace_draft_issues_primary_button": "Create your first draft"
"loading_members": "Loading members..."
}
+1 -261
View File
@@ -316,265 +316,5 @@
"remove_parent_issue": "Eliminar problema padre",
"add_parent": "Agregar padre",
"loading_members": "Cargando miembros...",
"inbox": "bandeja de entrada",
"empty_state_workspace_dashboard_title": "Resumen de sus proyectos, actividad y métricas",
"empty_state_workspace_dashboard_description": "¡Bienvenido a Plane! Nos alegra tenerte aquí. Crea tu primer proyecto y realiza un seguimiento de tus tareas, y esta página se transformará en un espacio que te ayuda a progresar. Los administradores también verán elementos que ayudan a su equipo a progresar.",
"empty_state_workspace_dashboard_primary_button": "Crea tu primer proyecto",
"empty_state_workspace_dashboard_comic_title": "Todo comienza con un proyecto en Plane",
"empty_state_workspace_dashboard_comic_description": "Un proyecto podría ser el plan de un producto, una campaña de marketing o el lanzamiento de un nuevo coche.",
"empty_state_workspace_analytics_title": "Sigue el progreso, las cargas de trabajo y las asignaciones. Identifica tendencias, elimina bloqueos y avanza más rápido",
"empty_state_workspace_analytics_description": "Observa el alcance frente a la demanda, las 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.",
"empty_state_workspace_analytics_primary_button": "Comienza tu primer proyecto",
"empty_state_workspace_analytics_comic_title": "Analytics funciona mejor con Ciclos + Módulos",
"empty_state_workspace_analytics_comic_description": "Primero, organiza tus tareas en Ciclos y, si puedes, agrupa las tareas que abarcan más de un ciclo en Módulos. Consulta ambos en la navegación izquierda.",
"empty_state_workspace_projects_title": "No hay proyectos activos",
"empty_state_workspace_projects_description": "Piensa en cada proyecto como el padre del trabajo orientado a objetivos. Los proyectos son donde viven los Trabajos, Ciclos y Módulos, y junto con tus colegas, te ayudan a alcanzar ese objetivo. Crea un nuevo proyecto o filtra los proyectos archivados.",
"empty_state_workspace_projects_primary_button": "Comienza tu primer proyecto",
"empty_state_workspace_projects_comic_title": "Todo comienza con un proyecto en Plane",
"empty_state_workspace_projects_comic_description": "Un proyecto podría ser el plan de un producto, una campaña de marketing o el lanzamiento de un nuevo coche.",
"empty_state_workspace_teams_title": "Equipos",
"empty_state_workspace_teams_description": "Los equipos son grupos de personas que colaboran en proyectos. Crea un equipo para empezar.",
"empty_state_workspace_teams_primary_button": "Crear nuevo equipo",
"empty_state_workspace_initiatives_title": "Organiza el trabajo al más alto nivel con Iniciativas",
"empty_state_workspace_initiatives_description": "Cuando necesites organizar trabajo que abarca varios proyectos y equipos, las Iniciativas son útiles. Conecta proyectos y épicas a iniciativas, observa actualizaciones automáticamente acumuladas y ve el bosque antes que los árboles.",
"empty_state_workspace_initiatives_primary_button": "Crear una iniciativa",
"empty_state_workspace_initiatives_empty_search_title": "No hay iniciativas coincidentes",
"empty_state_workspace_initiatives_empty_search_description": "No se detectaron iniciativas con los criterios coincidentes. \n Crea una nueva iniciativa en su lugar.",
"empty_state_workspace_all_issues_title": "No hay tareas en el proyecto",
"empty_state_workspace_all_issues_description": "¡Primer proyecto completado! Ahora, divide tu trabajo en piezas rastreables con tareas. ¡Vamos!",
"empty_state_workspace_all_issues_primary_button": "Crear nueva tarea",
"empty_state_workspace_assigned_title": "No hay tareas aún",
"empty_state_workspace_assigned_description": "Las tareas asignadas a ti se pueden rastrear desde aquí.",
"empty_state_workspace_assigned_primary_button": "Crear nueva tarea",
"empty_state_workspace_created_title": "No hay tareas aún",
"empty_state_workspace_created_description": "Todas las tareas creadas por ti vienen aquí, rastréalas directamente aquí.",
"empty_state_workspace_created_primary_button": "Crear nueva tarea",
"empty_state_workspace_subscribed_title": "No hay tareas aún",
"empty_state_workspace_subscribed_description": "Suscríbete a las tareas que te interesan, rastréalas todas aquí.",
"empty_state_workspace_custom_view_title": "No hay tareas aún",
"empty_state_workspace_custom_view_description": "Las tareas que aplican a los filtros, rastréalas todas aquí.",
"empty_state_project_no_epics_title": "Crea una épica y asígnala a alguien, incluso a ti mismo",
"empty_state_project_no_epics_description": "Para cuerpos de trabajo más grandes que abarcan varios ciclos y pueden vivir a través de módulos, crea una épica. Vincula tareas y subtareas en un proyecto a una épica y salta a una tarea desde la vista general.",
"empty_state_project_no_epics_primary_button": "Crear una Épica",
"empty_state_team_no_issues_title": "Crea una tarea en tus proyectos de equipo y asígnala a alguien, incluso a ti mismo",
"empty_state_team_no_issues_description": "Piensa en las tareas como trabajos, tareas, trabajo o JTBD. Una tarea y sus subtareas son generalmente acciones basadas en tiempo asignadas a miembros de tu equipo. Tu equipo crea, asigna y completa tareas para mover tu proyecto hacia su objetivo.",
"empty_state_team_no_issues_primary_button": "Crear tu primera tarea",
"empty_state_team_no_issues_comic_title": "Las tareas son los bloques de construcción en Plane.",
"empty_state_team_no_issues_comic_description": "Rediseñar la UI de Plane, Hacer un rebranding de la empresa, o Lanzar el nuevo sistema de inyección de combustible son ejemplos de tareas que probablemente tienen subtareas.",
"empty_state_team_empty_filter_title": "No se encontraron tareas que coincidan con los filtros aplicados",
"empty_state_team_empty_filter_secondary_button": "Limpiar todos los filtros",
"empty_state_team_view_title": "Guarda vistas filtradas para tu equipo. Crea tantas como necesites",
"empty_state_team_view_description": "Las vistas son un conjunto de filtros que usas frecuentemente o a los que quieres acceder fácilmente. Todos tus colegas en un equipo pueden ver las vistas de todos y elegir la que mejor se adapte a sus necesidades.",
"empty_state_team_view_primary_button": "Crear tu primera vista",
"empty_state_team_view_comic_title": "Las vistas funcionan sobre las propiedades de las Tareas.",
"empty_state_team_view_comic_description": "Puedes crear una vista desde aquí con tantas propiedades como filtros consideres adecuados.",
"empty_state_workspace_settings_api_tokens_title": "No se han creado tokens API",
"empty_state_workspace_settings_api_tokens_description": "Las APIs de Plane se pueden usar para integrar tus datos en Plane con cualquier sistema externo. Crea un token para comenzar.",
"empty_state_workspace_settings_webhooks_title": "No se han añadido webhooks",
"empty_state_workspace_settings_webhooks_description": "Crea webhooks para recibir actualizaciones en tiempo real y automatizar acciones.",
"empty_state_workspace_settings_export_title": "No hay exportaciones previas aún",
"empty_state_workspace_settings_export_description": "Cada vez que exportes, también tendrás una copia aquí para referencia.",
"empty_state_workspace_settings_import_title": "No hay importaciones previas aún",
"empty_state_workspace_settings_import_description": "Encuentra todas tus importaciones previas aquí y descárgalas.",
"empty_state_profile_activity_title": "No hay actividades aún",
"empty_state_profile_activity_description": "¡Comienza creando una nueva tarea! Añade detalles y propiedades. Explora más en Plane para ver tu actividad.",
"empty_state_project_settings_labels_title": "No hay etiquetas aún",
"empty_state_project_settings_labels_description": "Crea etiquetas para ayudar a organizar y filtrar tareas en tu proyecto.",
"empty_state_project_settings_integrations_title": "No hay integraciones configuradas",
"empty_state_project_settings_integrations_description": "Configura GitHub y otras integraciones para sincronizar las tareas de tu proyecto.",
"empty_state_project_settings_estimate_title": "No se han añadido estimaciones",
"empty_state_project_settings_estimate_description": "Crea un conjunto de estimaciones para comunicar la cantidad de trabajo por tarea.",
"empty_state_project_cycles_title": "Agrupa y delimita tu trabajo en Ciclos.",
"empty_state_project_cycles_description": "Divide el trabajo en fragmentos delimitados por tiempo, trabaja hacia atrás desde la fecha límite de tu proyecto para establecer fechas y logra un progreso tangible como equipo.",
"empty_state_project_cycles_primary_button": "Establece tu primer ciclo",
"empty_state_project_cycles_comic_title": "Los ciclos son cajas de tiempo repetitivas.",
"empty_state_project_cycles_comic_description": "Un sprint, una iteración, o cualquier otro término que uses para el seguimiento semanal o quincenal del trabajo es un ciclo.",
"empty_state_project_module_title": "Mapea los hitos de tu proyecto a Módulos y rastrea el trabajo agregado fácilmente.",
"empty_state_project_module_description": "Un grupo de tareas que pertenecen a un padre lógico y jerárquico forman un módulo. Piensa en ellos como una forma de rastrear el trabajo por hitos del proyecto. Tienen sus propios períodos y fechas límite, así como analíticas para ayudarte a ver qué tan cerca o lejos estás de un hito.",
"empty_state_project_module_primary_button": "Construye tu primer módulo",
"empty_state_project_module_comic_title": "Los módulos ayudan a agrupar el trabajo por jerarquía.",
"empty_state_project_module_comic_description": "Un módulo de carrito, un módulo de chasis y un módulo de almacén son buenos ejemplos de esta agrupación.",
"empty_state_notification_all_empty_state_title": "No hay tareas asignadas",
"empty_state_notification_all_empty_state_description": "Las actualizaciones para tareas asignadas a ti se pueden ver aquí",
"empty_state_notification_unread_empty_state_title": "No hay notificaciones sin leer",
"empty_state_notification_unread_empty_state_description": "¡Felicitaciones! Estás al día con todo lo que sucede en las tareas que te importan",
"empty_state_active_cycle_progress_empty_state_title": "Agrega tareas al ciclo para ver su progreso",
"empty_state_workspace_active_cycles_title": "No hay ciclos activos",
"empty_state_workspace_active_cycles_description": "Ciclos de tus proyectos que incluyen cualquier período que abarque la fecha de hoy dentro de su rango. Encuentra el progreso y detalles de todos tus ciclos activos aquí.",
"empty_state_disabled_project_inbox_title": "La bandeja de entrada no está habilitada para el proyecto",
"empty_state_disabled_project_inbox_description": "La bandeja de entrada te ayuda a gestionar las solicitudes entrantes a tu proyecto y agregarlas como tareas en tu flujo de trabajo. Habilita la bandeja de entrada desde la configuración del proyecto para gestionar solicitudes.",
"empty_state_disabled_project_inbox_primary_button": "Gestionar funciones",
"empty_state_inbox_sidebar_open_tab_title": "No hay tareas abiertas",
"empty_state_inbox_sidebar_open_tab_description": "Encuentra tareas abiertas aquí. Crea una nueva tarea.",
"empty_state_inbox_sidebar_closed_tab_title": "No hay tareas cerradas",
"empty_state_inbox_sidebar_closed_tab_description": "Todas las tareas, ya sean aceptadas o rechazadas, se pueden encontrar aquí.",
"empty_state_inbox_sidebar_filter_empty_state_title": "No hay tareas coincidentes",
"empty_state_inbox_sidebar_filter_empty_state_description": "Ninguna tarea coincide con el filtro aplicado en la bandeja de entrada. Crea una nueva tarea.",
"empty_state_inbox_detail_empty_state_title": "Selecciona una tarea para ver sus detalles.",
"empty_state_project_page_title": "Escribe una nota, un documento o una base de conocimientos completa. Obtén ayuda de Galileo, el asistente de IA de Plane.",
"empty_state_project_page_description": "Las páginas son el espacio de pensamiento en Plane. Toma notas de reuniones, dales formato fácilmente, incrusta tareas, organízalas usando una biblioteca de componentes y mantenlas todas en el contexto de tu proyecto. Para hacer un trabajo rápido de cualquier documento, invoca a Galileo, la IA de Plane, con un atajo o el clic de un botón.",
"empty_state_disabled_project_view_title": "Las vistas no están habilitadas para este proyecto.",
"empty_state_disabled_project_view_description": "Las vistas son un conjunto de filtros que usas frecuentemente o a los que quieres acceder fácilmente. Todos tus colegas en un proyecto pueden ver las vistas de todos y elegir la que mejor se adapte a sus necesidades. Habilita las vistas en la configuración del proyecto para comenzar a usarlas.",
"empty_state_disabled_project_view_primary_button": "Gestionar funciones",
"empty_state_views_empty_search_title": "No se encontraron vistas coincidentes",
"empty_state_views_empty_search_description": "No hay vistas que coincidan con los criterios de búsqueda. Crea una nueva vista en su lugar.",
"empty_state_projects_empty_search_title": "No se encontraron proyectos coincidentes",
"empty_state_projects_empty_search_description": "No se detectaron proyectos con los criterios coincidentes. Crea un nuevo proyecto en su lugar.",
"empty_state_members_empty_search_title": "No se encontraron miembros coincidentes",
"empty_state_members_empty_search_description": "Agrégalos al proyecto si ya son parte del espacio de trabajo",
"empty_state_project_module_issues_title": "No hay tareas en el módulo",
"empty_state_project_module_issues_description": "Crea o agrega tareas que desees lograr como parte de este módulo",
"empty_state_project_module_issues_primary_button": "Crear nueva tarea",
"empty_state_project_module_issues_secondary_button": "Agregar una tarea existente",
"empty_state_project_archived_no_modules_title": "No hay módulos archivados aún",
"empty_state_project_archived_no_modules_description": "Para ordenar tu proyecto, archiva los módulos completados o cancelados. Encuéntralos aquí una vez archivados.",
"empty_state_project_empty_filter_title": "No se encontraron tareas que coincidan con los filtros aplicados",
"empty_state_project_empty_filter_secondary_button": "Limpiar todos los filtros",
"empty_state_workspace_draft_issues_title": "Las tareas a medio escribir y, pronto, los comentarios aparecerán aquí.",
"empty_state_workspace_draft_issues_description": "Para probar esto, comienza a agregar una tarea y déjala a medias o crea tu primer borrador a continuación. 😉",
"empty_state_workspace_draft_issues_primary_button": "Crear tu primer borrador",
"empty_state_workspace_project_not_found_title": "No existe tal proyecto",
"empty_state_workspace_project_not_found_description": "Para crear tareas o gestionar tu trabajo, necesitas crear un proyecto o ser parte de uno.",
"empty_state_workspace_project_not_found_primary_button": "Crear Proyecto",
"empty_state_workspace_project_not_found_comic_title": "Todo comienza con un proyecto en Plane",
"empty_state_workspace_project_not_found_comic_description": "Un proyecto podría ser la hoja de ruta de un producto, una campaña de marketing o el lanzamiento de un nuevo coche.",
"empty_state_workspace_no_projects_title": "Sin proyecto",
"empty_state_workspace_no_projects_description": "Para crear tareas o gestionar tu trabajo, necesitas crear un proyecto o ser parte de uno.",
"empty_state_workspace_no_projects_primary_button": "Inicia tu primer proyecto",
"empty_state_workspace_no_projects_comic_title": "Todo comienza con un proyecto en Plane",
"empty_state_workspace_no_projects_comic_description": "Un proyecto podría ser la hoja de ruta de un producto, una campaña de marketing o el lanzamiento de un nuevo coche.",
"empty_state_project_cycle_no_issues_title": "No se han añadido tareas al ciclo",
"empty_state_project_cycle_no_issues_description": "Añade o crea tareas que desees programar y entregar dentro de este ciclo",
"empty_state_project_cycle_no_issues_primary_button": "Crear nueva tarea",
"empty_state_project_cycle_no_issues_secondary_button": "Añadir una tarea existente",
"empty_state_project_cycle_active_title": "No hay ciclo activo",
"empty_state_project_cycle_active_description": "Un ciclo activo incluye cualquier período que abarque la fecha de hoy dentro de su rango. Encuentra el progreso y los detalles del ciclo activo aquí.",
"empty_state_project_cycle_completed_no_issues_title": "No hay tareas en el ciclo",
"empty_state_project_cycle_completed_no_issues_description": "No hay tareas en el ciclo. Las tareas están transferidas u ocultas. Para ver las tareas ocultas si las hay, actualiza tus propiedades de visualización en consecuencia.",
"empty_state_command_k_search_empty_state_title": "No se encontraron resultados",
"empty_state_issue_relation_search_empty_state_title": "No se encontraron tareas coincidentes",
"empty_state_issue_relation_empty_state_title": "No se encontraron tareas",
"empty_state_issue_comment_empty_state_title": "Aún no hay comentarios",
"empty_state_issue_comment_empty_state_description": "Los comentarios se pueden usar como un espacio de discusión y seguimiento para las tareas",
"empty_state_notification_detail_empty_state_title": "Selecciona para ver detalles.",
"empty_state_notification_mentions_empty_state_title": "No hay tareas asignadas",
"empty_state_notification_mentions_empty_state_description": "Aquí se pueden ver las actualizaciones de las tareas que te han sido asignadas",
"empty_state_notification_my_issue_empty_state_title": "No hay tareas asignadas",
"empty_state_notification_my_issue_empty_state_description": "Aquí se pueden ver las actualizaciones de las tareas que te han sido asignadas",
"empty_state_notification_created_empty_state_title": "No hay actualizaciones de tareas",
"empty_state_notification_created_empty_state_description": "Aquí se pueden ver las actualizaciones de las tareas creadas por ti",
"empty_state_notification_subscribed_empty_state_title": "No hay actualizaciones de tareas",
"empty_state_notification_subscribed_empty_state_description": "Aquí se pueden ver las actualizaciones de cualquier tarea a la que estés suscrito",
"empty_state_notification_snoozed_empty_state_title": "Aún no hay notificaciones pospuestas",
"empty_state_notification_snoozed_empty_state_description": "Cualquier notificación que pospongas para más tarde estará disponible aquí para que actúes",
"empty_state_notification_archived_empty_state_title": "Aún no hay notificaciones archivadas",
"empty_state_notification_archived_empty_state_description": "Cualquier notificación que archives estará disponible aquí para ayudarte a concentrarte",
"empty_state_active_cycle_chart_empty_state_title": "Añade tareas al ciclo para ver el gráfico de avance.",
"empty_state_active_cycle_priority_issue_empty_state_title": "Observa las tareas de alta prioridad abordadas en el ciclo de un vistazo.",
"empty_state_active_cycle_assignee_empty_state_title": "Añade asignados a las tareas para ver un desglose del trabajo por asignados.",
"empty_state_active_cycle_label_empty_state_title": "Añade etiquetas a las tareas para ver el desglose del trabajo por etiquetas.",
"empty_state_disabled_project_cycle_title": "Los ciclos no están habilitados para este proyecto.",
"empty_state_disabled_project_cycle_description": "Divide el trabajo en fragmentos temporales, trabaja hacia atrás desde la fecha límite de tu proyecto para establecer fechas y logra un progreso tangible como equipo. Habilita la función de ciclos para tu proyecto para comenzar a usarlos.",
"empty_state_disabled_project_cycle_primary_button": "Gestionar funciones",
"empty_state_disabled_project_module_title": "Los módulos no están habilitados para el proyecto.",
"empty_state_disabled_project_module_description": "Un grupo de tareas que pertenecen a un padre lógico y jerárquico forman un módulo. Piensa en ellos como una forma de rastrear el trabajo por hitos del proyecto. Habilita los módulos desde la configuración del proyecto.",
"empty_state_disabled_project_module_primary_button": "Gestionar funciones",
"empty_state_disabled_project_page_title": "Las páginas no están habilitadas para el proyecto.",
"empty_state_disabled_project_page_description": "Las páginas son un espacio de pensamiento en Plane. Toma notas de reuniones, dales formato fácilmente, incrusta tareas, organízalas usando una biblioteca de componentes y mantenlas todas en el contexto de tu proyecto. Habilita la función de páginas para comenzar a crearlas en tu proyecto.",
"empty_state_disabled_project_page_primary_button": "Gestionar funciones",
"empty_state_project_archived_empty_filter_title": "No se encontraron tareas que coincidan con los filtros aplicados",
"empty_state_project_archived_empty_filter_secondary_button": "Limpiar todos los filtros",
"empty_state_project_draft_empty_filter_title": "No se encontraron tareas que coincidan con los filtros aplicados",
"empty_state_project_draft_empty_filter_secondary_button": "Limpiar todos los filtros",
"empty_state_project_no_issues_title": "Crea una tarea y asígnala a alguien, incluso a ti mismo",
"empty_state_project_no_issues_description": "Piensa en las tareas como trabajos, tareas, trabajo o JTBD. Lo cual nos gusta. Una tarea y sus subtareas son generalmente acciones basadas en tiempo asignadas a miembros de tu equipo. Tu equipo crea, asigna y completa tareas para mover tu proyecto hacia su objetivo.",
"empty_state_project_no_issues_primary_button": "Crea tu primera tarea",
"empty_state_project_no_issues_comic_title": "Las tareas son los bloques de construcción en Plane.",
"empty_state_project_no_issues_comic_description": "Rediseñar la interfaz de usuario de Plane, Cambiar la marca de la empresa o Lanzar el nuevo sistema de inyección de combustible son ejemplos de tareas que probablemente tienen subtareas.",
"empty_state_project_archived_no_issues_title": "Aún no hay tareas archivadas",
"empty_state_project_archived_no_issues_description": "Manualmente o mediante automatización, puedes archivar tareas que estén completadas o canceladas. Encuéntralas aquí una vez archivadas.",
"empty_state_project_archived_no_issues_primary_button": "Configurar automatización",
"empty_state_project_draft_no_issues_title": "Aún no hay borradores de tareas",
"empty_state_project_draft_no_issues_description": "¿Te vas rápidamente pero quieres mantener tu lugar? No te preocupes guarda un borrador ahora. Tus tareas estarán aquí esperándote.",
"empty_state_project_page_archived_title": "Aún no hay páginas archivadas",
"empty_state_project_page_archived_description": "Archiva las páginas que no estén en tu radar. Accede a ellas aquí cuando las necesites.",
"empty_state_workspace_page_private_title": "Aún no hay páginas privadas",
"empty_state_workspace_page_private_description": "Mantén tus pensamientos privados aquí. Cuando estés listo para compartir, el equipo está a solo un clic de distancia.",
"empty_state_workspace_page_private_primary_button": "Crea tu primera página",
"empty_state_workspace_page_public_title": "Aún no hay páginas públicas",
"empty_state_workspace_page_public_description": "Ve las páginas compartidas con todos en tu espacio de trabajo aquí mismo.",
"empty_state_workspace_page_public_primary_button": "Crea tu primera página",
"empty_state_workspace_page_archived_title": "Aún no hay páginas archivadas",
"empty_state_workspace_page_archived_description": "Archiva las páginas que no estén en tu radar. Accede a ellas aquí cuando las necesites."
"inbox": "bandeja de entrada"
}
+19 -277
View File
@@ -19,12 +19,12 @@
"remove_members": "Supprimer des membres",
"add": "Ajouter",
"remove": "Supprimer",
"add_new": "Ajouter nouveau",
"add_new": "Ajouter un nouveau",
"remove_selected": "Supprimer la sélection",
"first_name": "Prénom",
"last_name": "Nom de famille",
"email": "Email",
"display_name": "Nom d'affichage",
"display_name": "Pseudonyme",
"role": "Rôle",
"timezone": "Fuseau horaire",
"avatar": "Avatar",
@@ -32,7 +32,7 @@
"password": "Mot de passe",
"change_cover": "Modifier la couverture",
"language": "Langue",
"saving": "Enregistrement...",
"saving": "Enregistrement en cours...",
"save_changes": "Enregistrer les modifications",
"deactivate_account": "Désactiver le compte",
"deactivate_account_description": "Lors de la désactivation d'un compte, toutes les données et ressources de ce compte seront définitivement supprimées et ne pourront pas être récupérées.",
@@ -43,14 +43,14 @@
"activity": "Activité",
"appearance": "Apparence",
"notifications": "Notifications",
"workspaces": "Workspaces",
"create_workspace": "Créer un workspace",
"workspaces": "Espaces de travail",
"create_workspace": "Créer un espace de travail",
"invitations": "Invitations",
"summary": "Résumé",
"assigned": "Assigné",
"created": "Créé",
"subscribed": "Souscrit",
"you_do_not_have_the_permission_to_access_this_page": "Vous n'avez pas les permissions pour accéder à cette page.",
"you_do_not_have_the_permission_to_access_this_page": "Vous n'avez pas les permissions d'accéder à cette page.",
"failed_to_sign_out_please_try_again": "Impossible de se déconnecter. Veuillez réessayer.",
"password_changed_successfully": "Mot de passe changé avec succès.",
"something_went_wrong_please_try_again": "Quelque chose s'est mal passé. Veuillez réessayer.",
@@ -62,7 +62,7 @@
"this_field_is_required": "Ce champ est requis",
"passwords_dont_match": "Les mots de passe ne correspondent pas",
"please_enter_your_password": "Veuillez entrer votre mot de passe.",
"password_length_should_me_more_than_8_characters": "La longueur du mot de passe doit être supérieure à 8 caractères.",
"password_length_should_me_more_than_8_characters": "La longueur du mot de passe doit faire au moins 8 caractères.",
"password_is_weak": "Le mot de passe est faible.",
"password_is_strong": "Le mot de passe est fort.",
"load_more": "Charger plus",
@@ -106,8 +106,8 @@
"comments_description": "Me notifier lorsqu'un utilisateur commente un problème",
"mentions": "Mention",
"mentions_description": "Me notifier uniquement lorsqu'un utilisateur mentionne un problème",
"create_your_workspace": "Créer votre workspace",
"only_your_instance_admin_can_create_workspaces": "Seuls les administrateurs de votre instance peuvent créer des workspaces",
"create_your_workspace": "Créer votre espace de travail",
"only_your_instance_admin_can_create_workspaces": "Seuls les administrateurs de votre instance peuvent créer des espaces de travail",
"only_your_instance_admin_can_create_workspaces_description": "Si vous connaissez l'adresse email de votre administrateur d'instance, cliquez sur le bouton ci-dessous pour les contacter.",
"go_back": "Retour",
"request_instance_admin": "Demander à l'administrateur de l'instance",
@@ -135,7 +135,7 @@
"old_password": "Mot de passe actuel",
"general_settings": "Paramètres généraux",
"sign_out": "Déconnexion",
"signing_out": "Déconnexion",
"signing_out": "Déconnexion en cours",
"active_cycles": "Cycles actifs",
"active_cycles_description": "Surveillez les cycles dans les projets, suivez les issues de haute priorité et zoomez sur les cycles qui nécessitent attention.",
"on_demand_snapshots_of_all_your_cycles": "Captures instantanées sur demande de tous vos cycles",
@@ -190,17 +190,17 @@
"project_created_successfully_description": "Projet créé avec succès. Vous pouvez maintenant ajouter des issues à ce projet.",
"project_cover_image_alt": "Image de couverture du projet",
"name_is_required": "Le nom est requis",
"title_should_be_less_than_255_characters": "Le titre doit être inférieur à 255 caractères",
"title_should_be_less_than_255_characters": "Le titre ne doit pas dépasser 255 caractères",
"project_name": "Nom du projet",
"project_id_must_be_at_least_1_character": "Le projet ID doit être au moins de 1 caractère",
"project_id_must_be_at_most_5_characters": "Le projet ID doit être au plus de 5 caractères",
"project_id_must_be_at_least_1_character": "L'ID du projet doit être au moins de 1 caractère",
"project_id_must_be_at_most_5_characters": "L'ID du projet doit être au plus de 5 caractères",
"project_id": "ID du projet",
"project_id_tooltip_content": "Aide à identifier les issues du projet de manière unique. Max 5 caractères.",
"description_placeholder": "Description...",
"only_alphanumeric_non_latin_characters_allowed": "Seuls les caractères alphanumériques et non latins sont autorisés.",
"project_id_is_required": "Le projet ID est requis",
"project_id_is_required": "L'ID du projet est requis",
"select_network": "Sélectionner le réseau",
"lead": "Lead",
"lead": "Responsable",
"private": "Privé",
"public": "Public",
"accessible_only_by_invite": "Accessible uniquement par invitation",
@@ -245,11 +245,11 @@
"contact_sales": "Contacter les ventes",
"hyper_mode": "Mode hyper",
"keyboard_shortcuts": "Raccourcis clavier",
"whats_new": "Nouveautés?",
"whats_new": "Nouveautés ?",
"version": "Version",
"we_are_having_trouble_fetching_the_updates": "Nous avons des difficultés à récupérer les mises à jour.",
"our_changelogs": "nos changelogs",
"for_the_latest_updates": "pour les dernières mises à jour.",
"our_changelogs": "Notre journal de modifications",
"for_the_latest_updates": "Pour les dernières mises à jour.",
"please_visit": "Veuillez visiter",
"docs": "Documentation",
"full_changelog": "Journal complet",
@@ -316,263 +316,5 @@
"remove_parent_issue": "Supprimer le problème parent",
"add_parent": "Ajouter un parent",
"loading_members": "Chargement des membres...",
"inbox": "boîte de réception",
"empty_state_workspace_dashboard_title": "Aperçu de vos projets, activités et métriques",
"empty_state_workspace_dashboard_description": "Bienvenue sur Plane ! Nous sommes ravis de vous accueillir. Créez votre premier projet et suivez vos tâches, et cette page se transformera en un espace qui vous aide à progresser. Les administrateurs verront également des éléments qui aident leur équipe à progresser.",
"empty_state_workspace_dashboard_primary_button": "Créer votre premier projet",
"empty_state_workspace_dashboard_comic_title": "Tout commence avec un projet dans Plane",
"empty_state_workspace_dashboard_comic_description": "Un projet pourrait être la feuille de route d'un produit, une campagne marketing ou le lancement d'une nouvelle voiture.",
"empty_state_workspace_analytics_title": "Suivez les progrès, charges de travail et allocations. Identifiez les tendances, supprimez les blocages et avancez plus vite",
"empty_state_workspace_analytics_description": "Visualisez la portée par rapport à la demande, les estimations et l'augmentation de la portée. Obtenez les performances par membre d'équipe et équipes, et assurez-vous que votre projet se déroule dans les temps.",
"empty_state_workspace_analytics_primary_button": "Démarrer votre premier projet",
"empty_state_workspace_analytics_comic_title": "Analytics fonctionne mieux avec Cycles + Modules",
"empty_state_workspace_analytics_comic_description": "D'abord, organisez vos tâches en Cycles et, si possible, regroupez les tâches qui s'étendent sur plus d'un cycle en Modules. Consultez les deux dans la navigation de gauche.",
"empty_state_workspace_projects_title": "Aucun projet actif",
"empty_state_workspace_projects_description": "Considérez chaque projet comme le parent du travail orienté objectif. Les projets sont là où vivent les Tâches, Cycles et Modules, et avec vos collègues, ils vous aident à atteindre cet objectif. Créez un nouveau projet ou filtrez les projets archivés.",
"empty_state_workspace_projects_primary_button": "Démarrer votre premier projet",
"empty_state_workspace_projects_comic_title": "Tout commence avec un projet dans Plane",
"empty_state_workspace_projects_comic_description": "Un projet pourrait être la feuille de route d'un produit, une campagne marketing ou le lancement d'une nouvelle voiture.",
"empty_state_workspace_teams_title": "Équipes",
"empty_state_workspace_teams_description": "Les équipes sont des groupes de personnes qui collaborent sur des projets. Créez une équipe pour commencer.",
"empty_state_workspace_teams_primary_button": "Créer une nouvelle équipe",
"empty_state_workspace_initiatives_title": "Organisez le travail au plus haut niveau avec les Initiatives",
"empty_state_workspace_initiatives_description": "Lorsque vous devez organiser un travail qui s'étend sur plusieurs projets et équipes, les Initiatives sont utiles. Connectez les projets et les épopées aux initiatives, voyez les mises à jour automatiquement agrégées et voyez la forêt avant les arbres.",
"empty_state_workspace_initiatives_primary_button": "Créer une initiative",
"empty_state_workspace_initiatives_empty_search_title": "Aucune initiative correspondante",
"empty_state_workspace_initiatives_empty_search_description": "Aucune initiative détectée avec les critères correspondants. \n Créez une nouvelle initiative à la place.",
"empty_state_workspace_all_issues_title": "Aucune tâche dans le projet",
"empty_state_workspace_all_issues_description": "Premier projet terminé ! Maintenant, découpez votre travail en morceaux traçables avec des tâches. Allons-y !",
"empty_state_workspace_all_issues_primary_button": "Créer une nouvelle tâche",
"empty_state_workspace_assigned_title": "Aucune tâche pour le moment",
"empty_state_workspace_assigned_description": "Les tâches qui vous sont assignées peuvent être suivies ici.",
"empty_state_workspace_assigned_primary_button": "Créer une nouvelle tâche",
"empty_state_workspace_created_title": "Aucune tâche pour le moment",
"empty_state_workspace_created_description": "Toutes les tâches que vous créez arrivent ici, suivez-les directement ici.",
"empty_state_workspace_created_primary_button": "Créer une nouvelle tâche",
"empty_state_workspace_subscribed_title": "Aucune tâche pour le moment",
"empty_state_workspace_subscribed_description": "Abonnez-vous aux tâches qui vous intéressent, suivez-les toutes ici.",
"empty_state_workspace_custom_view_title": "Aucune tâche pour le moment",
"empty_state_workspace_custom_view_description": "Les tâches qui correspondent aux filtres, suivez-les toutes ici.",
"empty_state_project_no_epics_title": "Créez une épopée et assignez-la à quelqu'un, même à vous-même",
"empty_state_project_no_epics_description": "Pour les grands ensembles de travail qui s'étendent sur plusieurs cycles et peuvent vivre à travers les modules, créez une épopée. Liez les tâches et sous-tâches d'un projet à une épopée et accédez à une tâche depuis la vue d'ensemble.",
"empty_state_project_no_epics_primary_button": "Créer une Épopée",
"empty_state_team_no_issues_title": "Créez une tâche dans vos projets d'équipe et assignez-la à quelqu'un, même à vous-même",
"empty_state_team_no_issues_description": "Pensez aux tâches comme des emplois, des tâches, du travail ou JTBD. Une tâche et ses sous-tâches sont généralement des actions temporelles assignées aux membres de votre équipe. Votre équipe crée, assigne et complète des tâches pour faire avancer votre projet vers son objectif.",
"empty_state_team_no_issues_primary_button": "Créer votre première tâche",
"empty_state_team_no_issues_comic_title": "Les tâches sont les blocs de construction dans Plane.",
"empty_state_team_no_issues_comic_description": "Refaire le design de l'UI de Plane, Rebaptiser l'entreprise, ou Lancer le nouveau système d'injection de carburant sont des exemples de tâches qui ont probablement des sous-tâches.",
"empty_state_project_settings_labels_title": "Aucune étiquette pour le moment",
"empty_state_project_settings_labels_description": "Créez des étiquettes pour aider à organiser et filtrer les tâches dans votre projet.",
"empty_state_project_settings_integrations_title": "Aucune intégration configurée",
"empty_state_project_settings_integrations_description": "Configurez GitHub et d'autres intégrations pour synchroniser les tâches de votre projet.",
"empty_state_project_settings_estimate_title": "Aucune estimation ajoutée",
"empty_state_project_settings_estimate_description": "Créez un ensemble d'estimations pour communiquer la quantité de travail par tâche.",
"empty_state_project_cycles_title": "Regroupez et délimitez votre travail en Cycles.",
"empty_state_project_cycles_description": "Découpez le travail en morceaux temporels, travaillez à rebours depuis la date limite de votre projet pour définir les dates, et réalisez des progrès tangibles en équipe.",
"empty_state_project_cycles_primary_button": "Définir votre premier cycle",
"empty_state_project_cycles_comic_title": "Les cycles sont des boîtes temporelles répétitives.",
"empty_state_project_cycles_comic_description": "Un sprint, une itération, ou tout autre terme que vous utilisez pour le suivi hebdomadaire ou bimensuel du travail est un cycle.",
"empty_state_project_module_title": "Cartographiez les jalons de votre projet en Modules et suivez facilement le travail agrégé",
"empty_state_project_module_description": "Un groupe de tâches appartenant à un parent logique et hiérarchique forme un module. Pensez-y comme une façon de suivre le travail par jalons de projet. Ils ont leurs propres périodes et échéances, ainsi que des analyses pour vous aider à voir à quel point vous êtes proche ou loin d'un jalon.",
"empty_state_project_module_primary_button": "Construire votre premier module",
"empty_state_project_module_comic_title": "Les modules aident à regrouper le travail par hiérarchie.",
"empty_state_project_module_comic_description": "Un module de panier, un module de châssis et un module d'entrepôt sont de bons exemples de ce regroupement.",
"empty_state_project_view_title": "Enregistrez des vues filtrées pour votre projet. Créez-en autant que nécessaire",
"empty_state_project_view_description": "Les vues sont un ensemble de filtres que vous utilisez fréquemment ou auxquels vous souhaitez accéder facilement. Tous vos collègues dans un projet peuvent voir les vues de tout le monde et choisir celle qui convient le mieux à leurs besoins.",
"empty_state_project_view_primary_button": "Créer votre première vue",
"empty_state_project_view_comic_title": "Les vues fonctionnent sur les propriétés des Tâches.",
"empty_state_project_view_comic_description": "Vous pouvez créer une vue à partir d'ici avec autant de propriétés et de filtres que vous le jugez approprié.",
"empty_state_project_page_title": "Écrivez une note, un document ou une base de connaissances complète. Obtenez l'aide de Galileo, l'assistant IA de Plane",
"empty_state_project_page_description": "Les pages sont l'espace de réflexion dans Plane. Prenez des notes de réunion, formatez-les facilement, intégrez des tâches, disposez-les à l'aide d'une bibliothèque de composants et gardez-les toutes dans le contexte de votre projet. Pour faire un travail rapide de tout document, invoquez Galileo, l'IA de Plane, avec un raccourci ou un clic sur un bouton.",
"empty_state_project_page_primary_button": "Créer votre première page",
"empty_state_project_page_private_title": "Aucune page privée pour le moment",
"empty_state_project_page_private_description": "Gardez vos pensées privées ici. Quand vous serez prêt à partager, l'équipe n'est qu'à un clic.",
"empty_state_project_page_private_primary_button": "Créer votre première page",
"empty_state_project_page_public_title": "Aucune page publique pour le moment",
"empty_state_project_page_public_description": "Consultez les pages partagées avec tout le monde dans votre projet ici.",
"empty_state_project_page_public_primary_button": "Créer votre première page",
"empty_state_workspace_page_title": "Écrivez une note, un document ou une base de connaissances complète. Obtenez l'aide de Galileo, l'assistant IA de Plane",
"empty_state_workspace_page_description": "Les pages sont l'espace de réflexion dans Plane. Prenez des notes de réunion, formatez-les facilement, intégrez des tâches, disposez-les à l'aide d'une bibliothèque de composants et gardez-les toutes dans le contexte de votre espace de travail.",
"empty_state_workspace_page_primary_button": "Créer votre première page",
"empty_state_notification_all_empty_state_title": "Aucune tâche assignée",
"empty_state_notification_all_empty_state_description": "Les mises à jour des tâches qui vous sont assignées peuvent être vues ici",
"empty_state_notification_unread_empty_state_title": "Aucune notification non lue",
"empty_state_notification_unread_empty_state_description": "Félicitations, vous êtes à jour avec tout ce qui se passe dans les tâches qui vous intéressent",
"empty_state_active_cycle_progress_empty_state_title": "Ajoutez des tâches au cycle pour voir sa progression",
"empty_state_workspace_active_cycles_title": "Aucun cycle actif",
"empty_state_workspace_active_cycles_description": "Les cycles de vos projets qui incluent toute période englobant la date d'aujourd'hui dans leur plage. Trouvez ici la progression et les détails de tous vos cycles actifs.",
"empty_state_disabled_project_inbox_title": "La boîte de réception n'est pas activée pour le projet",
"empty_state_disabled_project_inbox_description": "La boîte de réception vous aide à gérer les demandes entrantes dans votre projet et à les ajouter comme tâches dans votre flux de travail. Activez la boîte de réception depuis les paramètres du projet pour gérer les demandes.",
"empty_state_disabled_project_inbox_primary_button": "Gérer les fonctionnalités",
"empty_state_workspace_project_not_found_title": "Ce projet n'existe pas",
"empty_state_workspace_project_not_found_description": "Pour créer des tâches ou gérer votre travail, vous devez créer un projet ou faire partie d'un projet existant.",
"empty_state_workspace_project_not_found_primary_button": "Créer un projet",
"empty_state_workspace_project_not_found_comic_title": "Tout commence par un projet dans Plane",
"empty_state_workspace_project_not_found_comic_description": "Un projet pourrait être la feuille de route d'un produit, une campagne marketing ou le lancement d'une nouvelle voiture.",
"empty_state_workspace_no_projects_title": "Aucun projet",
"empty_state_workspace_no_projects_description": "Pour créer des tâches ou gérer votre travail, vous devez créer un projet ou faire partie d'un projet existant.",
"empty_state_workspace_no_projects_primary_button": "Commencer votre premier projet",
"empty_state_workspace_no_projects_comic_title": "Tout commence par un projet dans Plane",
"empty_state_workspace_no_projects_comic_description": "Un projet pourrait être la feuille de route d'un produit, une campagne marketing ou le lancement d'une nouvelle voiture.",
"empty_state_project_cycle_no_issues_title": "Aucune tâche ajoutée au cycle",
"empty_state_project_cycle_no_issues_description": "Ajoutez ou créez des tâches que vous souhaitez programmer et livrer dans ce cycle",
"empty_state_project_cycle_no_issues_primary_button": "Créer une nouvelle tâche",
"empty_state_project_cycle_no_issues_secondary_button": "Ajouter une tâche existante",
"empty_state_project_cycle_active_title": "Aucun cycle actif",
"empty_state_project_cycle_active_description": "Un cycle actif inclut toute période qui englobe la date d'aujourd'hui dans sa plage. Trouvez ici l'avancement et les détails du cycle actif.",
"empty_state_project_cycle_completed_no_issues_title": "Aucune tâche dans le cycle",
"empty_state_project_cycle_completed_no_issues_description": "Aucune tâche dans le cycle. Les tâches sont soit transférées, soit masquées. Pour voir les tâches masquées, s'il y en a, mettez à jour vos propriétés d'affichage en conséquence.",
"empty_state_command_k_search_empty_state_title": "Aucun résultat trouvé",
"empty_state_issue_relation_search_empty_state_title": "Aucune tâche correspondante trouvée",
"empty_state_issue_relation_empty_state_title": "Aucune tâche trouvée",
"empty_state_issue_comment_empty_state_title": "Aucun commentaire pour le moment",
"empty_state_issue_comment_empty_state_description": "Les commentaires peuvent être utilisés comme espace de discussion et de suivi pour les tâches",
"empty_state_notification_detail_empty_state_title": "Sélectionnez pour voir les détails.",
"empty_state_notification_mentions_empty_state_title": "Aucune tâche assignée",
"empty_state_notification_mentions_empty_state_description": "Les mises à jour des tâches qui vous sont assignées peuvent être vues ici",
"empty_state_notification_my_issue_empty_state_title": "Aucune tâche assignée",
"empty_state_notification_my_issue_empty_state_description": "Les mises à jour des tâches qui vous sont assignées peuvent être vues ici",
"empty_state_notification_created_empty_state_title": "Aucune mise à jour des tâches",
"empty_state_notification_created_empty_state_description": "Les mises à jour des tâches que vous avez créées peuvent être vues ici",
"empty_state_notification_subscribed_empty_state_title": "Aucune mise à jour des tâches",
"empty_state_notification_subscribed_empty_state_description": "Les mises à jour de toute tâche à laquelle vous êtes abonné peuvent être vues ici",
"empty_state_notification_snoozed_empty_state_title": "Aucune notification mise en sommeil pour le moment",
"empty_state_notification_snoozed_empty_state_description": "Toute notification que vous mettez en sommeil pour plus tard sera disponible ici pour y donner suite",
"empty_state_notification_archived_empty_state_title": "Aucune notification archivée pour le moment",
"empty_state_notification_archived_empty_state_description": "Toute notification que vous archivez sera disponible ici pour vous aider à vous concentrer",
"empty_state_active_cycle_chart_empty_state_title": "Ajoutez des tâches au cycle pour voir le graphique d'avancement.",
"empty_state_active_cycle_priority_issue_empty_state_title": "Observez en un coup d'œil les tâches de haute priorité traitées dans le cycle.",
"empty_state_active_cycle_assignee_empty_state_title": "Ajoutez des assignés aux tâches pour voir une répartition du travail par assignés.",
"empty_state_active_cycle_label_empty_state_title": "Ajoutez des étiquettes aux tâches pour voir la répartition du travail par étiquettes.",
"empty_state_disabled_project_cycle_title": "Les cycles ne sont pas activés pour ce projet.",
"empty_state_disabled_project_cycle_description": "Découpez le travail en morceaux temporels, travaillez à rebours à partir de la date limite de votre projet pour définir les dates, et réalisez des progrès tangibles en équipe. Activez la fonctionnalité des cycles pour votre projet pour commencer à les utiliser.",
"empty_state_disabled_project_cycle_primary_button": "Gérer les fonctionnalités",
"empty_state_disabled_project_module_title": "Les modules ne sont pas activés pour ce projet.",
"empty_state_disabled_project_module_description": "Un groupe de tâches qui appartiennent à un parent logique et hiérarchique forme un module. Considérez-les comme un moyen de suivre le travail par jalons de projet. Activez les modules dans les paramètres du projet.",
"empty_state_disabled_project_module_primary_button": "Gérer les fonctionnalités",
"empty_state_disabled_project_page_title": "Les pages ne sont pas activées pour ce projet.",
"empty_state_disabled_project_page_description": "Les pages sont un espace de réflexion dans Plane. Prenez des notes de réunion, formatez-les facilement, intégrez des tâches, disposez-les à l'aide d'une bibliothèque de composants et gardez-les toutes dans le contexte de votre projet. Activez la fonctionnalité des pages pour commencer à les créer dans votre projet.",
"empty_state_disabled_project_page_primary_button": "Gérer les fonctionnalités",
"empty_state_disabled_project_view_title": "Les vues ne sont pas activées pour ce projet.",
"empty_state_disabled_project_view_description": "Les vues sont un ensemble de filtres enregistrés que vous utilisez fréquemment ou auxquels vous souhaitez un accès facile. Tous vos collègues dans un projet peuvent voir les vues de chacun et choisir celle qui convient le mieux à leurs besoins. Activez les vues dans les paramètres du projet pour commencer à les utiliser.",
"empty_state_disabled_project_view_primary_button": "Gérer les fonctionnalités",
"empty_state_inbox_sidebar_open_tab_title": "Aucune tâche ouverte",
"empty_state_inbox_sidebar_open_tab_description": "Trouvez ici les tâches ouvertes. Créez une nouvelle tâche.",
"empty_state_inbox_sidebar_closed_tab_title": "Aucune tâche fermée",
"empty_state_inbox_sidebar_closed_tab_description": "Toutes les tâches, qu'elles soient acceptées ou refusées, peuvent être trouvées ici.",
"empty_state_inbox_sidebar_filter_empty_state_title": "Aucune tâche correspondante",
"empty_state_inbox_sidebar_filter_empty_state_description": "Aucune tâche ne correspond au filtre appliqué dans la boîte de réception. Créez une nouvelle tâche.",
"empty_state_inbox_detail_empty_state_title": "Sélectionnez une tâche pour voir ses détails.",
"empty_state_views_empty_search_title": "Aucune vue correspondante",
"empty_state_views_empty_search_description": "Aucune vue ne correspond aux critères de recherche. Créez plutôt une nouvelle vue.",
"empty_state_projects_empty_search_title": "Aucun projet correspondant",
"empty_state_projects_empty_search_description": "Aucun projet détecté avec les critères correspondants. Créez plutôt un nouveau projet.",
"empty_state_members_empty_search_title": "Aucun membre correspondant",
"empty_state_members_empty_search_description": "Ajoutez-les au projet s'ils font déjà partie de l'espace de travail",
"empty_state_project_module_issues_title": "Aucune tâche dans le module",
"empty_state_project_module_issues_description": "Créez ou ajoutez des tâches que vous souhaitez accomplir dans le cadre de ce module",
"empty_state_project_module_issues_primary_button": "Créer une nouvelle tâche",
"empty_state_project_module_issues_secondary_button": "Ajouter une tâche existante",
"empty_state_project_archived_no_modules_title": "Aucun module archivé pour le moment",
"empty_state_project_archived_no_modules_description": "Pour ranger votre projet, archivez les modules terminés ou annulés. Retrouvez-les ici une fois archivés.",
"empty_state_project_empty_filter_title": "Aucune tâche trouvée correspondant aux filtres appliqués",
"empty_state_project_empty_filter_secondary_button": "Effacer tous les filtres",
"empty_state_project_archived_empty_filter_title": "Aucune tâche trouvée correspondant aux filtres appliqués",
"empty_state_project_archived_empty_filter_secondary_button": "Effacer tous les filtres",
"empty_state_project_draft_empty_filter_title": "Aucune tâche trouvée correspondant aux filtres appliqués",
"empty_state_project_draft_empty_filter_secondary_button": "Effacer tous les filtres",
"empty_state_project_no_issues_title": "Créez une tâche et assignez-la à quelqu'un, même à vous-même",
"empty_state_project_no_issues_description": "Pensez aux tâches comme des emplois, des tâches, du travail ou JTBD. Ce que nous aimons. Une tâche et ses sous-tâches sont généralement des actions temporelles assignées aux membres de votre équipe. Votre équipe crée, assigne et complète des tâches pour faire avancer votre projet vers son objectif.",
"empty_state_project_no_issues_primary_button": "Créez votre première tâche",
"empty_state_project_no_issues_comic_title": "Les tâches sont les blocs de construction dans Plane.",
"empty_state_project_no_issues_comic_description": "Refaire le design de l'interface utilisateur de Plane, rebaptiser l'entreprise ou lancer le nouveau système d'injection de carburant sont des exemples de tâches qui ont probablement des sous-tâches.",
"empty_state_project_archived_no_issues_title": "Aucune tâche archivée pour le moment",
"empty_state_project_archived_no_issues_description": "Manuellement ou par automatisation, vous pouvez archiver les tâches terminées ou annulées. Retrouvez-les ici une fois archivées.",
"empty_state_project_archived_no_issues_primary_button": "Configurer l'automatisation",
"empty_state_project_draft_no_issues_title": "Aucun brouillon de tâche pour le moment",
"empty_state_project_draft_no_issues_description": "Vous vous absentez rapidement mais vous voulez garder votre place ? Pas de souci - enregistrez un brouillon maintenant. Vos tâches seront là à vous attendre.",
"empty_state_project_page_archived_title": "Aucune page archivée pour le moment",
"empty_state_project_page_archived_description": "Archivez les pages qui ne sont pas sur votre radar. Accédez-y ici quand vous en aurez besoin.",
"empty_state_workspace_page_private_title": "Aucune page privée pour le moment",
"empty_state_workspace_page_private_description": "Gardez vos pensées privées ici. Quand vous serez prêt à partager, l'équipe n'est qu'à un clic.",
"empty_state_workspace_page_private_primary_button": "Créer votre première page",
"empty_state_workspace_page_public_title": "Aucune page publique pour le moment",
"empty_state_workspace_page_public_description": "Consultez ici les pages partagées avec tout le monde dans votre espace de travail.",
"empty_state_workspace_page_public_primary_button": "Créer votre première page",
"empty_state_workspace_page_archived_title": "Aucune page archivée pour le moment",
"empty_state_workspace_page_archived_description": "Archivez les pages qui ne sont pas sur votre radar. Accédez-y ici quand vous en aurez besoin.",
"empty_state_workspace_draft_issues_title": "Les tâches à moitié rédigées, et bientôt les commentaires, apparaîtront ici.",
"empty_state_workspace_draft_issues_description": "Pour essayer cela, commencez à ajouter une tâche et laissez-la en cours de route ou créez votre premier brouillon ci-dessous. 😉",
"empty_state_workspace_draft_issues_primary_button": "Créer votre premier brouillon"
"inbox": "Boîte de réception"
}
+1 -245
View File
@@ -316,249 +316,5 @@
"remove_parent_issue": "親問題を削除",
"add_parent": "親問題を追加",
"loading_members": "メンバーを読み込んでいます...",
"inbox": "受信箱",
"empty_state_workspace_dashboard_title": "プロジェクト、アクティビティ、メトリクスの概要",
"empty_state_workspace_dashboard_description": "Planeへようこそ。最初のプロジェクトを作成し、課題を追跡してください。このページは進捗を支援するスペースに変わります。管理者はチームの進捗を支援する項目も確認できます。",
"empty_state_workspace_dashboard_primary_button": "最初のプロジェクトを作成",
"empty_state_workspace_dashboard_comic_title": "すべてはPlaneのプロジェクトから始まります",
"empty_state_workspace_dashboard_comic_description": "プロジェクトは、製品のロードマップ、マーケティングキャンペーン、新車の発売などです。",
"empty_state_workspace_analytics_title": "進捗状況、ワークロード、割り当てを追跡。トレンドを把握し、ブロッカーを取り除き、作業を迅速に進めます",
"empty_state_workspace_analytics_description": "スコープと需要、見積もり、スコープクリープを確認します。チームメンバーとチームのパフォーマンスを把握し、プロジェクトが時間通りに進むようにします。",
"empty_state_workspace_analytics_primary_button": "最初のプロジェクトを開始",
"empty_state_workspace_analytics_comic_title": "アナリティクスはサイクル+モジュールで最適に機能します",
"empty_state_workspace_analytics_comic_description": "まず、課題をサイクルにタイムボックス化し、可能であれば、サイクルを超えて広がる課題をモジュールにグループ化します。左のナビゲーションで両方を確認してください。",
"empty_state_workspace_projects_title": "アクティブなプロジェクトがありません",
"empty_state_workspace_projects_description": "各プロジェクトは目標指向の作業の親として考えてください。プロジェクトには、ジョブ、サイクル、モジュールが含まれ、同僚と協力して目標達成を支援します。新しいプロジェクトを作成するか、アーカイブされたプロジェクトをフィルタリングしてください。",
"empty_state_workspace_projects_primary_button": "最初のプロジェクトを開始",
"empty_state_workspace_projects_comic_title": "すべてはPlaneのプロジェクトから始まります",
"empty_state_workspace_projects_comic_description": "プロジェクトは、製品のロードマップ、マーケティングキャンペーン、新車の発売などです。",
"empty_state_workspace_teams_title": "チーム",
"empty_state_workspace_teams_description": "チームはプロジェクトで協力する人々のグループです。チームを作成して始めましょう。",
"empty_state_workspace_teams_primary_button": "新しいチームを作成",
"empty_state_workspace_initiatives_title": "イニシアチブで最高レベルの作業を整理",
"empty_state_workspace_initiatives_description": "複数のプロジェクトやチームにまたがる作業を整理する必要がある場合、イニシアチブが役立ちます。プロジェクトとエピックをイニシアチブに接続し、自動的に更新をロールアップし、木々を見る前に森を見ます。",
"empty_state_workspace_initiatives_primary_button": "イニシアチブを作成",
"empty_state_workspace_initiatives_empty_search_title": "一致するイニシアチブがありません",
"empty_state_workspace_initiatives_empty_search_description": "一致する条件のイニシアチブは検出されません。\n代わりに新しいイニシアチブを作成してください。",
"empty_state_workspace_all_issues_title": "プロジェクトに課題がありません",
"empty_state_workspace_all_issues_description": "最初のプロジェクトが完了しました!次は、作業を追跡可能な部分に分割しましょう。頑張りましょう!",
"empty_state_workspace_all_issues_primary_button": "新しい課題を作成",
"empty_state_workspace_assigned_title": "まだ課題がありません",
"empty_state_workspace_assigned_description": "あなたに割り当てられた課題はここで追跡できます。",
"empty_state_workspace_assigned_primary_button": "新しい課題を作成",
"empty_state_workspace_created_title": "まだ課題がありません",
"empty_state_workspace_created_description": "あなたが作成したすべての課題がここに表示され、直接追跡できます。",
"empty_state_workspace_created_primary_button": "新しい課題を作成",
"empty_state_workspace_subscribed_title": "まだ課題がありません",
"empty_state_workspace_subscribed_description": "興味のある課題をサブスクライブし、それらをすべてここで追跡します。",
"empty_state_workspace_custom_view_title": "まだ課題がありません",
"empty_state_workspace_custom_view_description": "フィルターに適用される課題を、ここですべて追跡します。",
"empty_state_project_no_epics_title": "エピックを作成し、誰かに(自分自身でも)割り当てます",
"empty_state_project_no_epics_description": "複数のサイクルにまたがり、モジュール間で存在できる大規模な作業の場合、エピックを作成します。プロジェクト内の課題とサブ課題をエピックにリンクし、概要から課題にジャンプします。",
"empty_state_project_no_epics_primary_button": "エピックを作成",
"empty_state_team_no_issues_title": "チームプロジェクトで課題を作成し、誰かに(自分自身でも)割り当てます",
"empty_state_team_no_issues_description": "課題は仕事、タスク、作業、またはJTBDとして考えてください。課題とそのサブ課題は通常、チームのメンバーに割り当てられた時間ベースのアクション項目です。チームが課題を作成、割り当て、完了させることで、プロジェクトの目標達成に向けて進みます。",
"empty_state_team_no_issues_primary_button": "最初の課題を作成",
"empty_state_team_no_issues_comic_title": "課題はPlaneの構成要素です。",
"empty_state_team_no_issues_comic_description": "Plane UIの再設計、会社のリブランド、新しい燃料噴射システムの発売など、サブ課題を持つ可能性が高い課題の例です。",
"empty_state_inbox_sidebar_filter_empty_state_title": "一致する課題がありません",
"empty_state_inbox_sidebar_filter_empty_state_description": "受信箱で適用されたフィルターに一致する課題はありません。新しい課題を作成してください。",
"empty_state_inbox_detail_empty_state_title": "詳細を表示する課題を選択してください。",
"empty_state_views_empty_search_title": "一致するビューがありません",
"empty_state_views_empty_search_description": "検索条件に一致するビューがありません。代わりに新しいビューを作成してください。",
"empty_state_projects_empty_search_title": "一致するプロジェクトがありません",
"empty_state_projects_empty_search_description": "一致する条件のプロジェクトは検出されません。代わりに新しいプロジェクトを作成してください。",
"empty_state_members_empty_search_title": "一致するメンバーがありません",
"empty_state_members_empty_search_description": "ワークスペースのメンバーの場合は、プロジェクトに追加してください。",
"empty_state_project_module_issues_title": "モジュールに課題がありません",
"empty_state_project_module_issues_description": "このモジュールの一部として達成したい課題を作成または追加してください",
"empty_state_project_module_issues_primary_button": "新しい課題を作成",
"empty_state_project_module_issues_secondary_button": "既存の課題を追加",
"empty_state_project_archived_no_modules_title": "アーカイブされたモジュールはまだありません",
"empty_state_project_archived_no_modules_description": "プロジェクトを整理するために、完了またはキャンセルされたモジュールをアーカイブします。アーカイブ後はここで確認できます。",
"empty_state_project_empty_filter_title": "フィルターに一致する課題が見つかりません",
"empty_state_project_empty_filter_secondary_button": "すべてのフィルターをクリア",
"empty_state_project_archived_empty_filter_title": "フィルターに一致する課題が見つかりません",
"empty_state_project_archived_empty_filter_secondary_button": "すべてのフィルターをクリア",
"empty_state_project_draft_empty_filter_title": "フィルターに一致する課題が見つかりません",
"empty_state_project_draft_empty_filter_secondary_button": "すべてのフィルターをクリア",
"empty_state_project_no_issues_title": "誰かに(自分自身でも)課題を作成して割り当てましょう",
"empty_state_project_no_issues_description": "課題は仕事、タスク、作業、またはJTBDとして考えてください。課題とそのサブ課題は通常、チームのメンバーに割り当てられた時間ベースのアクション項目です。チームが課題を作成、割り当て、完了させることで、プロジェクトの目標達成に向けて進みます。",
"empty_state_project_no_issues_primary_button": "最初の課題を作成",
"empty_state_project_no_issues_comic_title": "課題はPlaneの構成要素です。",
"empty_state_project_no_issues_comic_description": "Plane UIの再設計、会社のリブランド、新しい燃料噴射システムの発売など、サブ課題を持つ可能性が高い課題の例です。",
"empty_state_project_archived_no_issues_title": "アーカイブされた課題はまだありません",
"empty_state_project_archived_no_issues_description": "手動または自動化を通じて、完了またはキャンセルされた課題をアーカイブできます。アーカイブ後はここで確認できます。",
"empty_state_project_archived_no_issues_primary_button": "自動化を設定",
"empty_state_project_draft_no_issues_title": "下書き課題はまだありません",
"empty_state_project_draft_no_issues_description": "急いで離れる必要がありますが、場所を保持したいですか?心配いりません – 下書きを保存してください。課題はここで待っています。",
"empty_state_project_page_title": "メモ、ドキュメント、または完全なナレッジベースを作成します。Planeのプロジェクトで最初のメモを書きましょう。",
"empty_state_project_page_description": "メモを取り、簡単にフォーマットし、課題を埋め込み、コンポーネントのライブラリを使用してレイアウトし、すべてをプロジェクトのコンテキストに保持します。ドキュメントを簡単に作成するには、Planeのページエディターを使用してください。",
"empty_state_project_page_primary_button": "最初のページを作成",
"empty_state_project_page_private_title": "プライベートページはまだありません",
"empty_state_project_page_private_description": "ここにプライベートな考えを保持します。共有する準備ができたら、チームはクリック一つで利用できます。",
"empty_state_project_page_private_primary_button": "最初のページを作成",
"empty_state_project_page_public_title": "公開ページはまだありません",
"empty_state_project_page_public_description": "プロジェクト内の全員と共有されているページをここで確認します。",
"empty_state_project_page_public_primary_button": "最初のページを作成",
"empty_state_project_page_archived_title": "アーカイブされたページはまだありません",
"empty_state_project_page_archived_description": "レーダーにないページをアーカイブします。必要な時にここでアクセスできます。",
"empty_state_workspace_page_title": "メモ、ドキュメント、または完全なナレッジベースを作成します。Plane AIアシスタントのGalileoが開始をサポートします",
"empty_state_workspace_page_description": "ページはPlaneでの思考のスペースです。会議のメモを取り、簡単にフォーマットし、課題を埋め込み、コンポーネントのライブラリを使用してレイアウトし、すべてをプロジェクトのコンテキストに保持します。ドキュメントを素早く作成するには、ショートカットまたはボタンのクリックでGalileo、Plane AIを呼び出します。",
"empty_state_workspace_page_primary_button": "最初のページを作成",
"empty_state_workspace_page_private_title": "プライベートページはまだありません",
"empty_state_workspace_page_private_description": "ここにプライベートな考えを保持します。共有する準備ができたら、チームはクリック一つで利用できます。",
"empty_state_workspace_page_private_primary_button": "最初のページを作成",
"empty_state_workspace_page_public_title": "公開ページはまだありません",
"empty_state_workspace_page_public_description": "ワークスペース内の全員と共有されているページをここで確認します。",
"empty_state_workspace_page_public_primary_button": "最初のページを作成",
"empty_state_workspace_page_archived_title": "アーカイブされたページはまだありません",
"empty_state_workspace_page_archived_description": "レーダーにないページをアーカイブします。必要な時にここでアクセスできます。",
"empty_state_command_k_search_empty_state_title": "結果が見つかりません",
"empty_state_issue_relation_search_empty_state_title": "一致する課題が見つかりません",
"empty_state_issue_relation_empty_state_title": "課題が見つかりません",
"empty_state_issue_comment_empty_state_title": "コメントはまだありません",
"empty_state_issue_comment_empty_state_description": "コメントは課題のディスカッションとフォローアップのスペースとして使用できます",
"empty_state_notification_detail_empty_state_title": "詳細を表示するには選択してください。",
"empty_state_notification_mentions_empty_state_title": "課題が割り当てられていません",
"empty_state_notification_mentions_empty_state_description": "あなたに割り当てられた課題の更新はここで確認できます",
"empty_state_notification_my_issue_empty_state_title": "課題が割り当てられていません",
"empty_state_notification_my_issue_empty_state_description": "あなたに割り当てられた課題の更新はここで確認できます",
"empty_state_notification_created_empty_state_title": "課題への更新はありません",
"empty_state_notification_created_empty_state_description": "あなたが作成した課題への更新はここで確認できます",
"empty_state_notification_subscribed_empty_state_title": "課題への更新はありません",
"empty_state_notification_subscribed_empty_state_description": "サブスクライブしている課題への更新はここで確認できます",
"empty_state_notification_snoozed_empty_state_title": "スヌーズされた通知はまだありません",
"empty_state_notification_snoozed_empty_state_description": "後で対応するためにスヌーズした通知はここで利用できます",
"empty_state_notification_archived_empty_state_title": "アーカイブされた通知はまだありません",
"empty_state_notification_archived_empty_state_description": "アーカイブした通知は、フォーカスを助けるためにここで利用できます",
"empty_state_active_cycle_chart_empty_state_title": "バーンダウンチャートを表示するには課題をサイクルに追加してください。",
"empty_state_active_cycle_priority_issue_empty_state_title": "サイクル内で取り組まれている高優先度の課題を一目で確認します。",
"empty_state_active_cycle_assignee_empty_state_title": "担当者別の作業の内訳を確認するには、課題に担当者を追加してください。",
"empty_state_active_cycle_label_empty_state_title": "ラベル別の作業の内訳を確認するには、課題にラベルを追加してください。",
"empty_state_disabled_project_cycle_title": "このプロジェクトではサイクルが有効になっていません。",
"empty_state_disabled_project_cycle_description": "作業をタイムボックス化された塊に分割し、プロジェクトの期限から逆算して日付を設定し、チームとして実質的な進捗を作ります。プロジェクトでサイクル機能を有効にして使用を開始してください。",
"empty_state_disabled_project_cycle_primary_button": "機能を管理",
"empty_state_disabled_project_module_title": "このプロジェクトではモジュールが有効になっていません。",
"empty_state_disabled_project_module_description": "論理的な階層的な親に属する課題のグループがモジュールを形成します。プロジェクトのマイルストーンによって作業を追跡する方法として考えてください。プロジェクト設定でモジュールを有効にしてください。",
"empty_state_disabled_project_module_primary_button": "機能を管理",
"empty_state_disabled_project_view_title": "このプロジェクトではビューが有効になっていません。",
"empty_state_disabled_project_view_description": "ビューは、頻繁に使用するか簡単にアクセスしたいフィルターのセットです。プロジェクト内のすべての同僚が全員のビューを見ることができ、ニーズに最適なものを選択できます。プロジェクト設定でビューを有効にして使用を開始してください。",
"empty_state_disabled_project_view_primary_button": "機能を管理",
"empty_state_workspace_draft_issues_title": "半分書かれた課題、そしてまもなく、コメントがここに表示されます。",
"empty_state_workspace_draft_issues_description": "これを試すには、課題の追加を開始して途中で離れるか、以下で最初の下書きを作成してください。😉",
"empty_state_workspace_draft_issues_primary_button": "最初の下書きを作成",
"empty_state_workspace_project_not_found_title": "そのようなプロジェクトは存在しません",
"empty_state_workspace_project_not_found_description": "課題を作成したり作業を管理したりするには、プロジェクトを作成するか、既存のプロジェクトに参加する必要があります。",
"empty_state_workspace_project_not_found_primary_button": "プロジェクトを作成",
"empty_state_workspace_project_not_found_comic_title": "Planeではすべてがプロジェクトから始まります",
"empty_state_workspace_project_not_found_comic_description": "プロジェクトは製品のロードマップ、マーケティングキャンペーン、新車の発売などです。",
"empty_state_workspace_no_projects_title": "プロジェクトがありません",
"empty_state_workspace_no_projects_description": "課題を作成したり作業を管理したりするには、プロジェクトを作成するか、既存のプロジェクトに参加する必要があります。",
"empty_state_workspace_no_projects_primary_button": "最初のプロジェクトを開始",
"empty_state_workspace_no_projects_comic_title": "Planeではすべてがプロジェクトから始まります",
"empty_state_workspace_no_projects_comic_description": "プロジェクトは製品のロードマップ、マーケティングキャンペーン、新車の発売などです。",
"empty_state_project_cycle_no_issues_title": "サイクルに課題が追加されていません",
"empty_state_project_cycle_no_issues_description": "このサイクル内でタイムボックス化して配信したい課題を追加または作成してください",
"empty_state_project_cycle_no_issues_primary_button": "新しい課題を作成",
"empty_state_project_cycle_no_issues_secondary_button": "既存の課題を追加",
"empty_state_project_cycle_active_title": "アクティブなサイクルがありません",
"empty_state_project_cycle_active_description": "アクティブなサイクルには、その範囲内に今日の日付を含む期間が含まれます。アクティブなサイクルの進捗状況と詳細はここで確認できます。",
"empty_state_project_cycle_completed_no_issues_title": "サイクルに課題がありません",
"empty_state_project_cycle_completed_no_issues_description": "サイクルに課題がありません。課題は転送されるか非表示になっています。非表示の課題がある場合は、表示プロパティを更新して確認してください。",
"empty_state_team_empty_filter_title": "適用されたフィルターに一致する課題が見つかりません",
"empty_state_team_empty_filter_secondary_button": "すべてのフィルターをクリア",
"empty_state_team_view_title": "チーム用のフィルタリングされたビューを保存します。必要な数だけ作成できます",
"empty_state_team_view_description": "ビューは、頻繁に使用するか簡単にアクセスしたいフィルターのセットです。チーム内のすべての同僚が全員のビューを見ることができ、ニーズに最適なものを選択できます。",
"empty_state_team_view_primary_button": "最初のビューを作成",
"empty_state_team_view_comic_title": "ビューは課題のプロパティ上で機能します。",
"empty_state_team_view_comic_description": "ここから、適切と思われる数のプロパティをフィルターとしてビューを作成できます。",
"empty_state_project_settings_labels_title": "ラベルがまだありません",
"empty_state_project_settings_labels_description": "プロジェクト内の課題を整理しフィルタリングするのに役立つラベルを作成してください。",
"empty_state_project_settings_integrations_title": "設定された統合がありません",
"empty_state_project_settings_integrations_description": "プロジェクトの課題を同期するためにGitHubやその他の統合を設定してください。",
"empty_state_project_settings_estimate_title": "見積もりが追加されていません",
"empty_state_project_settings_estimate_description": "課題ごとの作業量を伝えるための見積もりのセットを作成してください。",
"empty_state_inbox_sidebar_open_tab_title": "オープンな課題はありません",
"empty_state_inbox_sidebar_open_tab_description": "オープンな課題はここで見つかります。新しい課題を作成してください。",
"empty_state_inbox_sidebar_closed_tab_title": "クローズされた課題はありません",
"empty_state_inbox_sidebar_closed_tab_description": "受け入れられたか却下されたかにかかわらず、すべての課題はここで見つかります。",
"empty_state_disabled_project_page_title": "ページはこのプロジェクトで有効になっていません。",
"empty_state_disabled_project_page_description": "ページはPlaneでの思考のスペースです。会議のメモを取り、簡単にフォーマットし、課題を埋め込み、コンポーネントのライブラリを使用してレイアウトし、すべてをプロジェクトのコンテキストに保持します。プロジェクトでページ機能を有効にして作成を開始してください。",
"empty_state_disabled_project_page_primary_button": "機能を管理",
"empty_state_active_cycle_progress_empty_state_title": "進捗を表示するには、サイクルに課題を追加してください",
"empty_state_workspace_active_cycles_title": "アクティブなサイクルがありません",
"empty_state_workspace_active_cycles_description": "プロジェクトのサイクルで、その範囲内に今日の日付を含む期間があるもの。すべてのアクティブなサイクルの進捗と詳細はここで確認できます。",
"empty_state_disabled_project_inbox_title": "受信箱はこのプロジェクトで有効になっていません",
"empty_state_disabled_project_inbox_description": "受信箱は、プロジェクトへの着信リクエストを管理し、ワークフローに課題として追加するのに役立ちます。リクエストを管理するには、プロジェクト設定から受信箱を有効にしてください。",
"empty_state_disabled_project_inbox_primary_button": "機能を管理"
"inbox": "受信箱"
}
@@ -0,0 +1,319 @@
{
"submit": "提交",
"cancel": "取消",
"loading": "加载中",
"error": "错误",
"success": "成功",
"warning": "警告",
"info": "信息",
"close": "关闭",
"yes": "是",
"no": "否",
"ok": "确定",
"name": "名称",
"description": "描述",
"search": "搜索",
"add_member": "添加成员",
"remove_member": "移除成员",
"add_members": "添加成员",
"remove_members": "移除成员",
"add": "添加",
"remove": "移除",
"add_new": "新增",
"remove_selected": "移除选中项",
"first_name": "名字",
"last_name": "姓氏",
"email": "电子邮件",
"display_name": "显示名称",
"role": "角色",
"timezone": "时区",
"avatar": "头像",
"cover_image": "封面图片",
"password": "密码",
"change_cover": "更换封面",
"language": "语言",
"saving": "保存中...",
"save_changes": "保存更改",
"deactivate_account": "停用账户",
"deactivate_account_description": "停用账户后,该账户内的所有数据和资源将被永久删除,且无法恢复。",
"profile_settings": "个人资料设置",
"your_account": "账户",
"profile": "个人资料",
"security": "安全",
"activity": "活动",
"appearance": "外观",
"notifications": "通知",
"workspaces": "工作区",
"create_workspace": "创建工作区",
"invitations": "邀请",
"summary": "摘要",
"assigned": "已分配",
"created": "已创建",
"subscribed": "已订阅",
"you_do_not_have_the_permission_to_access_this_page": "您没有权限访问此页面。",
"failed_to_sign_out_please_try_again": "登出失败,请重试。",
"password_changed_successfully": "密码已成功更改。",
"something_went_wrong_please_try_again": "出错了,请重试。",
"change_password": "更改密码",
"passwords_dont_match": "密码不匹配",
"current_password": "当前密码",
"new_password": "新密码",
"confirm_password": "确认密码",
"this_field_is_required": "此字段为必填项",
"changing_password": "正在更改密码",
"please_enter_your_password": "请输入您的密码。",
"password_length_should_me_more_than_8_characters": "密码长度应大于8个字符。",
"password_is_weak": "密码强度较弱。",
"password_is_strong": "密码强度较强。",
"load_more": "加载更多",
"select_or_customize_your_interface_color_scheme": "选择或自定义界面配色方案。",
"theme": "主题",
"system_preference": "系统偏好",
"light": "浅色",
"dark": "深色",
"light_contrast": "浅色高对比",
"dark_contrast": "深色高对比",
"custom": "自定义主题",
"select_your_theme": "选择主题",
"customize_your_theme": "自定义主题",
"background_color": "背景颜色",
"text_color": "文字颜色",
"primary_color": "主色调",
"sidebar_background_color": "侧边栏背景颜色",
"sidebar_text_color": "侧边栏文字颜色",
"set_theme": "设置主题",
"enter_a_valid_hex_code_of_6_characters": "请输入6位有效的十六进制代码",
"background_color_is_required": "背景颜色为必填项",
"text_color_is_required": "文字颜色为必填项",
"primary_color_is_required": "主色调为必填项",
"sidebar_background_color_is_required": "侧边栏背景颜色为必填项",
"sidebar_text_color_is_required": "侧边栏文字颜色为必填项",
"updating_theme": "正在更新主题",
"theme_updated_successfully": "主题已成功更新",
"failed_to_update_the_theme": "主题更新失败",
"email_notifications": "电子邮件通知",
"stay_in_the_loop_on_issues_you_are_subscribed_to_enable_this_to_get_notified": "启用此功能以接收您订阅问题的通知。",
"email_notification_setting_updated_successfully": "电子邮件通知设置已成功更新",
"failed_to_update_email_notification_setting": "电子邮件通知设置更新失败",
"notify_me_when": "在以下情况下通知我",
"property_changes": "属性更改",
"property_changes_description": "当负责人、优先级、估算等属性更改时通知。",
"state_change": "状态更改",
"state_change_description": "当问题状态更改时通知",
"issue_completed": "问题完成",
"issue_completed_description": "仅在问题完成时通知",
"comments": "评论",
"comments_description": "当有人在问题中添加评论时通知",
"mentions": "提及",
"mentions_description": "仅在评论或描述中提及您时通知",
"create_your_workspace": "创建工作区",
"only_your_instance_admin_can_create_workspaces": "只有实例管理员可以创建工作区",
"only_your_instance_admin_can_create_workspaces_description": "如果您知道实例管理员的电子邮件地址,请点击以下按钮联系他们。",
"go_back": "返回",
"request_instance_admin": "请求实例管理员",
"plane_logo": "Plane 徽标",
"workspace_creation_disabled": "工作区创建已禁用",
"workspace_request_subject": "新工作区请求",
"workspace_request_body": "实例管理员:\n\n请在 URL [/workspace-name] 处创建一个新的工作区。[工作区创建目的]\n\n谢谢。\n{{firstName}} {{lastName}}\n{{email}}",
"creating_workspace": "正在创建工作区",
"workspace_created_successfully": "工作区已成功创建",
"create_workspace_page": "创建工作区页面",
"workspace_could_not_be_created_please_try_again": "无法创建工作区,请重试。",
"workspace_could_not_be_created_please_try_again_description": "创建工作区时出错,请重试。",
"this_is_a_required_field": "此字段为必填项。",
"name_your_workspace": "为工作区命名",
"workspaces_names_can_contain_only_space_dash_and_alphanumeric_characters": "工作区名称只能包含空格、破折号和字母数字字符。",
"limit_your_name_to_80_characters": "名称长度限制为80个字符。",
"set_your_workspace_url": "设置工作区URL",
"limit_your_url_to_48_characters": "URL长度限制为48个字符。",
"how_many_people_will_use_this_workspace": "有多少人将使用此工作区?",
"how_many_people_will_use_this_workspace_description": "这将帮助确定购买的席位数量。",
"select_a_range": "选择一个范围",
"urls_can_contain_only_dash_and_alphanumeric_characters": "URL只能包含破折号和字母数字字符。",
"something_familiar_and_recognizable_is_always_best": "熟悉且易于识别的名称总是最佳选择。",
"workspace_url_is_already_taken": "工作区URL已被占用!",
"old_password": "旧密码",
"general_settings": "常规设置",
"sign_out": "登出",
"signing_out": "正在登出",
"active_cycles": "活跃周期",
"active_cycles_description": "监控所有项目的周期,跟踪高优先级问题,并关注需要关注的周期。",
"on_demand_snapshots_of_all_your_cycles": "所有周期的按需快照",
"upgrade": "升级",
"10000_feet_view": "所有活跃周期的概览",
"10000_feet_view_description": "放大查看所有项目的周期,而不是单独查看每个项目的周期。",
"get_snapshot_of_each_active_cycle": "获取每个活跃周期的快照",
"get_snapshot_of_each_active_cycle_description": "跟踪所有活跃周期的高级指标,查看进度并了解范围与截止日期的关系。",
"compare_burndowns": "比较燃尽图",
"compare_burndowns_description": "监控每个团队的表现,查看每个周期的燃尽报告。",
"quickly_see_make_or_break_issues": "快速查看关键问题",
"quickly_see_make_or_break_issues_description": "预览每个周期中与截止日期相关的高优先级问题。一键查看所有周期的详细信息。",
"zoom_into_cycles_that_need_attention": "关注需要关注的周期",
"zoom_into_cycles_that_need_attention_description": "一键调查未达预期的周期状态。",
"stay_ahead_of_blockers": "提前解决阻碍",
"stay_ahead_of_blockers_description": "发现项目间的挑战,查看其他视图中不明显的周期依赖关系。",
"analytics": "分析",
"workspace_invites": "工作区邀请",
"workspace_settings": "工作区设置",
"enter_god_mode": "进入上帝模式",
"workspace_logo": "工作区徽标",
"new_issue": "新建问题",
"home": "首页",
"your_work": "您的工作",
"drafts": "草稿",
"projects": "项目",
"views": "视图",
"workspace": "工作区",
"archives": "归档",
"settings": "设置",
"failed_to_move_favorite": "移动收藏失败",
"your_favorites": "您的收藏",
"no_favorites_yet": "尚无收藏",
"create_folder": "创建文件夹",
"new_folder": "新建文件夹",
"favorite_updated_successfully": "收藏已成功更新",
"favorite_created_successfully": "收藏已成功创建",
"folder_already_exists": "文件夹已存在",
"folder_name_cannot_be_empty": "文件夹名称不能为空",
"something_went_wrong": "出错了",
"failed_to_reorder_favorite": "重新排序收藏失败",
"favorite_removed_successfully": "收藏已成功移除",
"failed_to_create_favorite": "创建收藏失败",
"failed_to_rename_favorite": "重命名收藏失败",
"project_link_copied_to_clipboard": "项目链接已复制到剪贴板",
"link_copied": "链接已复制",
"your_projects": "您的项目",
"add_project": "添加项目",
"create_project": "创建项目",
"failed_to_remove_project_from_favorites": "无法从收藏中移除项目,请重试。",
"project_created_successfully": "项目已成功创建",
"project_created_successfully_description": "项目已成功创建。您现在可以开始添加问题。",
"project_cover_image_alt": "项目封面图片",
"name_is_required": "名称为必填项",
"title_should_be_less_than_255_characters": "标题应少于255个字符",
"project_name": "项目名称",
"project_id_must_be_at_least_1_character": "项目ID至少为1个字符",
"project_id_must_be_at_most_5_characters": "项目ID最多为5个字符",
"project_id": "项目ID",
"project_id_tooltip_content": "用于唯一标识项目中的问题。最多5个字符。",
"description_placeholder": "描述...",
"only_alphanumeric_non_latin_characters_allowed": "仅允许字母数字和非拉丁字符。",
"project_id_is_required": "项目ID为必填项",
"select_network": "选择网络",
"lead": "负责人",
"private": "私有",
"public": "公开",
"accessible_only_by_invite": "仅限邀请访问",
"anyone_in_the_workspace_except_guests_can_join": "除访客外,工作区中的任何人都可以加入",
"creating": "创建中",
"creating_project": "正在创建项目",
"adding_project_to_favorites": "正在将项目添加到收藏",
"project_added_to_favorites": "项目已添加到收藏",
"couldnt_add_the_project_to_favorites": "无法将项目添加到收藏,请重试。",
"removing_project_from_favorites": "正在从收藏中移除项目",
"project_removed_from_favorites": "项目已从收藏中移除",
"couldnt_remove_the_project_from_favorites": "无法从收藏中移除项目,请重试。",
"add_to_favorites": "添加到收藏",
"remove_from_favorites": "从收藏中移除",
"publish_settings": "发布设置",
"publish": "发布",
"copy_link": "复制链接",
"leave_project": "离开项目",
"join_the_project_to_rearrange": "加入项目以重新排序",
"drag_to_rearrange": "拖动以重新排序",
"congrats": "恭喜!",
"project": "项目",
"open_project": "打开项目",
"issues": "问题",
"cycles": "周期",
"modules": "模块",
"pages": "页面",
"intake": "接收",
"time_tracking": "时间跟踪",
"work_management": "工作管理",
"projects_and_issues": "项目与问题",
"projects_and_issues_description": "在此项目上启用或禁用。",
"cycles_description": "按时间段对项目工作进行时间盒管理,并调整频率。",
"modules_description": "将工作分组为类似子项目的设置,拥有自己的负责人和分配人。",
"views_description": "保存排序、过滤和显示选项以供以后使用或共享。",
"pages_description": "写下一些内容。",
"intake_description": "启用此功能以接收您订阅问题的通知。",
"time_tracking_description": "跟踪问题和项目所花费的时间。",
"work_management_description": "轻松管理工作和项目。",
"documentation": "文档",
"message_support": "联系支持",
"contact_sales": "联系销售",
"hyper_mode": "超速模式",
"keyboard_shortcuts": "键盘快捷键",
"whats_new": "新功能",
"version": "版本",
"we_are_having_trouble_fetching_the_updates": "获取更新时出现问题。",
"our_changelogs": "我们的更新日志",
"for_the_latest_updates": "获取最新更新,请访问",
"please_visit": "请访问",
"docs": "文档",
"full_changelog": "完整更新日志",
"support": "支持",
"discord": "Discord",
"powered_by_plane_pages": "由 Plane Pages 提供支持",
"please_select_at_least_one_invitation": "请至少选择一个邀请。",
"please_select_at_least_one_invitation_description": "请至少选择一个邀请以加入工作区。",
"we_see_that_someone_has_invited_you_to_join_a_workspace": "我们发现有人邀请您加入一个工作区",
"join_a_workspace": "加入工作区",
"we_see_that_someone_has_invited_you_to_join_a_workspace_description": "我们发现有人邀请您加入一个工作区",
"join_a_workspace_description": "加入工作区",
"accept_and_join": "接受并加入",
"go_home": "返回首页",
"no_pending_invites": "没有待处理的邀请",
"you_can_see_here_if_someone_invites_you_to_a_workspace": "如果有人邀请您加入工作区,您可以在此处查看",
"back_to_home": "返回首页",
"workspace_name": "工作区名称",
"deactivate_your_account": "停用您的账户",
"deactivate_your_account_description": "停用后,您将无法再分配问题,工作区的计费也将停止。要重新激活账户,您需要使用此电子邮件地址收到工作区邀请。",
"deactivating": "正在停用",
"confirm": "确认",
"draft_created": "草稿已创建",
"issue_created_successfully": "问题已成功创建",
"draft_creation_failed": "草稿创建失败",
"issue_creation_failed": "问题创建失败",
"draft_issue": "草稿问题",
"issue_updated_successfully": "问题已成功更新",
"issue_could_not_be_updated": "无法更新问题",
"create_a_draft": "创建草稿",
"save_to_drafts": "保存为草稿",
"save": "保存",
"updating": "正在更新",
"create_new_issue": "创建新问题",
"editor_is_not_ready_to_discard_changes": "编辑器尚未准备好丢弃更改",
"failed_to_move_issue_to_project": "无法将问题移动到项目",
"create_more": "创建更多",
"add_to_project": "添加到项目",
"discard": "丢弃",
"duplicate_issue_found": "发现重复问题",
"duplicate_issues_found": "发现重复问题",
"no_matching_results": "没有匹配的结果",
"title_is_required": "标题为必填项",
"title": "标题",
"state": "状态",
"priority": "优先级",
"none": "无",
"urgent": "紧急",
"high": "高",
"medium": "中",
"low": "低",
"members": "成员",
"assignee": "分配人",
"assignees": "分配人",
"you": "您",
"labels": "标签",
"create_new_label": "创建新标签",
"start_date": "开始日期",
"due_date": "截止日期",
"cycle": "周期",
"estimate": "估算",
"change_parent_issue": "更改父问题",
"remove_parent_issue": "移除父问题",
"add_parent": "添加父问题",
"loading_members": "正在加载成员...",
"inbox": "收件箱"
}
+202
View File
@@ -0,0 +1,202 @@
import IntlMessageFormat from "intl-messageformat";
import get from "lodash/get";
import { makeAutoObservable } from "mobx";
// constants
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, STORAGE_KEY } from "../constants";
// types
import { TLanguage, ILanguageOption, ITranslations } from "../types";
/**
* Mobx store class for handling translations and language changes in the application
* Provides methods to translate keys with params and change the language
* Uses IntlMessageFormat to format the translations
*/
export class TranslationStore {
// List of translations for each language
private translations: ITranslations = {};
// Cache for IntlMessageFormat instances
private messageCache: Map<string, IntlMessageFormat> = new Map();
// Current language
currentLocale: TLanguage = FALLBACK_LANGUAGE;
/**
* Constructor for the TranslationStore class
*/
constructor() {
makeAutoObservable(this);
this.initializeLanguage();
this.loadTranslations();
}
/**
* Loads translations from JSON files and initializes the message cache
*/
private async loadTranslations() {
try {
// dynamic import of translations
const translations = {
en: (await import("../locales/en/translations.json")).default,
fr: (await import("../locales/fr/translations.json")).default,
es: (await import("../locales/es/translations.json")).default,
ja: (await import("../locales/ja/translations.json")).default,
"zh-CN": (await import("../locales/zh-CN/translations.json")).default,
};
this.translations = translations;
this.messageCache.clear(); // Clear cache when translations change
} catch (error) {
console.error("Failed to load translations:", error);
}
}
/** Initializes the language based on the local storage or browser language */
private initializeLanguage() {
if (typeof window === "undefined") return;
const savedLocale = localStorage.getItem(STORAGE_KEY) as TLanguage;
if (this.isValidLanguage(savedLocale)) {
this.setLanguage(savedLocale);
return;
}
const browserLang = this.getBrowserLanguage();
this.setLanguage(browserLang);
}
/** Checks if the language is valid based on the supported languages */
private isValidLanguage(lang: string | null): lang is TLanguage {
return lang !== null && this.availableLanguages.some((l) => l.value === lang);
}
/** Checks if a language code is similar to any supported language */
private findSimilarLanguage(lang: string): TLanguage | null {
// Convert to lowercase for case-insensitive comparison
const normalizedLang = lang.toLowerCase();
// Find a supported language that includes or is included in the browser language
const similarLang = this.availableLanguages.find(
(l) => normalizedLang.includes(l.value.toLowerCase()) || l.value.toLowerCase().includes(normalizedLang)
);
return similarLang ? similarLang.value : null;
}
/** Gets the browser language based on the navigator.language */
private getBrowserLanguage(): TLanguage {
const browserLang = navigator.language;
// Check exact match first
if (this.isValidLanguage(browserLang)) {
return browserLang;
}
// Check base language without region code
const baseLang = browserLang.split("-")[0];
if (this.isValidLanguage(baseLang)) {
return baseLang as TLanguage;
}
// Try to find a similar language
const similarLang = this.findSimilarLanguage(browserLang) || this.findSimilarLanguage(baseLang);
return similarLang || FALLBACK_LANGUAGE;
}
/**
* Gets the cache key for the given key and locale
* @param key - the key to get the cache key for
* @param locale - the locale to get the cache key for
* @returns the cache key for the given key and locale
*/
private getCacheKey(key: string, locale: TLanguage): string {
return `${locale}:${key}`;
}
/**
* Gets the IntlMessageFormat instance for the given key and locale
* Returns cached instance if available
* Throws an error if the key is not found in the translations
*/
private getMessageInstance(key: string, locale: TLanguage): IntlMessageFormat | null {
const cacheKey = this.getCacheKey(key, locale);
// Check if the cache already has the key
if (this.messageCache.has(cacheKey)) {
return this.messageCache.get(cacheKey) || null;
}
// Get the message from the translations
const message = get(this.translations[locale], key);
if (!message) return null;
try {
const formatter = new IntlMessageFormat(message as any, locale);
this.messageCache.set(cacheKey, formatter);
return formatter;
} catch (error) {
console.error(`Failed to create message formatter for key "${key}":`, error);
return null;
}
}
/**
* Translates a key with params using the current locale
* Falls back to the default language if the translation is not found
* Returns the key itself if the translation is not found
* @param key - The key to translate
* @param params - The params to format the translation with
* @returns The translated string
*/
t(key: string, params?: Record<string, any>): string {
try {
// Try current locale
let formatter = this.getMessageInstance(key, this.currentLocale);
// Fallback to default language if necessary
if (!formatter && this.currentLocale !== FALLBACK_LANGUAGE) {
formatter = this.getMessageInstance(key, FALLBACK_LANGUAGE);
}
// If we have a formatter, use it
if (formatter) {
return formatter.format(params || {}) as string;
}
// Last resort: return the key itself
return key;
} catch (error) {
console.error(`Translation error for key "${key}":`, error);
return key;
}
}
/**
* Sets the current language and updates the translations
* @param lng - The new language
*/
setLanguage(lng: TLanguage): void {
try {
if (!this.isValidLanguage(lng)) {
throw new Error(`Invalid language: ${lng}`);
}
if (typeof window !== "undefined") {
localStorage.setItem(STORAGE_KEY, lng);
}
this.currentLocale = lng;
this.messageCache.clear(); // Clear cache when language changes
if (typeof window !== "undefined") {
document.documentElement.lang = lng;
}
} catch (error) {
console.error("Failed to set language:", error);
}
}
/**
* Gets the available language options for the dropdown
* @returns An array of language options
*/
get availableLanguages(): ILanguageOption[] {
return SUPPORTED_LANGUAGES;
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from "./language";
export * from "./translation";
+6
View File
@@ -0,0 +1,6 @@
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN";
export interface ILanguageOption {
label: string;
value: TLanguage;
}
+7
View File
@@ -0,0 +1,7 @@
export interface ITranslation {
[key: string]: string | ITranslation;
}
export interface ITranslations {
[locale: string]: ITranslation;
}
+3
View File
@@ -0,0 +1,3 @@
build/*
dist/*
out/*
+6
View File
@@ -0,0 +1,6 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
};
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@plane/propel",
"version": "0.24.1",
"private": true,
"exports": {
"./globals.css": "./src/globals.css",
"./components/*": "./src/*.tsx"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.0",
"typescript": "^5.3.3"
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.469.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+59
View File
@@ -0,0 +1,59 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 47.4% 11.2%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--card: 0 0% 100%;
--card-foreground: 222.2 47.4% 11.2%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 100% 50%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--radius: 0.5rem;
}
.dark {
--background: 224 71% 4%;
--foreground: 213 31% 91%;
--muted: 223 47% 11%;
--muted-foreground: 215.4 16.3% 56.9%;
--accent: 216 34% 17%;
--accent-foreground: 210 40% 98%;
--popover: 224 71% 4%;
--popover-foreground: 215 20.2% 65.1%;
--border: 216 34% 17%;
--input: 216 34% 17%;
--card: 224 71% 4%;
--card-foreground: 213 31% 91%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 1.2%;
--secondary: 222.2 47.4% 11.2%;
--secondary-foreground: 210 40% 98%;
--destructive: 0 63% 31%;
--destructive-foreground: 210 40% 98%;
--ring: 216 34% 17%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply font-sans antialiased bg-background text-foreground;
}
}
+57
View File
@@ -0,0 +1,57 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@plane/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };
+7
View File
@@ -0,0 +1,7 @@
import type { Config } from "tailwindcss";
import sharedConfig from "@plane/tailwind-config/tailwind.config";
export default {
...sharedConfig,
content: ["./src/**/*.{ts,tsx}"],
} satisfies Config;
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@plane/typescript-config/react-library.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
+2 -1
View File
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { IndexedDBService } from "./indexedDB.service";
/**
* Abstract base class for making HTTP requests using axios
@@ -34,7 +35,7 @@ export abstract class APIService {
(error) => {
if (error.response && error.response.status === 401) {
const currentPath = window.location.pathname;
let prefix = "";
let prefix = "/";
let updatedPath = currentPath;
// Check for special path prefixes
@@ -0,0 +1,62 @@
export abstract class IndexedDBService {
private dbName: string;
private version: number;
private db: IDBDatabase | null = null;
constructor(dbName: string, version: number) {
this.dbName = dbName;
this.version = version;
}
async init(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains("workspaces")) {
db.createObjectStore("workspaces", { keyPath: "id" });
}
};
});
}
async save(workspaces: any[]): Promise<void> {
if (!this.db) throw new Error("Database not initialized");
const transaction = this.db.transaction("workspaces", "readwrite");
const store = transaction.objectStore("workspaces");
return new Promise((resolve, reject) => {
// Clear existing data
store.clear();
// Add new workspaces
workspaces.forEach((workspace) => {
store.add(workspace);
});
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
}
async query(): Promise<any[]> {
if (!this.db) throw new Error("Database not initialized");
const transaction = this.db.transaction("workspaces", "readonly");
const store = transaction.objectStore("workspaces");
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
+3
View File
@@ -0,0 +1,3 @@
build/*
dist/*
out/*
+9
View File
@@ -0,0 +1,9 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
+5
View File
@@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@plane/shared-state",
"version": "0.24.1",
"description": "Shared state shared across multiple apps internally",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"lint": "eslint src --ext .ts,.tsx",
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"zod": "^3.22.2"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@types/node": "^22.5.4",
"typescript": "^5.3.3"
}
}

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