Compare commits

...
Author SHA1 Message Date
Aaryan Khandelwal 79cb0d3860 fix: col drag handle orientation 2026-01-22 17:07:51 +05:30
Aaryan Khandelwal e7c84e86a9 fix: drag handle button UI 2026-01-22 15:49:48 +05:30
Aaryan Khandelwal ad9be51eb4 chore: remove backdrop event listeners 2026-01-22 14:42:40 +05:30
Aaryan Khandelwal c74377fdfb chore: update comment 2026-01-22 14:32:11 +05:30
Aaryan Khandelwal d0fe1ba284 refactor: remove unused files 2026-01-22 14:24:25 +05:30
Aaryan Khandelwal 60d37dde2f fix: insert handle performance 2026-01-22 14:22:53 +05:30
Aaryan Khandelwal c9a23d5b21 refactor: migrate table drag handles from react to vanilla js 2026-01-22 14:06:18 +05:30
Aaryan KhandelwalandGitHub f7debcde79 [WEB-5898] chore: update tailwind config #8516 2026-01-19 20:42:44 +05:30
Aaryan KhandelwalandGitHub 3de76206b5 [WIKI-735] fix: table insert handle z-index #8545 2026-01-19 20:41:50 +05:30
Vamsi KrishnaandGitHub f783447796 [WEB-5907] fix: magic code sign-in at Space app. #8552 2026-01-19 20:41:00 +05:30
SangeethaandGitHub fa1b4a102a [WEB-5890] migration: added getting_started_checklist, tips, explored_feature fields on the workspace member table (#8489)
* migration: added getting_started_checklist and tips field

* fix: remove defaults and added explored_features field

* fix: added user table migration
2026-01-15 14:25:31 +05:30
sriramveeraghanta 8399f64bee chore(deps): react router upgraded 2026-01-09 14:43:36 +05:30
Bavisetti NarayanandGitHub 5f3f9d2623 chore: updated migration file name (#8515) 2026-01-09 14:36:45 +05:30
Prateek ShouryaandGitHub 110dbd9acd [VPAT-50] chore(security): add X-Frame-Options header to nginx configuration to prevent clickjacking attacks (#8507)
* [VPAT-50] chore(security): add X-Frame-Options header to nginx configuration to prevent clickjacking attacks

* [SECURITY] chore: enhance nginx configuration with additional security headers
2026-01-09 04:03:41 +05:30
Vipin ChaudharyandGitHub b83d460938 [WIKI-826] chore: add unique id as key to logo selector (#8494) 2026-01-07 15:05:14 +05:30
ea1f92e0c6 [WEB-5537]refactor: rename IssueUserProperty to ProjectUserProperty and update related references (#8206)
* refactor: rename IssueUserProperty to ProjectUserProperty and update related references across the codebase

* migrate: move issue user properties to project user properties and update related fields and constraints

* refactor: rename IssueUserPropertySerializer and IssueUserDisplayPropertyEndpoint to ProjectUserPropertySerializer and ProjectUserDisplayPropertyEndpoint, updating all related references

* fix: enhance ProjectUserDisplayPropertyEndpoint to handle missing properties by creating new entries and improve response handling

* fix: correct formatting in migration for ProjectUserProperty model options

* migrate: add migration to update existing non-service API tokens to remove workspace association

* migrate: refine migration to update existing non-service API tokens by excluding bot users from workspace removal

* chore: changed the project sort order in project user property

* chore: remove allowed_rate_limit from APIToken

* chore: updated user-properties endpoint for frontend

* chore: removed the extra projectuserproperty

* chore: updated the migration file

* chore: code refactor

* fix: type error

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
Co-authored-by: vamsikrishnamathala <matalav55@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2026-01-06 15:37:19 +05:30
Prateek ShouryaandGitHub 3d5e427894 chore: remove unused right sidebar component and clean up workspace member settings (#8477) 2025-12-31 00:08:40 +05:30
sriram veeraghantaandGitHub 6129e8e53b Revert "[WEB-4959]chore: refactor project member page #8464" (#8476)
This reverts commit c97e418515.
2025-12-30 21:46:32 +05:30
sriram veeraghantaandGitHub d61b157929 chore: remove posthog events (#8465)
* chore: remove posthog events

* chore: remove event tracking

* chore: lint errors

* chore: minor changes based on comments

* fix: type errors
2025-12-30 19:59:50 +05:30
NikhilandGitHub a37e5e2f6a [WEB-5598] refactor: streamline object creation in workspace seed task and improve error handling in workspace creation #8264 2025-12-30 17:28:33 +05:30
lifandGitHub a976bea5af fix: correct spelling error in database.ts log message (#8452)
Fix "convertion" -> "conversion" in error log message.

Signed-off-by: majiayu000 <1835304752@qq.com>
2025-12-30 17:23:42 +05:30
1072509642 migration: added webhook version, navigation related fields and allowed_rate_limit for APIToken (#8339)
* migration: added version field in webhook

* chore: add max_length

* chore: added product tour fields

* chore: updated the migration file

* chore: removed the duplicated migration file

* chore: added allowed_rate_limit for api_tokens

* chore: changed key feature tour to product tour

* chore: added is_subscribed_to_changelog field

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2025-12-30 16:18:58 +05:30
Jayash TripathyandGitHub dcd8d27eae [WEB-5472] refactor: project form #8472 2025-12-30 15:57:33 +05:30
b-saikrishnakanthandGitHub c97e418515 [WEB-4959]chore: refactor project member page #8464 2025-12-30 14:59:16 +05:30
Aaryan KhandelwalandGitHub 031baaa162 [WEB-857] regression: image uploader error state #8471 2025-12-30 14:35:52 +05:30
Jayash TripathyandGitHub 9141a9377f [WEB-5472] refactor: components of project creation flow (#8462) 2025-12-30 14:32:48 +05:30
Anmol Singh BhatiaandGitHub 866338289e chore: navigation preference enhancements (#8468) 2025-12-30 13:22:28 +05:30
Prateek ShouryaandGitHub 8d479ac24c [WEB-5822] fix: migrate ImagePickerPopover to Propel Tabs component and render only enabled tabs #8290
- Replace custom tab implementation with Propel Tabs
- Dynamically render only enabled tabs based on configuration
- Filter tabs by isEnabled property for cleaner conditional rendering
- Improve tab navigation and accessibility with Propel components
2025-12-29 19:54:32 +05:30
lifandGitHub 94d5779f3a chore: remove unused get_client_ip import (#8453)
Remove unused import `get_client_ip` from workspace/invite.py.
Identified by ruff linter (F401 error).

Signed-off-by: majiayu000 <1835304752@qq.com>
2025-12-29 18:39:54 +05:30
b-saikrishnakanthandGitHub 20492ffa75 [WEB-5790] feat: new email templates (#8423) 2025-12-29 16:17:29 +05:30
232 changed files with 4087 additions and 6931 deletions
+6
View File
@@ -20,6 +20,12 @@ http {
server {
listen 3000;
# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
-1
View File
@@ -45,7 +45,6 @@
"react-dom": "catalog:",
"react-hook-form": "7.51.5",
"react-router": "catalog:",
"react-router-dom": "catalog:",
"serve": "14.2.5",
"swr": "catalog:",
"uuid": "catalog:"
+1 -8
View File
@@ -18,7 +18,7 @@ from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
from plane.db.models import (
Cycle,
Intake,
IssueUserProperty,
ProjectUserProperty,
Module,
Project,
DeployBoard,
@@ -218,8 +218,6 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
# Add the user as Administrator to the project
_ = ProjectMember.objects.create(project_id=serializer.instance.id, member=request.user, role=20)
# Also create the issue property for the user
_ = IssueUserProperty.objects.create(project_id=serializer.instance.id, user=request.user)
if serializer.instance.project_lead is not None and str(serializer.instance.project_lead) != str(
request.user.id
@@ -229,11 +227,6 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
member_id=serializer.instance.project_lead,
role=20,
)
# Also create the issue property for the user
IssueUserProperty.objects.create(
project_id=serializer.instance.id,
user_id=serializer.instance.project_lead,
)
State.objects.bulk_create(
[
+1 -1
View File
@@ -52,7 +52,7 @@ from .issue import (
IssueCreateSerializer,
IssueActivitySerializer,
IssueCommentSerializer,
IssueUserPropertySerializer,
ProjectUserPropertySerializer,
IssueAssigneeSerializer,
LabelSerializer,
IssueSerializer,
+3 -3
View File
@@ -18,7 +18,7 @@ from plane.db.models import (
Issue,
IssueActivity,
IssueComment,
IssueUserProperty,
ProjectUserProperty,
IssueAssignee,
IssueSubscriber,
IssueLabel,
@@ -346,9 +346,9 @@ class IssueActivitySerializer(BaseSerializer):
fields = "__all__"
class IssueUserPropertySerializer(BaseSerializer):
class ProjectUserPropertySerializer(BaseSerializer):
class Meta:
model = IssueUserProperty
model = ProjectUserProperty
fields = "__all__"
read_only_fields = ["user", "workspace", "project"]
+4 -4
View File
@@ -14,7 +14,7 @@ from plane.app.views import (
IssueReactionViewSet,
IssueRelationViewSet,
IssueSubscriberViewSet,
IssueUserDisplayPropertyEndpoint,
ProjectUserDisplayPropertyEndpoint,
IssueViewSet,
LabelViewSet,
BulkArchiveIssuesEndpoint,
@@ -208,13 +208,13 @@ urlpatterns = [
name="project-issue-comment-reactions",
),
## End Comment Reactions
## IssueUserProperty
## ProjectUserProperty
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
IssueUserDisplayPropertyEndpoint.as_view(),
ProjectUserDisplayPropertyEndpoint.as_view(),
name="project-issue-display-properties",
),
## IssueUserProperty End
## ProjectUserProperty End
## Issue Archives
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
+1 -1
View File
@@ -114,7 +114,7 @@ from .asset.v2 import (
from .issue.base import (
IssueListEndpoint,
IssueViewSet,
IssueUserDisplayPropertyEndpoint,
ProjectUserDisplayPropertyEndpoint,
BulkDeleteIssuesEndpoint,
DeletedIssuesListViewSet,
IssuePaginatedViewSet,
+23 -13
View File
@@ -34,7 +34,7 @@ from plane.app.serializers import (
IssueDetailSerializer,
IssueListDetailSerializer,
IssueSerializer,
IssueUserPropertySerializer,
ProjectUserPropertySerializer,
)
from plane.bgtasks.issue_activities_task import issue_activity
from plane.bgtasks.issue_description_version_task import issue_description_version_task
@@ -51,7 +51,7 @@ from plane.db.models import (
IssueReaction,
IssueRelation,
IssueSubscriber,
IssueUserProperty,
ProjectUserProperty,
ModuleIssue,
Project,
ProjectMember,
@@ -723,23 +723,33 @@ class IssueViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
class ProjectUserDisplayPropertyEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def patch(self, request, slug, project_id):
issue_property = IssueUserProperty.objects.get(user=request.user, project_id=project_id)
try:
issue_property = ProjectUserProperty.objects.get(
user=request.user,
project_id=project_id
)
except ProjectUserProperty.DoesNotExist:
issue_property = ProjectUserProperty.objects.create(
user=request.user,
project_id=project_id
)
issue_property.rich_filters = request.data.get("rich_filters", issue_property.rich_filters)
issue_property.filters = request.data.get("filters", issue_property.filters)
issue_property.display_filters = request.data.get("display_filters", issue_property.display_filters)
issue_property.display_properties = request.data.get("display_properties", issue_property.display_properties)
issue_property.save()
serializer = IssueUserPropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_201_CREATED)
serializer = ProjectUserPropertySerializer(
issue_property,
data=request.data,
partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id):
issue_property, _ = IssueUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
serializer = IssueUserPropertySerializer(issue_property)
issue_property, _ = ProjectUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
serializer = ProjectUserPropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_200_OK)
+2 -8
View File
@@ -24,14 +24,15 @@ from plane.bgtasks.webhook_task import model_activity, webhook_activity
from plane.db.models import (
UserFavorite,
DeployBoard,
ProjectUserProperty,
Intake,
IssueUserProperty,
Project,
ProjectIdentifier,
ProjectMember,
ProjectNetwork,
State,
DEFAULT_STATES,
UserFavorite,
Workspace,
WorkspaceMember,
)
@@ -250,8 +251,6 @@ class ProjectViewSet(BaseViewSet):
member=request.user,
role=ROLE.ADMIN.value,
)
# Also create the issue property for the user
_ = IssueUserProperty.objects.create(project_id=serializer.data["id"], user=request.user)
if serializer.data["project_lead"] is not None and str(serializer.data["project_lead"]) != str(
request.user.id
@@ -261,11 +260,6 @@ class ProjectViewSet(BaseViewSet):
member_id=serializer.data["project_lead"],
role=ROLE.ADMIN.value,
)
# Also create the issue property for the user
IssueUserProperty.objects.create(
project_id=serializer.data["id"],
user_id=serializer.data["project_lead"],
)
State.objects.bulk_create(
[
+4 -4
View File
@@ -24,7 +24,7 @@ from plane.db.models import (
User,
WorkspaceMember,
Project,
IssueUserProperty,
ProjectUserProperty,
)
from plane.db.models.project import ProjectNetwork
from plane.utils.host import base_host
@@ -160,9 +160,9 @@ class UserProjectInvitationsViewset(BaseViewSet):
ignore_conflicts=True,
)
IssueUserProperty.objects.bulk_create(
ProjectUserProperty.objects.bulk_create(
[
IssueUserProperty(
ProjectUserProperty(
project_id=project_id,
user=request.user,
workspace=workspace,
@@ -220,7 +220,7 @@ class ProjectJoinEndpoint(BaseAPIView):
if project_member is None:
# Create a Project Member
_ = ProjectMember.objects.create(
workspace_id=project_invite.workspace_id,
project_id=project_id,
member=user,
role=project_invite.role,
)
+16 -16
View File
@@ -1,6 +1,7 @@
# Third Party imports
from rest_framework.response import Response
from rest_framework import status
from django.db.models import Min
# Module imports
from .base import BaseViewSet, BaseAPIView
@@ -13,7 +14,7 @@ from plane.app.serializers import (
from plane.app.permissions import WorkspaceUserPermission
from plane.db.models import Project, ProjectMember, IssueUserProperty, WorkspaceMember
from plane.db.models import Project, ProjectMember, ProjectUserProperty, WorkspaceMember
from plane.bgtasks.project_add_user_email_task import project_add_user_email
from plane.utils.host import base_host
from plane.app.permissions.base import allow_permission, ROLE
@@ -89,24 +90,23 @@ class ProjectMemberViewSet(BaseViewSet):
# Update the roles of the existing members
ProjectMember.objects.bulk_update(bulk_project_members, ["is_active", "role"], batch_size=100)
# Get the list of project members of the requested workspace with the given slug
project_members = (
ProjectMember.objects.filter(
# Get the minimum sort_order for each member in the workspace
member_sort_orders = (
ProjectUserProperty.objects.filter(
workspace__slug=slug,
member_id__in=[member.get("member_id") for member in members],
user_id__in=[member.get("member_id") for member in members],
)
.values("member_id", "sort_order")
.order_by("sort_order")
.values("user_id")
.annotate(min_sort_order=Min("sort_order"))
)
# Convert to dictionary for easy lookup: {user_id: min_sort_order}
sort_order_map = {str(item["user_id"]): item["min_sort_order"] for item in member_sort_orders}
# Loop through requested members
for member in members:
# Get the sort orders of the member
sort_order = [
project_member.get("sort_order")
for project_member in project_members
if str(project_member.get("member_id")) == str(member.get("member_id"))
]
member_id = str(member.get("member_id"))
# Get the minimum sort_order for this member, or use default
min_sort_order = sort_order_map.get(member_id)
# Create a new project member
bulk_project_members.append(
ProjectMember(
@@ -114,22 +114,22 @@ class ProjectMemberViewSet(BaseViewSet):
role=member.get("role", 5),
project_id=project_id,
workspace_id=project.workspace_id,
sort_order=(sort_order[0] - 10000 if len(sort_order) else 65535),
)
)
# Create a new issue property
bulk_issue_props.append(
IssueUserProperty(
ProjectUserProperty(
user_id=member.get("member_id"),
project_id=project_id,
workspace_id=project.workspace_id,
sort_order=(min_sort_order - 10000 if min_sort_order is not None else 65535),
)
)
# Bulk create the project members and issue properties
project_members = ProjectMember.objects.bulk_create(bulk_project_members, batch_size=10, ignore_conflicts=True)
_ = IssueUserProperty.objects.bulk_create(bulk_issue_props, batch_size=10, ignore_conflicts=True)
_ = ProjectUserProperty.objects.bulk_create(bulk_issue_props, batch_size=10, ignore_conflicts=True)
project_members = ProjectMember.objects.filter(
project_id=project_id,
+6 -8
View File
@@ -77,14 +77,12 @@ class WorkSpaceViewSet(BaseViewSet):
def create(self, request):
try:
(DISABLE_WORKSPACE_CREATION,) = get_configuration_value(
[
{
"key": "DISABLE_WORKSPACE_CREATION",
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
}
]
)
(DISABLE_WORKSPACE_CREATION,) = get_configuration_value([
{
"key": "DISABLE_WORKSPACE_CREATION",
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
}
])
if DISABLE_WORKSPACE_CREATION == "1":
return Response(
@@ -26,7 +26,6 @@ from plane.bgtasks.workspace_invitation_task import workspace_invitation
from plane.db.models import User, Workspace, WorkspaceMember, WorkspaceMemberInvite
from plane.utils.cache import invalidate_cache, invalidate_cache_directly
from plane.utils.host import base_host
from plane.utils.ip_address import get_client_ip
from plane.utils.analytics_events import USER_JOINED_WORKSPACE, USER_INVITED_TO_WORKSPACE
from .. import BaseViewSet
+31 -14
View File
@@ -21,7 +21,7 @@ from plane.db.models import (
WorkspaceMember,
Project,
ProjectMember,
IssueUserProperty,
ProjectUserProperty,
State,
Label,
Issue,
@@ -94,7 +94,7 @@ def create_project_and_member(workspace: Workspace, bot_user: User) -> Dict[int,
project_seed.pop("name", None)
project_seed.pop("identifier", None)
project = Project.objects.create(
project = Project(
**project_seed,
workspace=workspace,
name=workspace.name, # Use workspace name
@@ -105,6 +105,7 @@ def create_project_and_member(workspace: Workspace, bot_user: User) -> Dict[int,
module_view=True,
issue_views_view=True,
)
project.save(created_by_id=bot_user.id, disable_auto_set_user=True)
# Create project members
ProjectMember.objects.bulk_create(
@@ -121,9 +122,9 @@ def create_project_and_member(workspace: Workspace, bot_user: User) -> Dict[int,
)
# Create issue user properties
IssueUserProperty.objects.bulk_create(
ProjectUserProperty.objects.bulk_create(
[
IssueUserProperty(
ProjectUserProperty(
project=project,
user_id=workspace_member["member_id"],
workspace_id=workspace.id,
@@ -191,13 +192,13 @@ def create_project_states(
state_id = state_seed.pop("id")
project_id = state_seed.pop("project_id")
state = State.objects.create(
state = State(
**state_seed,
project_id=project_map[project_id],
workspace=workspace,
created_by_id=bot_user.id,
)
state.save(created_by_id=bot_user.id, disable_auto_set_user=True)
state_map[state_id] = state.id
logger.info(f"Task: workspace_seed_task -> State {state_id} created")
return state_map
@@ -224,12 +225,13 @@ def create_project_labels(
for label_seed in label_seeds:
label_id = label_seed.pop("id")
project_id = label_seed.pop("project_id")
label = Label.objects.create(
label = Label(
**label_seed,
project_id=project_map[project_id],
workspace=workspace,
created_by_id=bot_user.id,
)
label.save(created_by_id=bot_user.id, disable_auto_set_user=True)
label_map[label_id] = label.id
logger.info(f"Task: workspace_seed_task -> Label {label_id} created")
@@ -276,13 +278,14 @@ def create_project_issues(
cycle_id = issue_seed.pop("cycle_id")
module_ids = issue_seed.pop("module_ids")
issue = Issue.objects.create(
issue = Issue(
**issue_seed,
state_id=states_map[state_id],
project_id=project_map[project_id],
workspace=workspace,
created_by_id=bot_user.id,
)
issue.save(created_by_id=bot_user.id, disable_auto_set_user=True)
IssueSequence.objects.create(
issue=issue,
project_id=project_map[project_id],
@@ -351,7 +354,7 @@ def create_pages(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_us
for page_seed in page_seeds:
page_id = page_seed.pop("id")
page = Page.objects.create(
page = Page(
workspace_id=workspace.id,
is_global=False,
access=page_seed.get("access", Page.PUBLIC_ACCESS),
@@ -365,16 +368,18 @@ def create_pages(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_us
owned_by_id=bot_user.id,
)
page.save(created_by_id=bot_user.id, disable_auto_set_user=True)
logger.info(f"Task: workspace_seed_task -> Page {page_id} created")
if page_seed.get("project_id") and page_seed.get("type") == "PROJECT":
ProjectPage.objects.create(
project_page = ProjectPage(
workspace_id=workspace.id,
project_id=project_map[page_seed.get("project_id")],
page_id=page.id,
created_by_id=bot_user.id,
updated_by_id=bot_user.id,
)
project_page.save(created_by_id=bot_user.id, disable_auto_set_user=True)
logger.info(f"Task: workspace_seed_task -> Project Page {page_id} created")
return
@@ -414,7 +419,7 @@ def create_cycles(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_u
start_date = timezone.now() + timedelta(days=14)
end_date = start_date + timedelta(days=14)
cycle = Cycle.objects.create(
cycle = Cycle(
**cycle_seed,
start_date=start_date,
end_date=end_date,
@@ -423,6 +428,7 @@ def create_cycles(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_u
created_by_id=bot_user.id,
owned_by_id=bot_user.id,
)
cycle.save(created_by_id=bot_user.id, disable_auto_set_user=True)
cycle_map[cycle_id] = cycle.id
logger.info(f"Task: workspace_seed_task -> Cycle {cycle_id} created")
@@ -450,7 +456,7 @@ def create_modules(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_
start_date = timezone.now() + timedelta(days=index * 2)
end_date = start_date + timedelta(days=14)
module = Module.objects.create(
module = Module(
**module_seed,
start_date=start_date,
target_date=end_date,
@@ -458,6 +464,7 @@ def create_modules(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_
workspace=workspace,
created_by_id=bot_user.id,
)
module.save(created_by_id=bot_user.id, disable_auto_set_user=True)
module_map[module_id] = module.id
logger.info(f"Task: workspace_seed_task -> Module {module_id} created")
return module_map
@@ -478,13 +485,15 @@ def create_views(workspace: Workspace, project_map: Dict[int, uuid.UUID], bot_us
for view_seed in view_seeds:
project_id = view_seed.pop("project_id")
IssueView.objects.create(
view_seed.pop("id")
issue_view = IssueView(
**view_seed,
project_id=project_map[project_id],
workspace=workspace,
created_by_id=bot_user.id,
owned_by_id=bot_user.id,
)
issue_view.save(created_by_id=bot_user.id, disable_auto_set_user=True)
@shared_task
@@ -518,6 +527,14 @@ def workspace_seed(workspace_id: uuid.UUID) -> None:
is_password_autoset=True,
)
# Add bot user to workspace as member
WorkspaceMember.objects.create(
workspace=workspace,
member=bot_user,
role=20,
company_role="",
)
# Create a project with the same name as workspace
project_map = create_project_and_member(workspace, bot_user)
@@ -8,7 +8,7 @@ from plane.db.models import (
WorkspaceMember,
ProjectMember,
Project,
IssueUserProperty,
ProjectUserProperty,
)
@@ -47,27 +47,18 @@ class Command(BaseCommand):
if not WorkspaceMember.objects.filter(workspace=project.workspace, member=user, is_active=True).exists():
raise CommandError("User not member in workspace")
# Get the smallest sort order
smallest_sort_order = (
ProjectMember.objects.filter(workspace_id=project.workspace_id).order_by("sort_order").first()
)
if smallest_sort_order:
sort_order = smallest_sort_order.sort_order - 1000
else:
sort_order = 65535
if ProjectMember.objects.filter(project=project, member=user).exists():
# Update the project member
ProjectMember.objects.filter(project=project, member=user).update(
is_active=True, sort_order=sort_order, role=role
is_active=True, role=role
)
else:
# Create the project member
ProjectMember.objects.create(project=project, member=user, role=role, sort_order=sort_order)
ProjectMember.objects.create(project=project, member=user, role=role)
# Issue Property
IssueUserProperty.objects.get_or_create(user=user, project=project)
ProjectUserProperty.objects.get_or_create(user=user, project=project)
# Success message
self.stdout.write(self.style.SUCCESS(f"User {user_email} added to project {project_id}"))
@@ -0,0 +1,57 @@
# Generated by Django 4.2.26 on 2025-12-15 10:29
from django.db import migrations, models
import plane.db.models.workspace
def get_default_product_tour():
return {
"work_items": True,
"cycles": True,
"modules": True,
"intake": True,
"pages": True,
}
def populate_product_tour(apps, _schema_editor):
WorkspaceUserProperties = apps.get_model('db', 'WorkspaceUserProperties')
default_value = get_default_product_tour()
# Use bulk update for better performance
WorkspaceUserProperties.objects.all().update(product_tour=default_value)
class Migration(migrations.Migration):
dependencies = [
('db', '0112_auto_20251124_0603'),
]
operations = [
migrations.AddField(
model_name='webhook',
name='version',
field=models.CharField(default='v1', max_length=50),
),
migrations.AddField(
model_name='profile',
name='is_navigation_tour_completed',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='workspaceuserproperties',
name='product_tour',
field=models.JSONField(default=plane.db.models.workspace.get_default_product_tour),
),
migrations.AddField(
model_name='apitoken',
name='allowed_rate_limit',
field=models.CharField(default='60/min', max_length=255),
),
migrations.AddField(
model_name='profile',
name='is_subscribed_to_changelog',
field=models.BooleanField(default=False),
),
migrations.RunPython(populate_product_tour, reverse_code=migrations.RunPython.noop),
]
@@ -0,0 +1,50 @@
# Generated by Django 4.2.22 on 2026-01-05 08:35
from django.db import migrations, models
import plane.db.models.project
import django.db.models.deletion
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('db', '0113_webhook_version'),
]
operations = [
migrations.AlterModelTable(
name='issueuserproperty',
table='project_user_properties',
),
migrations.RenameModel(
old_name='IssueUserProperty',
new_name='ProjectUserProperty',
),
migrations.AddField(
model_name='projectuserproperty',
name='preferences',
field=models.JSONField(default=plane.db.models.project.get_default_preferences),
),
migrations.AddField(
model_name='projectuserproperty',
name='sort_order',
field=models.FloatField(default=65535),
),
migrations.AlterModelOptions(
name='projectuserproperty',
options={'ordering': ('-created_at',), 'verbose_name': 'Project User Property', 'verbose_name_plural': 'Project User Properties'},
),
migrations.RemoveConstraint(
model_name='projectuserproperty',
name='issue_user_property_unique_user_project_when_deleted_at_null',
),
migrations.AlterField(
model_name='projectuserproperty',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_property_user', to=settings.AUTH_USER_MODEL),
),
migrations.AddConstraint(
model_name='projectuserproperty',
constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('user', 'project'), name='project_user_property_unique_user_project_when_deleted_at_null'),
),
]
@@ -0,0 +1,51 @@
# Generated by Django 4.2.22 on 2026-01-05 08:36
from django.db import migrations
def move_issue_user_properties_to_project_user_properties(apps, schema_editor):
ProjectMember = apps.get_model('db', 'ProjectMember')
ProjectUserProperty = apps.get_model('db', 'ProjectUserProperty')
# Get all project members
project_members = ProjectMember.objects.filter(deleted_at__isnull=True).values('member_id', 'project_id', 'preferences', 'sort_order')
# create a mapping with consistent ordering
pm_dict = {
(pm['member_id'], pm['project_id']): pm
for pm in project_members
}
# Get all project user properties
properties_to_update = []
for projectuserproperty in ProjectUserProperty.objects.filter(deleted_at__isnull=True):
pm = pm_dict.get((projectuserproperty.user_id, projectuserproperty.project_id))
if pm:
projectuserproperty.preferences = pm['preferences']
projectuserproperty.sort_order = pm['sort_order']
properties_to_update.append(projectuserproperty)
ProjectUserProperty.objects.bulk_update(properties_to_update, ['preferences', 'sort_order'], batch_size=2000)
def migrate_existing_api_tokens(apps, schema_editor):
APIToken = apps.get_model('db', 'APIToken')
# Update all the existing non-service api tokens to not have a workspace
APIToken.objects.filter(is_service=False, user__is_bot=False).update(
workspace_id=None,
)
return
class Migration(migrations.Migration):
dependencies = [
('db', '0114_projectuserproperty_delete_issueuserproperty_and_more'),
]
operations = [
migrations.RunPython(move_issue_user_properties_to_project_user_properties, reverse_code=migrations.RunPython.noop),
migrations.RunPython(migrate_existing_api_tokens, reverse_code=migrations.RunPython.noop),
]
@@ -0,0 +1,38 @@
# Generated by Django 4.2.27 on 2026-01-13 10:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0115_auto_20260105_1406'),
]
operations = [
migrations.AddField(
model_name='profile',
name='notification_view_mode',
field=models.CharField(choices=[('full', 'Full'), ('compact', 'Compact')], default='full', max_length=255),
),
migrations.AddField(
model_name='user',
name='is_password_reset_required',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='workspacemember',
name='explored_features',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='workspacemember',
name='getting_started_checklist',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='workspacemember',
name='tips',
field=models.JSONField(default=dict),
),
]
+1 -1
View File
@@ -34,7 +34,6 @@ from .issue import (
IssueLabel,
IssueLink,
IssueMention,
IssueUserProperty,
IssueReaction,
IssueRelation,
IssueSequence,
@@ -54,6 +53,7 @@ from .project import (
ProjectMemberInvite,
ProjectNetwork,
ProjectPublicMember,
ProjectUserProperty,
)
from .session import Session
from .social_connection import SocialLoginConnection
+1
View File
@@ -32,6 +32,7 @@ class APIToken(BaseModel):
workspace = models.ForeignKey("db.Workspace", related_name="api_tokens", on_delete=models.CASCADE, null=True)
expired_at = models.DateTimeField(blank=True, null=True)
is_service = models.BooleanField(default=False)
allowed_rate_limit = models.CharField(max_length=255, default="60/min")
class Meta:
verbose_name = "API Token"
-30
View File
@@ -526,36 +526,6 @@ class IssueComment(ChangeTrackerMixin, ProjectBaseModel):
return str(self.issue)
class IssueUserProperty(ProjectBaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="issue_property_user",
)
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
rich_filters = models.JSONField(default=dict)
class Meta:
verbose_name = "Issue User Property"
verbose_name_plural = "Issue User Properties"
db_table = "issue_user_properties"
ordering = ("-created_at",)
unique_together = ["user", "project", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["user", "project"],
condition=Q(deleted_at__isnull=True),
name="issue_user_property_unique_user_project_when_deleted_at_null",
)
]
def __str__(self):
"""Return properties status of the issue"""
return str(self.user)
class IssueLabel(ProjectBaseModel):
issue = models.ForeignKey("db.Issue", on_delete=models.CASCADE, related_name="label_issue")
label = models.ForeignKey("db.Label", on_delete=models.CASCADE, related_name="label_issue")
+47 -8
View File
@@ -12,7 +12,6 @@ from django.db.models import Q
# Module imports
from plane.db.mixins import AuditModel
# Module imports
from .base import BaseModel
ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest"))
@@ -219,14 +218,20 @@ class ProjectMember(ProjectBaseModel):
is_active = models.BooleanField(default=True)
def save(self, *args, **kwargs):
if self._state.adding:
smallest_sort_order = ProjectMember.objects.filter(
workspace_id=self.project.workspace_id, member=self.member
).aggregate(smallest=models.Min("sort_order"))["smallest"]
if self._state.adding and self.member:
# Get the minimum sort_order for this member in the workspace
min_sort_order_result = ProjectUserProperty.objects.filter(
workspace_id=self.project.workspace_id, user=self.member
).aggregate(min_sort_order=models.Min("sort_order"))
min_sort_order = min_sort_order_result.get("min_sort_order")
# Project ordering
if smallest_sort_order is not None:
self.sort_order = smallest_sort_order - 10000
# create project user property with project sort order
ProjectUserProperty.objects.create(
workspace_id=self.project.workspace_id,
project=self.project,
user=self.member,
sort_order=(min_sort_order - 10000 if min_sort_order is not None else 65535),
)
super(ProjectMember, self).save(*args, **kwargs)
@@ -326,3 +331,37 @@ class ProjectPublicMember(ProjectBaseModel):
verbose_name_plural = "Project Public Members"
db_table = "project_public_members"
ordering = ("-created_at",)
class ProjectUserProperty(ProjectBaseModel):
from .issue import get_default_filters, get_default_display_filters, get_default_display_properties
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="project_property_user",
)
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
rich_filters = models.JSONField(default=dict)
preferences = models.JSONField(default=get_default_preferences)
sort_order = models.FloatField(default=65535)
class Meta:
verbose_name = "Project User Property"
verbose_name_plural = "Project User Properties"
db_table = "project_user_properties"
ordering = ("-created_at",)
unique_together = ["user", "project", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["user", "project"],
condition=Q(deleted_at__isnull=True),
name="project_user_property_unique_user_project_when_deleted_at_null",
)
]
def __str__(self):
"""Return properties status of the project"""
return str(self.user)
+12 -2
View File
@@ -84,7 +84,7 @@ class User(AbstractBaseUser, PermissionsMixin):
is_staff = models.BooleanField(default=False)
is_email_verified = models.BooleanField(default=False)
is_password_autoset = models.BooleanField(default=False)
is_password_reset_required = models.BooleanField(default=False)
# random token generated
token = models.CharField(max_length=64, blank=True)
@@ -192,6 +192,10 @@ class Profile(TimeAuditModel):
FRIDAY = 5
SATURDAY = 6
class NotificationViewMode(models.TextChoices):
FULL = "full", "Full"
COMPACT = "compact", "Compact"
START_OF_THE_WEEK_CHOICES = (
(SUNDAY, "Sunday"),
(MONDAY, "Monday"),
@@ -221,7 +225,9 @@ class Profile(TimeAuditModel):
billing_address = models.JSONField(null=True)
has_billing_address = models.BooleanField(default=False)
company_name = models.CharField(max_length=255, blank=True)
notification_view_mode = models.CharField(
max_length=255, choices=NotificationViewMode.choices, default=NotificationViewMode.FULL
)
is_smooth_cursor_enabled = models.BooleanField(default=False)
# mobile
is_mobile_onboarded = models.BooleanField(default=False)
@@ -233,8 +239,12 @@ class Profile(TimeAuditModel):
goals = models.JSONField(default=dict)
background_color = models.CharField(max_length=255, default=get_random_color)
# navigation tour
is_navigation_tour_completed = models.BooleanField(default=False)
# marketing
has_marketing_email_consent = models.BooleanField(default=False)
is_subscribed_to_changelog = models.BooleanField(default=False)
class Meta:
verbose_name = "Profile"
+1
View File
@@ -38,6 +38,7 @@ class Webhook(BaseModel):
cycle = models.BooleanField(default=False)
issue_comment = models.BooleanField(default=False)
is_internal = models.BooleanField(default=False)
version = models.CharField(default="v1", max_length=50)
def __str__(self):
return f"{self.workspace.slug} {self.url}"
+14
View File
@@ -112,6 +112,16 @@ def slug_validator(value):
raise ValidationError("Slug is not valid")
def get_default_product_tour():
return {
"work_items": False,
"cycles": False,
"modules": False,
"intake": False,
"pages": False,
}
class Workspace(BaseModel):
TIMEZONE_CHOICES = tuple(zip(pytz.common_timezones, pytz.common_timezones))
@@ -204,6 +214,9 @@ class WorkspaceMember(BaseModel):
default_props = models.JSONField(default=get_default_props)
issue_props = models.JSONField(default=get_issue_props)
is_active = models.BooleanField(default=True)
getting_started_checklist = models.JSONField(default=dict)
tips = models.JSONField(default=dict)
explored_features = models.JSONField(default=dict)
class Meta:
unique_together = ["workspace", "member", "deleted_at"]
@@ -325,6 +338,7 @@ class WorkspaceUserProperties(BaseModel):
choices=NavigationControlPreference.choices,
default=NavigationControlPreference.ACCORDION,
)
product_tour = models.JSONField(default=get_default_product_tour)
class Meta:
unique_together = ["workspace", "user", "deleted_at"]
@@ -6,7 +6,7 @@ from django.utils import timezone
from plane.db.models import (
Project,
ProjectMember,
IssueUserProperty,
ProjectUserProperty,
State,
WorkspaceMember,
User,
@@ -82,8 +82,8 @@ class TestProjectAPIPost(TestProjectBase):
assert project_member.role == 20 # Administrator
assert project_member.is_active is True
# Verify IssueUserProperty was created
assert IssueUserProperty.objects.filter(project=project, user=user).exists()
# Verify ProjectUserProperty was created
assert ProjectUserProperty.objects.filter(project=project, user=user).exists()
# Verify default states were created
states = State.objects.filter(project=project)
@@ -116,8 +116,8 @@ class TestProjectAPIPost(TestProjectBase):
project = Project.objects.get(name=project_data["name"])
assert ProjectMember.objects.filter(project=project, role=20).count() == 2
# Verify both have IssueUserProperty
assert IssueUserProperty.objects.filter(project=project).count() == 2
# Verify both have ProjectUserProperty
assert ProjectUserProperty.objects.filter(project=project).count() == 2
@pytest.mark.django_db
def test_create_project_guest_forbidden(self, session_client, workspace):
@@ -1,330 +1,306 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" >
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="format-detection" content="telephone=no" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Set a new password to your Plane account</title>
<style type="text/css" emogrify="no"> #outlook a { padding: 0; } .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide: all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0; } img { outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; } a img { border: none; } table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important; } .r1-i { background-color: #ffffff !important; } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important; } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important; } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important; } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important; } .r6-o { border-style: solid !important; width: 100% !important; } .r7-i { padding-left: 0px !important; padding-right: 0px !important; } .r8-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important; } .r9-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r10-i { padding-bottom: 10px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 10px !important; } .r11-i { padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; } .r12-i { background-color: #ffffff !important; padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; } .r13-c { box-sizing: border-box !important; padding: 0 !important; text-align: center !important; valign: top !important; width: 100% !important; } .r14-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 15px !important; margin-top: 15px !important; width: 100% !important; } .r15-i { padding: 0 !important; text-align: center !important; } .r16-r { background-color: #006399 !important; border-radius: 4px !important; border-width: 0px !important; box-sizing: border-box; height: initial !important; padding: 0 !important; padding-bottom: 12px !important; padding-top: 12px !important; text-align: center !important; width: 100% !important; } .r17-i { padding-bottom: 0px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 0px !important; } .r18-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important; } .r19-i { padding-bottom: 5px !important; padding-top: 5px !important; } .r20-i { padding-bottom: 15px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 15px !important; } .r21-o { border-bottom-color: #efefef !important; border-bottom-width: 2px !important; border-left-color: #efefef !important; border-left-width: 2px !important; border-right-color: #efefef !important; border-right-width: 2px !important; border-style: solid !important; border-top-color: #efefef !important; border-top-width: 2px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r22-i { padding-bottom: 5px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 5px !important; text-align: left !important; } .r23-i { padding-bottom: 5px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 5px !important; } .r24-c { box-sizing: border-box !important; width: 100% !important; } .r25-i { font-size: 0px !important; padding-bottom: 10px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 10px !important; } .r26-c { box-sizing: border-box !important; width: 32px !important; } .r27-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important; } .r28-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } .r29-i { padding-bottom: 0px !important; padding-top: 5px !important; text-align: center !important; } body { -webkit-text-size-adjust: none; } .nl2go-responsive-hide { display: none; } .nl2go-body-table { min-width: unset !important; } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important; } .resp-table { display: inline-table !important; } .magic-resp { display: table-cell !important; } } </style>
<style type="text/css"> p, h1, h2, h3, h4, ol, ul { margin: 0; } a, a:link { color: #006399; text-decoration: underline; } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 16px; line-height: 1.5; word-break: break-word; } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; } .default-heading1 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 36px; word-break: break-word; } .default-heading2 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 32px; word-break: break-word; } .default-heading3 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 24px; word-break: break-word; } .default-heading4 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 18px; word-break: break-word; } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style>
<!--[if mso ]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<! [endif]-->
<style type="text/css"> a:link { color: #006399; text-decoration: underline; } </style>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Reset your Plane password</title>
<style>
/* Basic resets for email clients */
body,
table,
td,
p {
margin: 0;
padding: 0;
}
img {
border: 0;
line-height: 100%;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
table {
border-collapse: collapse;
}
/* Mobile adjustments */
@media only screen and (max-width: 600px) {
.email-container {
width: 100% !important;
}
.email-content {
padding: 24px 20px !important;
}
.email-footer {
padding: 0 20px 16px 20px !important;
}
}
</style>
</head>
<body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #ffffff" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%" >
<tr>
<td>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="600" align="center" class="r0-o" style="table-layout: fixed; width: 600px" >
<tr>
<td valign="top" class="r1-i" style="background-color: #ffffff">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
<tr>
<td class="r4-i" style="padding-bottom: 20px; padding-top: 20px" >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
<tr>
<td valign="top" class="r7-i" style=" padding-left: 10px; padding-right: 10px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="150" class="r9-o" style=" table-layout: fixed; width: 150px; " >
<tr>
<td style=" font-size: 0px; line-height: 0px; " > <img src="https://media.docs.plane.so/logo/new-logo-white.png" width="150" border="0" style=" display: block; width: 100%; " /> </td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
<tr>
<td class="r10-i" style="padding-bottom: 10px; padding-top: 10px" >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
<tr>
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r9-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td align="left" valign="top" class="r11-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; line-height: 1.5; padding-bottom: 15px; padding-top: 15px; text-align: left; " >
<div>
<p style="margin: 0"> <span style=" font-family: Arial, helvetica, sans-serif; font-size: 32px; " ><strong >Reset your Plane account's password</strong ></span > </p>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r9-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td align="left" valign="top" class="r12-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: arial, helvetica, sans-serif; font-size: 16px; line-height: 1.5; word-break: break-word; background-color: #ffffff; padding-bottom: 15px; padding-top: 15px; text-align: left; " >
<div>
<p style="margin: 0"> <span style="font-size: 17px" >Someone, hopefully you, has requested a new password be set to your Plane account. If it was you, please click the button below to reset your password.</span > </p>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="r13-c" align="center" style=" align: center; padding-bottom: 15px; padding-top: 15px; valign: top; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="285" class="r14-o" style=" background-color: #006399; border-collapse: separate; border-color: #006399; border-radius: 4px; border-style: solid; border-width: 0px; table-layout: fixed; width: 285px; " >
<tr>
<td height="18" align="center" valign="top" class="r15-i nl2go-default-textstyle" style=" word-break: break-word; background-color: #006399; border-radius: 4px; color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; line-height: 1.15; padding-bottom: 12px; padding-top: 12px; text-align: center; " > <a href="{{forgot_password_url}}" class="r16-r default-button" target="_blank" data-btn="1" style=" font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; word-wrap: break-word; display: block; -webkit-text-size-adjust: none; color: #ffffff; font-family: arial, helvetica, sans-serif; font-size: 16px; " > <span>Reset password</span></a > </td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r9-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td align="left" valign="top" class="r11-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: arial, helvetica, sans-serif; font-size: 16px; line-height: 1.5; word-break: break-word; padding-bottom: 15px; padding-top: 15px; text-align: left; " >
<div>
<p style="margin: 0"> <span style="font-size: 17px" >Ignore this if you didn't ask for a new link.</span > </p>
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
<tr>
<td class="r17-i">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
<tr>
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r18-c" align="center">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" class="r3-o" style="table-layout: fixed" >
<tr>
<td class="r19-i" style=" padding-bottom: 5px; padding-top: 5px; height: 1px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td>
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" valign="" class="r19-i" height="1" style=" border-top-style: solid; background-clip: border-box; border-top-color: #efefef; border-top-width: 1px; font-size: 1px; line-height: 1px; " >
<tr>
<td height="0" style=" font-size: 0px; line-height: 0px; " > ­ </td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
<tr>
<td class="r20-i" style="padding-bottom: 15px; padding-top: 15px" >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
<tr>
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style=" table-layout: fixed; width: 100%; " >
<tr class="nl2go-responsive-hide">
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > ­ </td>
<td height="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > ­ </td>
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > ­ </td>
</tr>
<tr>
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td>
<td align="left" valign="top" class="r22-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; line-height: 1.5; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; text-align: left; " >
<div>
<p style="margin: 0"> <span style="font-size: 13px" >Despite our popularity, we are humbly early-stage. We are shipping fast, so please reach out to us with feature requests, major and minor nits, and anything else you find missing. We read every </span ><a href="https://discord.com/channels/1031547764020084846/1094927053867995176" title="Plane Support on Discod" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >message</span ></a ><span style="font-size: 13px" >, </span ><a href="http://twitter.com/planepowers" title="@planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >tweet</span ></a ><span style="font-size: 13px" >, and </span ><a href="https://github.com/makeplane/plane/issues" title="Plane's GitHub conversations" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >conversation</span ></a ><span style="font-size: 13px" > and update </span ><a href="https://plane.sh/plane/0b170a1c-0e55-47cb-9307-ea49a05672b5?board=kanban" title="Plane's roadmap" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >our public roadmap</span ></a ><span style="font-size: 13px" >.</span > </p>
</div>
</td>
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td>
</tr>
<tr class="nl2go-responsive-hide">
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > ­ </td>
<td height="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > ­ </td>
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > ­ </td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
<tr>
<td class="r23-i" style="padding-bottom: 5px; padding-top: 5px" >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
<tr>
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r18-c" align="center">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" align="center" class="r3-o" style=" table-layout: fixed; width: 570px; " >
<tr>
<td valign="top">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r24-c" style=" display: inline-block; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" class="r6-o" style=" table-layout: fixed; width: 570px; " >
<tr>
<td class="r25-i" style=" padding-bottom: 10px; padding-left: 209px; padding-right: 209px; padding-top: 10px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="40" class="r26-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://github.com/makeplane" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr>
</table>
</th>
<th width="40" class="r26-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr>
</table>
</th>
<th width="40" class="r26-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://twitter.com/planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr>
</table>
</th>
<th width="32" class="r26-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r28-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://plane.so/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
<tr>
<td class="r23-i" style="padding-bottom: 5px; padding-top: 5px" >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
<tr>
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r9-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td align="center" valign="top" class="r29-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; word-break: break-word; font-size: 18px; line-height: 1.5; padding-top: 5px; text-align: center; " >
<div>
<p style=" margin: 0; font-size: 14px; " > <span style="font-size: 12px" >This email was sent to {{email}}. Please delete if you aren't the intended recipient.</span > </p>
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<body
bgcolor="#f9f9f9"
style="
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
sans-serif;
font-weight: 400;
color: #474747;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
background-color: #f9f9f9;
"
>
<!-- Preheader (hidden) -->
<span
class="preheader"
style="
display: none;
font-size: 1px;
color: transparent;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
"
>
Reset your Plane password with this secure link.
</span>
<!-- Outer Wrapper -->
<center
class="email-body"
style="width: 100%; background-color: #f9f9f9; padding: 40px 0;"
>
<!-- HEADER -->
<table role="presentation" width="100%">
<tr>
<td></td>
<td
class="email-container"
width="620"
style="width: 620px; max-width: 620px; margin: 0 auto;"
>
<table
role="presentation"
width="100%"
style="background-color: #000000;"
>
<tr>
<td style="padding: 24px 40px; text-align: center;">
<img
src="https://media.docs.plane.so/logo/new-logo-dark.png"
width="120"
alt="Plane"
style="height: auto; display: block; margin: 0 auto;"
/>
</td>
</tr>
</table>
</td>
<td></td>
</tr>
</table>
<!-- BODY (card under header) -->
<table role="presentation" width="100%">
<tr>
<td></td>
<td
class="email-container"
width="620"
style="width: 620px; max-width: 620px; margin: 0 auto;"
>
<table
role="presentation"
width="100%"
class="email-content"
style="background-color: #ffffff; padding: 32px 40px 32px 40px;"
>
<tr>
<td
style="
font-size: 16px;
line-height: 1.5;
color: #474747;
padding: 24px;
"
>
<!-- ========================================= -->
<!-- PLANE RESET PASSWORD EMAIL -->
<!-- ========================================= -->
<!-- Heading -->
<h1
style="
margin: 0 0 12px;
font-size: 22px;
line-height: 1.35;
font-weight: 600;
color: #111111;
"
>
Reset your Plane password
</h1>
<!-- Intro copy -->
<p style="margin: 0 0 16px;">
We received a request to reset your Plane password. To reset
your password, click the button below.
</p>
<!-- Primary button -->
<table
role="presentation"
cellspacing="0"
cellpadding="0"
border="0"
style="margin: 0 0 24px;"
>
<tr>
<td>
<a
href="{{forgot_password_url}}"
target="_blank"
style="
background-color: #1080bc;
border-radius: 6px;
color: #ffffff;
padding: 12px 24px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
font-weight: 500;
white-space: nowrap;
"
>
Reset password
</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<p
style="
margin: 0;
font-size: 13px;
line-height: 1.6;
color: #5f5e5e;
"
>
If the button doesnt work, copy and paste this link into
your browser:
</p>
<p
style="
margin: 6px 0 0;
font-size: 13px;
line-height: 1.6;
color: #5f5e5e;
"
>
<span
style="
display: inline-block;
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
background-color: #f3f3f3;
padding: 4px 6px;
border-radius: 3px;
border: 1px solid #e9e9e9;
word-break: break-all;
"
>{{forgot_password_url}}</span
>
</p>
<!-- Note -->
<p
style="
margin: 16px 0 0;
font-size: 13px;
line-height: 1.6;
color: #5f5e5e;
"
>
Note: This email was sent to
<span
style="
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
background-color: #f3f3f3;
padding: 2px 4px;
border-radius: 3px;
border: 1px solid #e9e9e9;
"
>{{email}}</span
>. If you werent expecting a password reset, you can safely
ignore this email.
</p>
</td>
</tr>
</table>
</td>
<td></td>
</tr>
</table>
<!-- FOOTER (simple, aligned with body) -->
<table role="presentation" width="100%">
<tr>
<td></td>
<td
class="email-container"
width="620"
style="width: 620px; max-width: 620px; margin: 0 auto;"
>
<table
role="presentation"
width="100%"
class="email-footer"
style="background-color: #ffffff; padding: 0 40px 16px 40px;"
>
<tr>
<td
style="
border-top: 1px solid #e9e9e9;
padding: 24px;
font-size: 12px;
line-height: 1.6;
color: #979797;
text-align: left;
"
>
<p style="margin: 0;">
Plane Software, Inc.
</p>
</td>
</tr>
</table>
</td>
<td></td>
</tr>
</table>
</center>
<base target="_blank" />
</body>
</html>
+261 -284
View File
@@ -1,288 +1,265 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" >
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="format-detection" content="telephone=no" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Your unique Plane login code is code</title>
<style type="text/css" emogrify="no"> #outlook a { padding: 0; } .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide: all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0; } img { outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; } a img { border: none; } table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important; } .r1-i { background-color: #ffffff !important; } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important; } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important; } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important; } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important; } .r6-o { border-style: solid !important; width: 100% !important; } .r7-i { padding-left: 0px !important; padding-right: 0px !important; } .r8-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important; } .r9-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r10-i { padding-bottom: 15px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 15px !important; } .r11-i { padding-bottom: 10px !important; padding-top: 10px !important; text-align: left !important; } .r12-i { padding-bottom: 10px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 10px !important; } .r13-o { border-bottom-color: #d9e4ff !important; border-bottom-width: 1px !important; border-left-color: #d9e4ff !important; border-left-width: 1px !important; border-right-color: #d9e4ff !important; border-right-width: 1px !important; border-style: solid !important; border-top-color: #d9e4ff !important; border-top-width: 1px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r14-i { background-color: #ecf1ff !important; padding-bottom: 10px !important; padding-left: 10px !important; padding-right: 10px !important; padding-top: 10px !important; text-align: left !important; } .r15-o { border-bottom-color: #efefef !important; border-bottom-width: 2px !important; border-left-color: #efefef !important; border-left-width: 2px !important; border-right-color: #efefef !important; border-right-width: 2px !important; border-style: solid !important; border-top-color: #efefef !important; border-top-width: 2px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r16-i { padding-bottom: 5px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 5px !important; text-align: left !important; } .r17-i { padding-bottom: 5px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 5px !important; } .r18-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important; } .r19-c { box-sizing: border-box !important; width: 100% !important; } .r20-i { font-size: 0px !important; padding-bottom: 10px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 10px !important; } .r21-c { box-sizing: border-box !important; width: 32px !important; } .r22-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important; } .r23-i { padding-bottom: 5px !important; padding-top: 5px !important; } .r24-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } .r25-i { padding-bottom: 0px !important; padding-top: 5px !important; text-align: center !important; } body { -webkit-text-size-adjust: none; } .nl2go-responsive-hide { display: none; } .nl2go-body-table { min-width: unset !important; } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important; } .resp-table { display: inline-table !important; } .magic-resp { display: table-cell !important; } } </style>
<style type="text/css"> p, h1, h2, h3, h4, ol, ul { margin: 0; } a, a:link { color: #006399; text-decoration: underline; } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 16px; line-height: 1.5; word-break: break-word; } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; } .default-heading1 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 36px; word-break: break-word; } .default-heading2 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 32px; word-break: break-word; } .default-heading3 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 24px; word-break: break-word; } .default-heading4 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 18px; word-break: break-word; } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style>
<!--[if mso ]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<! [endif]-->
<style type="text/css"> a:link { color: #006399; text-decoration: underline; } </style>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Your Plane login code</title>
<style>
/* Basic resets for email clients */
body,
table,
td,
p {
margin: 0;
padding: 0;
}
img {
border: 0;
line-height: 100%;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
table {
border-collapse: collapse;
}
/* Mobile adjustments */
@media only screen and (max-width: 600px) {
.email-container {
width: 100% !important;
}
.email-content {
padding: 24px 20px !important;
}
.email-footer {
padding: 0 20px 16px 20px !important;
}
}
</style>
</head>
<body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #ffffff" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%" >
<tr>
<td>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="600" align="center" class="r0-o" style="table-layout: fixed; width: 600px" >
<tr>
<td valign="top" class="r1-i" style="background-color: #ffffff">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
<tr>
<td class="r4-i" style="padding-bottom: 20px; padding-top: 20px" >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
<tr>
<td valign="top" class="r7-i" style=" padding-left: 10px; padding-right: 10px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="150" class="r9-o" style=" table-layout: fixed; width: 150px; " >
<tr>
<td style=" font-size: 0px; line-height: 0px; " > <img src="https://media.docs.plane.so/logo/new-logo-white.png" width="150" border="0" style=" display: block; width: 100%; " /> </td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
<tr>
<td class="r10-i" style="padding-bottom: 15px; padding-top: 15px" >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
<tr>
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r9-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td align="left" valign="top" class="r11-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; line-height: 1.5; word-break: break-word; padding-bottom: 10px; padding-top: 10px; text-align: left; " >
<div>
<h2 class="default-heading2" style=" margin: 0; color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 32px; word-break: break-word; " > The code below is only valid for 10 minutes. </h2>
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
<tr>
<td class="r12-i" style="padding-bottom: 10px; padding-top: 10px" >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
<tr>
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r13-o" style=" background-color: #ecf1ff; border-bottom-color: #d9e4ff; border-bottom-width: 1px; border-collapse: separate; border-left-color: #d9e4ff; border-left-width: 1px; border-radius: 5px; border-right-color: #d9e4ff; border-right-width: 1px; border-style: solid; border-top-color: #d9e4ff; border-top-width: 1px; table-layout: fixed; width: 100%; " >
<tr>
<td align="left" valign="top" class="r14-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; background-color: #ecf1ff; border-radius: 4px; line-height: 3; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; padding-top: 10px; text-align: left; " >
<div>
<p style="margin: 0"> <span style=" font-family: Arial, helvetica, sans-serif; font-size: 24px; " >{{code}}</span > </p>
<p style="margin: 0"> <span style=" color: #5f5e5e; font-family: Arial, helvetica, sans-serif; font-size: 14px; " >Please copy and paste this on the screen where you requested this from.</span > </p>
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
<tr>
<td class="r10-i" style="padding-bottom: 15px; padding-top: 15px" >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
<tr>
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r15-o" style=" table-layout: fixed; width: 100%; " >
<tr class="nl2go-responsive-hide">
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > ­ </td>
<td height="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > ­ </td>
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > ­ </td>
</tr>
<tr>
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td>
<td align="left" valign="top" class="r16-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; line-height: 1.5; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; text-align: left; " >
<div>
<p style="margin: 0"> <span style="font-size: 13px" >Despite our popularity, we are humbly early-stage. We are shipping fast, so please reach out to us with feature requests, major and minor nits, and anything else you find missing. We read every </span ><a href="https://discord.com/channels/1031547764020084846/1094927053867995176" title="Plane Support on Discod" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >message</span ></a ><span style="font-size: 13px" >, </span ><a href="http://twitter.com/planepowers" title="@planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >tweet</span ></a ><span style="font-size: 13px" >, and </span ><a href="https://github.com/makeplane/plane/issues" title="Plane's GitHub conversations" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >conversation</span ></a ><span style="font-size: 13px" > and update </span ><a href="https://plane.sh/plane/0b170a1c-0e55-47cb-9307-ea49a05672b5?board=kanban" title="Plane's roadmap" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >our public roadmap</span ></a ><span style="font-size: 13px" >.</span > </p>
</div>
</td>
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td>
</tr>
<tr class="nl2go-responsive-hide">
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > ­ </td>
<td height="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > ­ </td>
<td height="2" width="2" style=" font-size: 2px; line-height: 2px; background-color: #efefef; " > ­ </td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
<tr>
<td class="r17-i" style="padding-bottom: 5px; padding-top: 5px" >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
<tr>
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r18-c" align="center">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" align="center" class="r3-o" style=" table-layout: fixed; width: 570px; " >
<tr>
<td valign="top">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r19-c" style=" display: inline-block; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" class="r6-o" style=" table-layout: fixed; width: 570px; " >
<tr>
<td class="r20-i" style=" padding-bottom: 10px; padding-left: 209px; padding-right: 209px; padding-top: 10px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="40" class="r21-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r22-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://github.com/makeplane" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr>
</table>
</th>
<th width="40" class="r21-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r22-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr>
</table>
</th>
<th width="40" class="r21-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r22-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://twitter.com/planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr>
</table>
</th>
<th width="32" class="r21-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r24-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://plane.so/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%" >
<tr>
<td class="r17-i" style="padding-bottom: 5px; padding-top: 5px" >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%" >
<tr>
<td valign="top" class="r7-i" style=" padding-left: 15px; padding-right: 15px; " >
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" >
<tr>
<td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r9-o" style=" table-layout: fixed; width: 100%; " >
<tr>
<td align="center" valign="top" class="r25-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; word-break: break-word; font-size: 18px; line-height: 1.5; padding-top: 5px; text-align: center; " >
<div>
<p style=" margin: 0; font-size: 14px; " > <span style="font-size: 12px" >This email was sent to {{email}}. Please delete if you aren't the intended recipient.</span > </p>
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<body
bgcolor="#f9f9f9"
style="
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
sans-serif;
font-weight: 400;
color: #474747;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
background-color: #f9f9f9;
"
>
<!-- Preheader (hidden) -->
<span
class="preheader"
style="
display: none;
font-size: 1px;
color: transparent;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
"
>
Your Plane login code {{code}} is valid for 10 minutes.
</span>
<!-- Outer Wrapper -->
<center
class="email-body"
style="width: 100%; background-color: #f9f9f9; padding: 40px 0;"
>
<!-- HEADER -->
<table role="presentation" width="100%">
<tr>
<td></td>
<td
class="email-container"
width="620"
style="width: 620px; max-width: 620px; margin: 0 auto;"
>
<table
role="presentation"
width="100%"
style="background-color: #000000;"
>
<tr>
<td style="padding: 24px 40px; text-align: center;">
<img
src="https://media.docs.plane.so/logo/new-logo-dark.png"
width="120"
alt="Plane"
style="height: auto; display: block; margin: 0 auto;"
/>
</td>
</tr>
</table>
</td>
<td></td>
</tr>
</table>
<!-- BODY (card under header) -->
<table role="presentation" width="100%">
<tr>
<td></td>
<td
class="email-container"
width="620"
style="width: 620px; max-width: 620px; margin: 0 auto;"
>
<table
role="presentation"
width="100%"
class="email-content"
style="background-color: #ffffff; padding: 32px 40px 32px 40px;"
>
<tr>
<td
style="
font-size: 16px;
line-height: 1.5;
color: #474747;
padding: 24px;
"
>
<!-- ========================================= -->
<!-- PLANE LOGIN CODE EMAIL -->
<!-- ========================================= -->
<!-- Heading -->
<h1
style="
margin: 0 0 12px;
font-size: 22px;
line-height: 1.35;
font-weight: 600;
color: #111111;
"
>
Your login code for Plane
</h1>
<!-- Code chip -->
<p style="margin: 12px 0 24px;">
<span
style="
display: inline-block;
padding: 8px 14px;
background-color: #e4e5e7;
border-radius: 6px;
font-family: 'SF Mono', Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
font-size: 15px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #111111;
"
>
{{code}}
</span>
</p>
<!-- Main info -->
<p
style="
margin: 0;
font-size: 13px;
line-height: 1.6;
color: #5f5e5e;
"
>
This code is valid for the next 10 minutes. Enter it on the
sign-in page to complete your login.
</p>
<!-- Secondary info -->
<p
style="
margin: 16px 0 0;
font-size: 13px;
line-height: 1.6;
color: #5f5e5e;
"
>
Note: This email was sent to
<span
style="
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
background-color: #f3f3f3;
padding: 2px 4px;
border-radius: 3px;
border: 1px solid #e9e9e9;
"
>{{email}}</span
>. If you were not expecting a sign-in code, you can safely
ignore this message.
</p>
</td>
</tr>
</table>
</td>
<td></td>
</tr>
</table>
<!-- FOOTER (simple, aligned with body) -->
<table role="presentation" width="100%">
<tr>
<td></td>
<td
class="email-container"
width="620"
style="width: 620px; max-width: 620px; margin: 0 auto;"
>
<table
role="presentation"
width="100%"
class="email-footer"
style="background-color: #ffffff; padding: 0 40px 16px 40px;"
>
<tr>
<td
style="
border-top: 1px solid #e9e9e9;
padding: 24px;
font-size: 12px;
line-height: 1.6;
color: #979797;
text-align: left;
"
>
<p style="margin: 0;">
Plane Software, Inc.
</p>
</td>
</tr>
</table>
</td>
<td></td>
</tr>
</table>
</center>
<base target="_blank" />
</body>
</html>
@@ -1,2 +1,215 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html> Hey there,<br/> Your requested data export from Plane Analytics is now ready. The information has been compiled into a CSV format for your convenience.<br/> Please find the attachment and download the CSV file. This file can easily be imported into any spreadsheet program for further analysis.<br/> If you require any assistance or have any questions, please do not hesitate to contact us.<br/> Thank you </html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Your Plane Analytics export is ready</title>
<style>
/* Basic resets for email clients */
body,
table,
td,
p {
margin: 0;
padding: 0;
}
img {
border: 0;
line-height: 100%;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
table {
border-collapse: collapse;
}
/* Mobile adjustments */
@media only screen and (max-width: 600px) {
.email-container {
width: 100% !important;
}
.email-content {
padding: 24px 20px !important;
}
.email-footer {
padding: 0 20px 16px 20px !important;
}
}
</style>
</head>
<body
bgcolor="#f9f9f9"
style="
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
sans-serif;
font-weight: 400;
color: #474747;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
background-color: #f9f9f9;
"
>
<!-- Preheader (hidden) -->
<span
class="preheader"
style="
display: none;
font-size: 1px;
color: transparent;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
"
>
Your requested Plane Analytics data export is attached as a CSV file.
</span>
<!-- Outer Wrapper -->
<center
class="email-body"
style="width: 100%; background-color: #f9f9f9; padding: 40px 0;"
>
<!-- HEADER -->
<table role="presentation" width="100%">
<tr>
<td></td>
<td
class="email-container"
width="620"
style="width: 620px; max-width: 620px; margin: 0 auto;"
>
<table role="presentation" width="100%" style="background-color: #000000;">
<tr>
<td style="padding: 24px 40px; text-align: center;">
<img
src="https://media.docs.plane.so/logo/new-logo-dark.png"
width="120"
alt="Plane"
style="height: auto; display: block; margin: 0 auto;"
/>
</td>
</tr>
</table>
</td>
<td></td>
</tr>
</table>
<!-- BODY (card under header) -->
<table role="presentation" width="100%">
<tr>
<td></td>
<td
class="email-container"
width="620"
style="width: 620px; max-width: 620px; margin: 0 auto;"
>
<table
role="presentation"
width="100%"
class="email-content"
style="background-color: #ffffff; padding: 32px 40px 32px 40px;"
>
<tr>
<td
style="
font-size: 16px;
line-height: 1.5;
color: #474747;
padding: 24px;
"
>
<!-- ========================================= -->
<!-- PLANE ANALYTICS DATA EXPORT EMAIL -->
<!-- ========================================= -->
<!-- Heading -->
<h1
style="
margin: 0 0 12px;
font-size: 22px;
line-height: 1.35;
font-weight: 600;
color: #111111;
"
>
Your Plane Analytics export is ready
</h1>
<!-- Body copy -->
<p style="margin: 0 0 16px;">
Your requested data export from Plane Analytics is ready. Weve attached it as a CSV file for your convenience.
</p>
<!-- Small-note styled paragraphs -->
<p
style="
margin: 0 0 12px;
font-size: 13px;
line-height: 1.6;
color: #5f5e5e;
"
>
You can open the CSV in any spreadsheet or BI tool to analyze the data, build reports, or share insights with your team.
</p>
</td>
</tr>
</table>
</td>
<td></td>
</tr>
</table>
<!-- FOOTER -->
<table role="presentation" width="100%">
<tr>
<td></td>
<td
class="email-container"
width="620"
style="width: 620px; max-width: 620px; margin: 0 auto;"
>
<table
role="presentation"
width="100%"
class="email-footer"
style="background-color: #ffffff; padding: 0 40px 16px 40px;"
>
<tr>
<td
style="
border-top: 1px solid #e9e9e9;
padding: 24px;
font-size: 12px;
line-height: 1.6;
color: #979797;
text-align: left;
"
>
<p style="margin: 0;">
Plane Software, Inc.
</p>
</td>
</tr>
</table>
</td>
<td></td>
</tr>
</table>
</center>
<base target="_blank" />
</body>
</html>
@@ -1,219 +1,306 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{first_name}} has invited you to join them in {{workspace_name}} on Plane.</title>
<style type="text/css" emogrify="no">#outlook a { padding:0; } .ExternalClass { width:100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide:all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0; } img { outline:none; text-decoration:none; -ms-interpolation-mode: bicubic; } a img { border:none; } table { border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: ' \03D1';} } </style>
<style type="text/css" emogrify="no">@media (max-width: 600px) { .gmx-killpill { content: ' \03D1';} .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important } .r1-i { background-color: #ffffff !important } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important } .r6-o { border-style: solid !important; width: 100% !important } .r7-i { padding-left: 0px !important; padding-right: 0px !important } .r8-i { padding-bottom: 20px !important; padding-left: 10px !important; padding-right: 10px !important; padding-top: 20px !important } .r9-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important } .r10-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important } .r11-i { padding-top: 15px !important; text-align: center !important } .r12-c { box-sizing: border-box !important; padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; valign: top !important; width: 100% !important } .r13-c { box-sizing: border-box !important; padding: 0 !important; text-align: center !important; valign: top !important; width: 100% !important } .r14-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 15px !important; margin-top: 15px !important; width: 100% !important } .r15-i { padding: 0 !important; text-align: center !important } .r16-r { background-color: #006399 !important; border-radius: 4px !important; border-width: 0px !important; box-sizing: border-box; height: initial !important; padding: 0 !important; padding-bottom: 12px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 12px !important; text-align: center !important; width: 100% !important } .r17-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important } .r18-c { box-sizing: border-box !important; width: 100% !important } .r19-i { font-size: 0px !important; padding-bottom: 15px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 15px !important } .r20-c { box-sizing: border-box !important; width: 32px !important } .r21-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important } .r22-i { padding-bottom: 5px !important; padding-top: 5px !important } .r23-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important } body { -webkit-text-size-adjust: none } .nl2go-responsive-hide { display: none } .nl2go-body-table { min-width: unset !important } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important } .resp-table { display: inline-table !important } .magic-resp { display: table-cell !important } } </style>
<style type="text/css">p, h1, h2, h3, h4, ol, ul, li { margin: 0; } a, a:link { color: #006399; text-decoration: underline } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 18px; line-height: 1.5; word-break: break-word } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: bold; line-height: 1.15; text-decoration: none; word-break: break-word } .default-heading1 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 36px; word-break: break-word } .default-heading2 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 32px; word-break: break-word } .default-heading3 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 24px; word-break: break-word } .default-heading4 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 18px; word-break: break-word } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<style type="text/css">a:link{color: #006399; text-decoration: underline;}</style>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{{first_name}} invited you to {{workspace_name}} on Plane</title>
<style>
/* Basic resets for email clients */
body,
table,
td,
p {
margin: 0;
padding: 0;
}
img {
border: 0;
line-height: 100%;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
table {
border-collapse: collapse;
}
/* Mobile adjustments */
@media only screen and (max-width: 600px) {
.email-container {
width: 100% !important;
}
.email-content {
padding: 24px 20px !important;
}
.email-footer {
padding: 0 20px 16px 20px !important;
}
}
</style>
</head>
<body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #ffffff;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%;">
<tr>
<td>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="600" align="center" class="r0-o" style="table-layout: fixed; width: 600px;">
<tr>
<td valign="top" class="r1-i" style="background-color: #ffffff;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%;">
<tr>
<td class="r4-i" style="padding-bottom: 20px; padding-top: 20px;">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%;">
<tr>
<td valign="top" class="r7-i" style="padding-left: 10px; padding-right: 10px;">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
<tr>
<td class="r2-c" align="center">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="200" class="r3-o" style="table-layout: fixed; width: 200px;">
<tr>
<td style="font-size: 0px; line-height: 0px;"> <img src="https://media.docs.plane.so/logo/new-logo-white.png" width="200" border="0" style="display: block; width: 100%;"></td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%;">
<tr>
<td class="r8-i" style="padding-bottom: 20px; padding-top: 20px;">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%;">
<tr>
<td valign="top" class="r7-i">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
<tr>
<td class="r9-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r10-o" style="table-layout: fixed; width: 100%;">
<tr>
<td align="center" valign="top" class="r11-i nl2go-default-textstyle" style="color: #3b3f44; font-family: georgia,serif; font-size: 18px; word-break: break-word; line-height: 1.5; padding-top: 15px; text-align: center;">
<div>
<h1 class="default-heading1" style="margin: 0; color: #1f2d3d; font-family: arial,helvetica,sans-serif; font-size: 36px; word-break: break-word;">You are a celebrated colleague!</h1>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="r12-c nl2go-default-textstyle" align="left" style="color: #3b3f44; font-family: georgia,serif; font-size: 18px; line-height: 1.5; word-break: break-word; padding-bottom: 15px; padding-top: 15px; text-align: left; valign: top;">
<div>
<p style="margin: 0;">{{first_name}} has invited you to join them in {{workspace_name}}, a workspace for projects and progress, on Plane. Some of our users have told us it's a privilege, but we will let you be the judge of that.</p>
</div>
</td>
</tr>
<tr>
<td class="r13-c" align="center" style="align: center; padding-bottom: 15px; padding-top: 15px; valign: top;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="300" class="r14-o" style="background-color: #006399; border-collapse: separate; border-color: #006399; border-radius: 4px; border-style: solid; border-width: 0px; table-layout: fixed; width: 300px;">
<tr>
<td height="18" align="center" valign="top" class="r15-i nl2go-default-textstyle" style="word-break: break-word; background-color: #006399; border-radius: 4px; color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; line-height: 1.15; padding-bottom: 12px; padding-left: 5px; padding-right: 5px; padding-top: 12px; text-align: center;"> <a href="{{abs_url}}" class="r16-r default-button" target="_blank" data-btn="1" style="font-style: normal; font-weight: bold; line-height: 1.15; text-decoration: none; word-break: break-word; word-wrap: break-word; display: block; -webkit-text-size-adjust: none; color: #ffffff; font-family: georgia, serif; font-size: 16px;"> <span><span style="font-family: Arial, helvetica, sans-serif;">Join them on Plane</span></span></a> </td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%;">
<tr>
<td class="r4-i" style="padding-bottom: 20px; padding-top: 20px;">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%;">
<tr>
<td valign="top" class="r7-i" style="padding-left: 15px; padding-right: 15px;">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
<tr>
<td class="r17-c" align="center">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" align="center" class="r3-o" style="table-layout: fixed; width: 570px;">
<tr>
<td valign="top">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
<tr>
<td class="r18-c" style="display: inline-block;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="570" class="r6-o" style="table-layout: fixed; width: 570px;">
<tr>
<td class="r19-i" style="padding-bottom: 15px; padding-left: 209px; padding-right: 209px; padding-top: 15px;">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
<tr>
<th width="40" class="r20-c mobshow resp-table" style="font-weight: normal;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style="table-layout: fixed; width: 100%;">
<tr>
<td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://github.com/makeplane" target="_blank" style="color: #006399; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td>
<td class="nl2go-responsive-hide" width="8" style="font-size: 0px; line-height: 1px;">­ </td>
</tr>
</table>
</th>
<th width="40" class="r20-c mobshow resp-table" style="font-weight: normal;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style="table-layout: fixed; width: 100%;">
<tr>
<td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style="color: #006399; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td>
<td class="nl2go-responsive-hide" width="8" style="font-size: 0px; line-height: 1px;">­ </td>
</tr>
</table>
</th>
<th width="40" class="r20-c mobshow resp-table" style="font-weight: normal;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style="table-layout: fixed; width: 100%;">
<tr>
<td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://twitter.com/planepowers" target="_blank" style="color: #006399; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td>
<td class="nl2go-responsive-hide" width="8" style="font-size: 0px; line-height: 1px;">­ </td>
</tr>
</table>
</th>
<th width="32" class="r20-c mobshow resp-table" style="font-weight: normal;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r23-o" style="table-layout: fixed; width: 100%;">
<tr>
<td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://plane.so/" target="_blank" style="color: #006399; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" align="center" class="r3-o" style="table-layout: fixed; width: 100%;">
<tr>
<td class="r4-i" style="padding-bottom: 20px; padding-top: 20px;">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
<tr>
<th width="100%" valign="top" class="r5-c" style="font-weight: normal;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r6-o" style="table-layout: fixed; width: 100%;">
<tr>
<td valign="top" class="r7-i" style="padding-left: 15px; padding-right: 15px;">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation">
<tr>
<td class="r12-c nl2go-default-textstyle" align="left" style="color: #3b3f44; font-family: georgia,serif; font-size: 18px; line-height: 1.5; word-break: break-word; padding-bottom: 15px; padding-top: 15px; text-align: left; valign: top;">
<div>
<p style="margin: 0; text-align: center;"><span style="font-family: Georgia, serif; font-size: 12px;">This email was sent to {{email}}. Please delete if you are not the intended recipient.</span></p>
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<body
bgcolor="#f9f9f9"
style="
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
sans-serif;
font-weight: 400;
color: #474747;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
background-color: #f9f9f9;
"
>
<!-- Preheader (hidden) -->
<span
class="preheader"
style="
display: none;
font-size: 1px;
color: transparent;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
"
>
{{first_name}} has invited you to join {{workspace_name}} on Plane.
</span>
<!-- Outer Wrapper -->
<center
class="email-body"
style="width: 100%; background-color: #f9f9f9; padding: 40px 0;"
>
<!-- HEADER -->
<table role="presentation" width="100%">
<tr>
<td></td>
<td
class="email-container"
width="620"
style="width: 620px; max-width: 620px; margin: 0 auto;"
>
<table
role="presentation"
width="100%"
style="background-color: #000000;"
>
<tr>
<td style="padding: 24px 40px; text-align: center;">
<img
src="https://media.docs.plane.so/logo/new-logo-dark.png"
width="120"
alt="Plane"
style="height: auto; display: block; margin: 0 auto;"
/>
</td>
</tr>
</table>
</td>
<td></td>
</tr>
</table>
<!-- BODY (card under header) -->
<table role="presentation" width="100%">
<tr>
<td></td>
<td
class="email-container"
width="620"
style="width: 620px; max-width: 620px; margin: 0 auto;"
>
<table
role="presentation"
width="100%"
class="email-content"
style="background-color: #ffffff; padding: 32px 40px 32px 40px;"
>
<tr>
<td
style="
font-size: 16px;
line-height: 1.5;
color: #474747;
padding: 24px;
"
>
<!-- ========================================= -->
<!-- PLANE WORKSPACE INVITE EMAIL -->
<!-- ========================================= -->
<!-- Heading -->
<h1
style="
margin: 0 0 12px;
font-size: 22px;
line-height: 1.35;
font-weight: 600;
color: #111111;
"
>
Join {{workspace_name}} on Plane
</h1>
<!-- Intro copy -->
<p style="margin: 0 0 12px;">
{{first_name}} has invited you to collaborate with them in
<strong>{{workspace_name}}</strong> workspace on Plane.
</p>
<!-- Primary button -->
<table
role="presentation"
cellspacing="0"
cellpadding="0"
border="0"
style="margin: 0 0 24px;"
>
<tr>
<td>
<a
href="{{abs_url}}"
target="_blank"
style="
background-color: #1080bc;
border-radius: 6px;
color: #ffffff;
padding: 12px 24px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
font-weight: 500;
white-space: nowrap;
"
>
Accept invite
</a>
</td>
</tr>
</table>
<!-- Secondary info -->
<p
style="
margin: 0;
font-size: 13px;
line-height: 1.6;
color: #5f5e5e;
"
>
If the button doesnt work, copy and paste this link into
your browser:
</p>
<p
style="
margin: 6px 0 0;
font-size: 13px;
line-height: 1.6;
color: #5f5e5e;
"
>
<span
style="
display: inline-block;
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
background-color: #f3f3f3;
padding: 4px 6px;
border-radius: 3px;
border: 1px solid #e9e9e9;
word-break: break-all;
"
>{{abs_url}}</span
>
</p>
<!-- Note -->
<p
style="
margin: 16px 0 0;
font-size: 13px;
line-height: 1.6;
color: #5f5e5e;
"
>
Note: This invitation was intended for
<span
style="
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
background-color: #f3f3f3;
padding: 2px 4px;
border-radius: 3px;
border: 1px solid #e9e9e9;
"
>{{email}}</span
>. If you were not expecting this invitation, you can ignore
this email.
</p>
</td>
</tr>
</table>
</td>
<td></td>
</tr>
</table>
<!-- FOOTER (simple, aligned with body) -->
<table role="presentation" width="100%">
<tr>
<td></td>
<td
class="email-container"
width="620"
style="width: 620px; max-width: 620px; margin: 0 auto;"
>
<table
role="presentation"
width="100%"
class="email-footer"
style="background-color: #ffffff; padding: 0 40px 16px 40px;"
>
<tr>
<td
style="
border-top: 1px solid #e9e9e9;
padding: 24px;
font-size: 12px;
line-height: 1.6;
color: #979797;
text-align: left;
"
>
<p style="margin: 0;">
Plane Software, Inc.
</p>
</td>
</tr>
</table>
</td>
<td></td>
</tr>
</table>
</center>
<base target="_blank" />
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -98,7 +98,7 @@ export const AuthRoot = observer(function AuthRoot() {
}
if (currentAuthMode === EAuthModes.SIGN_IN) {
if (response.is_password_autoset && isSMTPConfigured && isMagicLoginEnabled) {
if (isSMTPConfigured && isMagicLoginEnabled && response.status === "MAGIC_CODE") {
setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email);
} else if (isEmailPasswordEnabled) {
@@ -109,7 +109,7 @@ export const AuthRoot = observer(function AuthRoot() {
setErrorInfo(errorhandler);
}
} else {
if (isSMTPConfigured && isMagicLoginEnabled) {
if (isSMTPConfigured && isMagicLoginEnabled && response.status === "MAGIC_CODE") {
setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email);
} else if (isEmailPasswordEnabled) {
@@ -119,6 +119,7 @@ export const AuthRoot = observer(function AuthRoot() {
setErrorInfo(errorhandler);
}
}
return;
})
.catch((error) => {
const errorhandler = authErrorHandler(error?.error_code?.toString(), data?.email || undefined);
+1
View File
@@ -19,6 +19,7 @@ export interface IEmailCheckData {
}
export interface IEmailCheckResponse {
status: "MAGIC_CODE" | "CREDENTIAL";
is_password_autoset: boolean;
existing: boolean;
}
-1
View File
@@ -50,7 +50,6 @@
"react-hook-form": "7.51.5",
"react-popper": "^2.3.0",
"react-router": "catalog:",
"react-router-dom": "catalog:",
"swr": "catalog:",
"uuid": "catalog:"
},
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/navigation";
// plane package imports
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { Tabs } from "@plane/propel/tabs";
@@ -11,7 +11,6 @@ import { cn } from "@plane/utils";
import AnalyticsFilterActions from "@/components/analytics/analytics-filter-actions";
import { PageHead } from "@/components/core/page-title";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useProject } from "@/hooks/store/use-project";
import { useWorkspace } from "@/hooks/store/use-workspace";
@@ -120,7 +119,6 @@ function AnalyticsPage({ params }: Route.ComponentProps) {
label: "Create a project",
onClick: () => {
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
},
disabled: !canPerformEmptyStateActions,
},
@@ -30,7 +30,7 @@ export const ProjectWorkItemDetailsHeader = observer(function ProjectWorkItemDet
return (
<>
{projectPreferences.navigationMode === "horizontal" && (
{projectPreferences.navigationMode === "TABBED" && (
<div className="z-20">
<Row className="h-header flex gap-2 w-full items-center border-b border-subtle bg-surface-1">
<div className="flex items-center gap-2 divide-x divide-subtle h-full w-full">
@@ -23,7 +23,7 @@ function ProjectLayout({ params }: Route.ComponentProps) {
return (
<>
{projectPreferences.navigationMode === "horizontal" && (
{projectPreferences.navigationMode === "TABBED" && (
<div className="z-20">
<Row className="h-header flex gap-2 w-full items-center border-b border-subtle bg-surface-1">
<div className="flex items-center gap-2 divide-x divide-subtle h-full w-full">
@@ -2,7 +2,7 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { useParams, useRouter, useSearchParams } from "next/navigation";
// constants
import { EPageAccess, PROJECT_PAGE_TRACKER_EVENTS, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { EPageAccess } from "@plane/constants";
// plane types
import { Button } from "@plane/propel/button";
import { PageIcon } from "@plane/propel/icons";
@@ -12,7 +12,6 @@ import type { TPage } from "@plane/types";
import { Breadcrumbs, Header } from "@plane/ui";
// helpers
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useProject } from "@/hooks/store/use-project";
// plane web imports
@@ -40,23 +39,10 @@ export const PagesListHeader = observer(function PagesListHeader() {
await createPage(payload)
.then((res) => {
captureSuccess({
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
payload: {
id: res?.id,
state: "SUCCESS",
},
});
const pageId = `/${workspaceSlug}/projects/${currentProjectDetails?.id}/pages/${res?.id}`;
router.push(pageId);
})
.catch((err) => {
captureError({
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
payload: {
state: "ERROR",
},
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -86,13 +72,7 @@ export const PagesListHeader = observer(function PagesListHeader() {
</Header.LeftItem>
{canCurrentUserCreatePage && (
<Header.RightItem>
<Button
variant="primary"
size="lg"
onClick={handleCreatePage}
loading={isCreatingPage}
data-ph-element={PROJECT_TRACKER_ELEMENTS.CREATE_HEADER_BUTTON}
>
<Button variant="primary" size="lg" onClick={handleCreatePage} loading={isCreatingPage}>
{isCreatingPage ? "Adding" : "Add page"}
</Button>
</Header.RightItem>
@@ -1,12 +1,9 @@
import { useTheme } from "next-themes";
// plane imports
import { HEADER_GITHUB_ICON, GITHUB_REDIRECTED_TRACKER_EVENT } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// assets
import githubBlackImage from "@/app/assets/logos/github-black.png?url";
import githubWhiteImage from "@/app/assets/logos/github-white.png?url";
// helpers
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
export function StarUsOnGitHubLink() {
// plane hooks
@@ -18,17 +15,6 @@ export function StarUsOnGitHubLink() {
return (
<a
aria-label={t("home.star_us_on_github")}
onClick={() =>
captureElementAndEvent({
element: {
elementName: HEADER_GITHUB_ICON,
},
event: {
eventName: GITHUB_REDIRECTED_TRACKER_EVENT,
state: "SUCCESS",
},
})
}
className="flex flex-shrink-0 items-center gap-1.5 rounded-sm bg-layer-2 px-3 py-1.5"
href="https://github.com/makeplane/plane"
target="_blank"
@@ -8,8 +8,6 @@ import { SettingsMobileNav } from "@/components/settings/mobile";
// plane imports
import { WORKSPACE_SETTINGS_ACCESS } from "@plane/constants";
import type { EUserWorkspaceRoles } from "@plane/types";
// plane web components
import { WorkspaceSettingsRightSidebar } from "@/plane-web/components/workspace/right-sidebar";
// hooks
import { useUserPermissions } from "@/hooks/store/user";
// local components
@@ -48,7 +46,6 @@ const WorkspaceSettingLayout = observer(function WorkspaceSettingLayout({ params
<div className="w-full h-full overflow-y-scroll md:pt-page-y">
<Outlet />
</div>
<WorkspaceSettingsRightSidebar workspaceSlug={workspaceSlug} />
</div>
)}
</div>
@@ -1,12 +1,7 @@
import { useState } from "react";
import { observer } from "mobx-react";
// types
import {
EUserPermissions,
EUserPermissionsLevel,
MEMBER_TRACKER_ELEMENTS,
MEMBER_TRACKER_EVENTS,
} from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { SearchIcon } from "@plane/propel/icons";
@@ -20,8 +15,6 @@ import { PageHead } from "@/components/core/page-title";
import { MemberListFiltersDropdown } from "@/components/project/dropdowns/filters/member-list";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { WorkspaceMembersList } from "@/components/workspace/settings/members-list";
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useMember } from "@/hooks/store/use-member";
import { useWorkspace } from "@/hooks/store/use-workspace";
@@ -42,7 +35,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
const {
workspace: { workspaceMemberIds, inviteMembersToWorkspace, filtersStore },
} = useMember();
const { currentWorkspace, mutateWorkspaceMembersActivity } = useWorkspace();
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
// derived values
@@ -55,17 +48,9 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
const handleWorkspaceInvite = async (data: IWorkspaceBulkInviteFormData) => {
try {
await inviteMembersToWorkspace(workspaceSlug, data);
void mutateWorkspaceMembersActivity(workspaceSlug);
setInviteModal(false);
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.invite,
payload: {
emails: data.emails.map((email) => email.email),
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
@@ -77,14 +62,6 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
const err = error as Error & { error?: string };
message = err.error;
}
captureError({
eventName: MEMBER_TRACKER_EVENTS.invite,
payload: {
emails: data.emails.map((email) => email.email),
},
error: error as Error,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -154,12 +131,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
/>
<MembersActivityButton workspaceSlug={workspaceSlug} />
{canPerformWorkspaceAdminActions && (
<Button
variant="primary"
size="lg"
onClick={() => setInviteModal(true)}
data-ph-element={MEMBER_TRACKER_ELEMENTS.HEADER_ADD_BUTTON}
>
<Button variant="primary" size="lg" onClick={() => setInviteModal(true)}>
{t("workspace_settings.settings.members.add_member")}
</Button>
)}
@@ -1,7 +1,7 @@
import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IWebhook } from "@plane/types";
// ui
@@ -11,7 +11,6 @@ import { PageHead } from "@/components/core/page-title";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "@/components/web-hooks";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useWebhook } from "@/hooks/store/use-webhook";
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUserPermissions } from "@/hooks/store/user";
@@ -55,12 +54,6 @@ function WebhookDetailsPage({ params }: Route.ComponentProps) {
try {
await updateWebhook(workspaceSlug, formData.id, payload);
captureSuccess({
eventName: WORKSPACE_SETTINGS_TRACKER_EVENTS.webhook_updated,
payload: { webhook: formData.id },
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
@@ -68,12 +61,6 @@ function WebhookDetailsPage({ params }: Route.ComponentProps) {
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
captureError({
eventName: WORKSPACE_SETTINGS_TRACKER_EVENTS.webhook_updated,
payload: { webhook: formData.id },
error: error as Error,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// plane imports
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
import { EmptyStateCompact } from "@plane/propel/empty-state";
@@ -13,7 +13,6 @@ import { SettingsHeading } from "@/components/settings/heading";
import { WebhookSettingsLoader } from "@/components/ui/loader/settings/web-hook";
import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useWebhook } from "@/hooks/store/use-webhook";
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUserPermissions } from "@/hooks/store/user";
@@ -72,9 +71,6 @@ function WebhooksListPage({ params }: Route.ComponentProps) {
button={{
label: t("workspace_settings.settings.webhooks.add_webhook"),
onClick: () => {
captureClick({
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.HEADER_ADD_WEBHOOK_BUTTON,
});
setShowCreateWebhookModal(true);
},
}}
@@ -94,9 +90,6 @@ function WebhooksListPage({ params }: Route.ComponentProps) {
{
label: t("settings_empty_state.webhooks.cta_primary"),
onClick: () => {
captureClick({
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_WEBHOOK_BUTTON,
});
setShowCreateWebhookModal(true);
},
},
@@ -1,8 +1,7 @@
import React, { useState } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// plane imports
import { PROFILE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// component
import { EmptyStateCompact } from "@plane/propel/empty-state";
@@ -14,7 +13,6 @@ import { SettingsHeading } from "@/components/settings/heading";
import { APITokenSettingsLoader } from "@/components/ui/loader/settings/api-token";
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
// store hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useWorkspace } from "@/hooks/store/use-workspace";
const apiTokenService = new APITokenService();
@@ -51,9 +49,6 @@ function ApiTokensPage() {
button={{
label: t("workspace_settings.settings.api_tokens.add_token"),
onClick: () => {
captureClick({
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.HEADER_ADD_PAT_BUTTON,
});
setIsCreateTokenModalOpen(true);
},
}}
@@ -72,9 +67,6 @@ function ApiTokensPage() {
button={{
label: t("workspace_settings.settings.api_tokens.add_token"),
onClick: () => {
captureClick({
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.HEADER_ADD_PAT_BUTTON,
});
setIsCreateTokenModalOpen(true);
},
}}
@@ -89,9 +81,6 @@ function ApiTokensPage() {
{
label: t("settings_empty_state.tokens.cta_primary"),
onClick: () => {
captureClick({
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_PAT_BUTTON,
});
setIsCreateTokenModalOpen(true);
},
},
+3 -25
View File
@@ -1,11 +1,11 @@
import React, { useState } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import useSWR, { mutate } from "swr";
import { CheckCircle2 } from "lucide-react";
// plane imports
import { ROLE, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS, GROUP_WORKSPACE_TRACKER_EVENT } from "@plane/constants";
import { ROLE } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import { Button } from "@plane/propel/button";
@@ -19,9 +19,7 @@ import emptyInvitation from "@/app/assets/empty-state/invitation.svg?url";
import { EmptyState } from "@/components/common/empty-state";
import { WorkspaceLogo } from "@/components/workspace/logo";
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
// helpers
// hooks
import { captureError, captureSuccess, joinEventGroup } from "@/helpers/event-tracker.helper";
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUser, useUserProfile } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
@@ -79,18 +77,6 @@ function UserInvitationsPage() {
const firstInviteId = invitationsRespond[0];
const invitation = invitations?.find((i) => i.id === firstInviteId);
const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace;
if (redirectWorkspace?.id) {
joinEventGroup(GROUP_WORKSPACE_TRACKER_EVENT, redirectWorkspace?.id, {
date: new Date().toDateString(),
workspace_id: redirectWorkspace?.id,
});
}
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.accept,
payload: {
member_id: invitation?.id,
},
});
updateUserProfile({ last_workspace_id: redirectWorkspace?.id })
.then(() => {
setIsJoiningWorkspaces(false);
@@ -107,14 +93,7 @@ function UserInvitationsPage() {
setIsJoiningWorkspaces(false);
});
})
.catch((err) => {
captureError({
eventName: MEMBER_TRACKER_EVENTS.accept,
payload: {
member_id: invitationsRespond?.[0],
},
error: err,
});
.catch((_err) => {
setToast({
type: TOAST_TYPE.ERROR,
title: t("error"),
@@ -183,7 +162,6 @@ function UserInvitationsPage() {
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
loading={isJoiningWorkspaces}
data-ph-element={MEMBER_TRACKER_ELEMENTS.ACCEPT_INVITATION_BUTTON}
>
{t("accept_and_join")}
</Button>
+1 -7
View File
@@ -29,10 +29,6 @@ const ChatSupportModal = lazy(function ChatSupportModal() {
return import("@/components/global/chat-support-modal");
});
const PostHogProvider = lazy(function PostHogProvider() {
return import("@/lib/posthog-provider");
});
export interface IAppProvider {
children: React.ReactNode;
}
@@ -52,9 +48,7 @@ export function AppProvider(props: IAppProvider) {
<InstanceWrapper>
<Suspense>
<ChatSupportModal />
<PostHogProvider>
<SWRConfig value={WEB_SWR_CONFIG}>{children}</SWRConfig>
</PostHogProvider>
<SWRConfig value={WEB_SWR_CONFIG}>{children}</SWRConfig>
</Suspense>
</InstanceWrapper>
</StoreWrapper>
@@ -12,6 +12,6 @@ export function CommonProjectBreadcrumbs(props: TCommonProjectBreadcrumbProps) {
// preferences
const { preferences: projectPreferences } = useProjectNavigationPreferences();
if (projectPreferences.navigationMode === "horizontal") return null;
if (projectPreferences.navigationMode === "TABBED") return null;
return <ProjectBreadcrumb workspaceSlug={workspaceSlug} projectId={projectId} />;
}
@@ -16,7 +16,7 @@ export const ExtendedAppHeader = observer(function ExtendedAppHeader(props: { he
// store hooks
const { sidebarCollapsed } = useAppTheme();
// derived values
const shouldShowSidebarToggleButton = projectPreferences.navigationMode === "accordion" || (!projectId && !workItem);
const shouldShowSidebarToggleButton = projectPreferences.navigationMode === "ACCORDION" || (!projectId && !workItem);
return (
<>
@@ -10,8 +10,6 @@ import IssuesTour from "@/app/assets/onboarding/issues.webp?url";
import ModulesTour from "@/app/assets/onboarding/modules.webp?url";
import PagesTour from "@/app/assets/onboarding/pages.webp?url";
import ViewsTour from "@/app/assets/onboarding/views.webp?url";
// helpers
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useUser } from "@/hooks/store/user";
@@ -107,9 +105,6 @@ export const TourRoot = observer(function TourRoot(props: TOnboardingTourProps)
<Button
variant="primary"
onClick={() => {
captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.START_BUTTON,
});
setStep("work-items");
}}
>
@@ -119,9 +114,6 @@ export const TourRoot = observer(function TourRoot(props: TOnboardingTourProps)
type="button"
className="bg-transparent text-11 font-medium text-accent-primary outline-subtle-1"
onClick={() => {
captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.SKIP_BUTTON,
});
onComplete();
}}
>
@@ -170,9 +162,6 @@ export const TourRoot = observer(function TourRoot(props: TOnboardingTourProps)
<Button
variant="primary"
onClick={() => {
captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.CREATE_PROJECT_BUTTON,
});
onComplete();
toggleCreateProjectModal(true);
}}
@@ -2,10 +2,7 @@ import { useState, useEffect, useRef } from "react";
import { observer } from "mobx-react";
import { LockKeyhole, LockKeyholeOpen } from "lucide-react";
// plane imports
import { PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
import { Tooltip } from "@plane/propel/tooltip";
// helpers
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { usePageOperations } from "@/hooks/use-page-operations";
// store
@@ -79,7 +76,6 @@ export const PageLockControl = observer(function PageLockControl({ page }: Props
<button
type="button"
onClick={toggleLock}
data-ph-element={PROJECT_PAGE_TRACKER_ELEMENTS.LOCK_BUTTON}
className="flex-shrink-0 size-6 grid place-items-center rounded-sm text-secondary hover:text-primary hover:bg-layer-1 transition-colors"
aria-label="Lock"
>
@@ -92,7 +88,6 @@ export const PageLockControl = observer(function PageLockControl({ page }: Props
<button
type="button"
onClick={toggleLock}
data-ph-element={PROJECT_PAGE_TRACKER_ELEMENTS.LOCK_BUTTON}
className="h-6 flex items-center gap-1 px-2 rounded-sm text-accent-primary bg-accent-primary/20 hover:bg-accent-primary/30 transition-colors"
aria-label="Locked"
>
@@ -1,4 +1,3 @@
import type { FC } from "react";
import { Controller, useFormContext } from "react-hook-form";
// plane imports
import { NETWORK_CHOICES, ETabIndices } from "@plane/constants";
@@ -79,7 +78,7 @@ function ProjectAttributes(props: Props) {
placeholder={t("lead")}
multiple={false}
buttonVariant="border-with-text"
tabIndex={5}
tabIndex={getIndex("lead")}
/>
</div>
);
@@ -1,24 +1,21 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { FormProvider, useForm } from "react-hook-form";
import { PROJECT_TRACKER_EVENTS, RANDOM_EMOJI_CODES } from "@plane/constants";
// plane imports
import { useTranslation } from "@plane/i18n";
// ui
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EFileAssetType } from "@plane/types";
import type { IProject } from "@plane/types";
// constants
// components
import ProjectCommonAttributes from "@/components/project/create/common-attributes";
import ProjectCreateHeader from "@/components/project/create/header";
import ProjectCreateButtons from "@/components/project/create/project-create-buttons";
// hooks
import { DEFAULT_COVER_IMAGE_URL, getCoverImageType, uploadCoverImage } from "@/helpers/cover-image.helper";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { getCoverImageType, uploadCoverImage } from "@/helpers/cover-image.helper";
import { useProject } from "@/hooks/store/use-project";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web types
import type { TProject } from "@/plane-web/types/projects";
import ProjectAttributes from "./attributes";
import { ProjectAttributes } from "./attributes";
import { getProjectFormValues } from "./utils";
export type TCreateProjectFormProps = {
@@ -37,7 +34,7 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
const { t } = useTranslation();
const { addProjectToFavorites, createProject, updateProject } = useProject();
// states
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
const [shouldAutoSyncIdentifier, setShouldAutoSyncIdentifier] = useState(true);
// form info
const methods = useForm<TProject>({
defaultValues: { ...getProjectFormValues(), ...data },
@@ -98,12 +95,6 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
await updateCoverImageStatus(res.id, coverImage);
await updateProject(workspaceSlug.toString(), res.id, { cover_image_url: coverImage });
}
captureSuccess({
eventName: PROJECT_TRACKER_EVENTS.create,
payload: {
identifier: formData.identifier,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("success"),
@@ -117,13 +108,6 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
})
.catch((err) => {
try {
captureError({
eventName: PROJECT_TRACKER_EVENTS.create,
payload: {
identifier: formData.identifier,
},
});
// Handle the new error format where codes are nested in arrays under field names
const errorData = err?.data ?? {};
@@ -167,7 +151,7 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
const handleClose = () => {
onClose();
setIsChangeInIdentifierRequired(true);
setShouldAutoSyncIdentifier(true);
setTimeout(() => {
reset();
}, 300);
@@ -182,8 +166,8 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
<ProjectCommonAttributes
setValue={setValue}
isMobile={isMobile}
isChangeInIdentifierRequired={isChangeInIdentifierRequired}
setIsChangeInIdentifierRequired={setIsChangeInIdentifierRequired}
shouldAutoSyncIdentifier={shouldAutoSyncIdentifier}
setShouldAutoSyncIdentifier={setShouldAutoSyncIdentifier}
/>
<ProjectAttributes isMobile={isMobile} />
</div>
@@ -1,11 +1,6 @@
type TProjectTemplateDropdownSize = "xs" | "sm";
export type TProjectTemplateSelect = {
disabled?: boolean;
size?: TProjectTemplateDropdownSize;
placeholder?: string;
dropDownContainerClassName?: string;
handleModalClose: () => void;
onClick?: () => void;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -4,8 +4,6 @@ import {
SUBSCRIPTION_REDIRECTION_URLS,
SUBSCRIPTION_WITH_BILLING_FREQUENCY,
TALK_TO_SALES_URL,
WORKSPACE_SETTINGS_TRACKER_ELEMENTS,
WORKSPACE_SETTINGS_TRACKER_EVENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
@@ -16,7 +14,6 @@ import { getSubscriptionName } from "@plane/utils";
import { DiscountInfo } from "@/components/license/modal/card/discount-info";
import type { TPlanDetail } from "@/constants/plans";
// local imports
import { captureSuccess } from "@/helpers/event-tracker.helper";
import { PlanFrequencyToggle } from "./frequency-toggle";
type TPlanDetailProps = {
@@ -45,12 +42,6 @@ export const PlanDetail = observer(function PlanDetail(props: TPlanDetailProps)
const frequency = billingFrequency ?? "year";
// Get the redirection URL based on the subscription type and billing frequency
const redirectUrl = SUBSCRIPTION_REDIRECTION_URLS[subscriptionType][frequency] ?? TALK_TO_SALES_URL;
captureSuccess({
eventName: WORKSPACE_SETTINGS_TRACKER_EVENTS.upgrade_plan_redirected,
payload: {
subscriptionType,
},
});
// Open the URL in a new tab
window.open(redirectUrl, "_blank");
};
@@ -103,17 +94,7 @@ export const PlanDetail = observer(function PlanDetail(props: TPlanDetailProps)
{/* Subscription button */}
<div className="flex flex-col gap-1 py-3 items-start">
<Button
variant="primary"
size="lg"
onClick={handleRedirection}
className="w-full"
data-ph-element={
isSubscriptionActive
? WORKSPACE_SETTINGS_TRACKER_ELEMENTS.BILLING_UPGRADE_BUTTON(subscriptionType)
: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.BILLING_TALK_TO_SALES_BUTTON
}
>
<Button variant="primary" size="lg" onClick={handleRedirection} className="w-full">
{isSubscriptionActive ? `Upgrade to ${subscriptionName}` : t("common.upgrade_cta.talk_to_sales")}
</Button>
</div>
@@ -1 +0,0 @@
export * from "./root";
@@ -1,10 +0,0 @@
import { observer } from "mobx-react";
type TWorkspaceSettingsRightSidebarProps = { workspaceSlug: string };
export const WorkspaceSettingsRightSidebar = observer(function WorkspaceSettingsRightSidebar(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
props: TWorkspaceSettingsRightSidebarProps
) {
return <></>;
});
-16
View File
@@ -1,15 +1,5 @@
// types
import {
CYCLE_TRACKER_ELEMENTS,
MODULE_TRACKER_ELEMENTS,
PROJECT_PAGE_TRACKER_ELEMENTS,
PROJECT_TRACKER_ELEMENTS,
PROJECT_VIEW_TRACKER_ELEMENTS,
WORK_ITEM_TRACKER_ELEMENTS,
} from "@plane/constants";
import type { TCommandPaletteActionList, TCommandPaletteShortcut, TCommandPaletteShortcutList } from "@plane/types";
// store
import { captureClick } from "@/helpers/event-tracker.helper";
import { store } from "@/lib/store-context";
export const getGlobalShortcutsList: () => TCommandPaletteActionList = () => {
@@ -21,7 +11,6 @@ export const getGlobalShortcutsList: () => TCommandPaletteActionList = () => {
description: "Create a new work item in the current project",
action: () => {
toggleCreateIssueModal(true);
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_BUTTON });
},
},
};
@@ -36,7 +25,6 @@ export const getWorkspaceShortcutsList: () => TCommandPaletteActionList = () =>
description: "Create a new project in the current workspace",
action: () => {
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_CREATE_BUTTON });
},
},
};
@@ -57,7 +45,6 @@ export const getProjectShortcutsList: () => TCommandPaletteActionList = () => {
description: "Create a new page in the current project",
action: () => {
toggleCreatePageModal({ isOpen: true });
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_CREATE_BUTTON });
},
},
m: {
@@ -65,7 +52,6 @@ export const getProjectShortcutsList: () => TCommandPaletteActionList = () => {
description: "Create a new module in the current project",
action: () => {
toggleCreateModuleModal(true);
captureClick({ elementName: MODULE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM });
},
},
q: {
@@ -73,7 +59,6 @@ export const getProjectShortcutsList: () => TCommandPaletteActionList = () => {
description: "Create a new cycle in the current project",
action: () => {
toggleCreateCycleModal(true);
captureClick({ elementName: CYCLE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM });
},
},
v: {
@@ -81,7 +66,6 @@ export const getProjectShortcutsList: () => TCommandPaletteActionList = () => {
description: "Create a new view in the current project",
action: () => {
toggleCreateViewModal(true);
captureClick({ elementName: PROJECT_VIEW_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM });
},
},
backspace: {
@@ -5,14 +5,11 @@ import { Controller, useForm } from "react-hook-form";
// icons
import { CircleCheck } from "lucide-react";
// plane imports
import { AUTH_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Input } from "@plane/ui";
import { cn, checkEmailValidity } from "@plane/utils";
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import useTimer from "@/hooks/use-timer";
// services
@@ -59,12 +56,6 @@ export const ForgotPasswordForm = observer(function ForgotPasswordForm() {
email: formData.email,
})
.then(() => {
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.forgot_password,
payload: {
email: formData.email,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("auth.forgot_password.toast.success.title"),
@@ -73,12 +64,6 @@ export const ForgotPasswordForm = observer(function ForgotPasswordForm() {
setResendCodeTimer(30);
})
.catch((err) => {
captureError({
eventName: AUTH_TRACKER_EVENTS.forgot_password,
payload: {
email: formData.email,
},
});
setToast({
type: TOAST_TYPE.ERROR,
title: t("auth.forgot_password.toast.error.title"),
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
@@ -15,8 +15,6 @@ import { ForgotPasswordPopover } from "@/components/account/auth-forms/forgot-pa
// constants
// helpers
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// services
import { AuthService } from "@/services/auth.service";
@@ -154,15 +152,6 @@ export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props)
: true;
if (isPasswordValid) {
setIsSubmitting(true);
captureSuccess({
eventName:
mode === EAuthModes.SIGN_IN
? AUTH_TRACKER_EVENTS.sign_in_with_password
: AUTH_TRACKER_EVENTS.sign_up_with_password,
payload: {
email: passwordFormData.email,
},
});
if (formRef.current) formRef.current.submit(); // Manually submit the form if the condition is met
} else {
setBannerMessage(true);
@@ -170,15 +159,6 @@ export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props)
}}
onError={() => {
setIsSubmitting(false);
captureError({
eventName:
mode === EAuthModes.SIGN_IN
? AUTH_TRACKER_EVENTS.sign_in_with_password
: AUTH_TRACKER_EVENTS.sign_up_with_password,
payload: {
email: passwordFormData.email,
},
});
}}
>
<input type="hidden" name="csrfmiddlewaretoken" />
@@ -5,15 +5,13 @@ import { useSearchParams } from "next/navigation";
// icons
import { Eye, EyeOff } from "lucide-react";
// plane imports
import { AUTH_TRACKER_ELEMENTS, AUTH_TRACKER_EVENTS, E_PASSWORD_STRENGTH } from "@plane/constants";
import { E_PASSWORD_STRENGTH } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Input, PasswordStrengthIndicator } from "@plane/ui";
// components
import { getPasswordStrength } from "@plane/utils";
// helpers
import { captureError, captureSuccess, captureView } from "@/helpers/event-tracker.helper";
// hooks
import { useUser } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
@@ -60,12 +58,6 @@ export const SetPasswordForm = observer(function SetPasswordForm() {
// hooks
const { data: user, handleSetPassword } = useUser();
useEffect(() => {
captureView({
elementName: AUTH_TRACKER_ELEMENTS.SET_PASSWORD_FORM,
});
}, []);
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
@@ -92,9 +84,6 @@ export const SetPasswordForm = observer(function SetPasswordForm() {
e.preventDefault();
if (!csrfToken) throw new Error("csrf token not found");
await handleSetPassword(csrfToken, { password: passwordFormData.password });
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.password_created,
});
router.push("/");
} catch (error: unknown) {
let message = undefined;
@@ -102,9 +91,6 @@ export const SetPasswordForm = observer(function SetPasswordForm() {
const err = error as Error & { error?: string };
message = err.error;
}
captureError({
eventName: AUTH_TRACKER_EVENTS.password_created,
});
setToast({
type: TOAST_TYPE.ERROR,
title: t("common.errors.default.title"),
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { CircleCheck, XCircle } from "lucide-react";
import { API_BASE_URL, AUTH_TRACKER_ELEMENTS, AUTH_TRACKER_EVENTS } from "@plane/constants";
import { API_BASE_URL } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { Input, Spinner } from "@plane/ui";
@@ -8,7 +8,6 @@ import { Input, Spinner } from "@plane/ui";
// helpers
import { EAuthModes } from "@/helpers/authentication.helper";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import useTimer from "@/hooks/use-timer";
// services
import { AuthService } from "@/services/auth.service";
@@ -59,22 +58,10 @@ export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {
setResendCodeTimer(defaultResetTimerValue);
handleFormChange("code", uniqueCode?.code || "");
setIsRequestingNewCode(false);
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.new_code_requested,
payload: {
email: email,
},
});
} catch {
setResendCodeTimer(0);
console.error("Error while requesting new code");
setIsRequestingNewCode(false);
captureError({
eventName: AUTH_TRACKER_EVENTS.new_code_requested,
payload: {
email: email,
},
});
}
};
@@ -93,22 +80,9 @@ export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "magic-sign-in" : "magic-sign-up"}/`}
onSubmit={() => {
setIsSubmitting(true);
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.code_verify,
payload: {
state: "SUCCESS",
first_time: !isExistingEmail,
},
});
}}
onError={() => {
setIsSubmitting(false);
captureError({
eventName: AUTH_TRACKER_EVENTS.code_verify,
payload: {
state: "FAILED",
},
});
}}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
@@ -163,7 +137,6 @@ export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {
</p>
<button
type="button"
data-ph-element={AUTH_TRACKER_ELEMENTS.REQUEST_NEW_CODE}
onClick={() => generateNewCode(uniqueCodeFormData.email)}
className={
isRequestNewCodeDisabled
@@ -182,14 +155,7 @@ export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {
</div>
<div className="space-y-2.5">
<Button
type="submit"
variant="primary"
className="w-full"
size="xl"
disabled={isButtonDisabled}
data-ph-element={AUTH_TRACKER_ELEMENTS.VERIFY_CODE}
>
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={isButtonDisabled}>
{isRequestingNewCode ? (
t("auth.common.unique_code.sending_code")
) : isSubmitting ? (
@@ -1,5 +1,4 @@
import { useState } from "react";
import { PROFILE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { Button } from "@plane/propel/button";
@@ -7,7 +6,6 @@ import { TrashIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useUser } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
@@ -36,9 +34,6 @@ export function DeactivateAccountModal(props: Props) {
await deactivateAccount()
.then(() => {
captureSuccess({
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.deactivate_account,
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
@@ -50,9 +45,6 @@ export function DeactivateAccountModal(props: Props) {
return;
})
.catch((err: any) => {
captureError({
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.deactivate_account,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -1,8 +1,6 @@
import type { FC } from "react";
import { useState } from "react";
import { mutate } from "swr";
// types
import { PROFILE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { APITokenService } from "@plane/services";
@@ -11,7 +9,6 @@ import type { IApiToken } from "@plane/types";
import { AlertModalCore } from "@plane/ui";
// fetch-keys
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
type Props = {
isOpen: boolean;
@@ -50,32 +47,18 @@ export function DeleteApiTokenModal(props: Props) {
(prevData) => (prevData ?? []).filter((token) => token.id !== tokenId),
false
);
captureSuccess({
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.pat_deleted,
payload: {
token: tokenId,
},
});
handleClose();
setDeleteLoading(false);
})
.catch((err) =>
.catch((err) => {
setToast({
type: TOAST_TYPE.ERROR,
title: t("workspace_settings.settings.api_tokens.delete.error.title"),
message: err?.message ?? t("workspace_settings.settings.api_tokens.delete.error.message"),
})
)
.catch((err) => {
captureError({
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.pat_deleted,
payload: {
token: tokenId,
},
error: err as Error,
});
})
.finally(() => setDeleteLoading(false));
setDeleteLoading(false);
});
};
return (
@@ -1,7 +1,6 @@
import React, { useState } from "react";
import { useState } from "react";
import { mutate } from "swr";
// plane imports
import { PROFILE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { APITokenService } from "@plane/services";
import type { IApiToken } from "@plane/types";
@@ -9,8 +8,6 @@ import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { renderFormattedDate, csvDownload } from "@plane/utils";
// constants
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// local imports
import { CreateApiTokenForm } from "./form";
import { GeneratedTokenDetails } from "./generated-token-details";
@@ -66,12 +63,6 @@ export function CreateApiTokenModal(props: Props) {
},
false
);
captureSuccess({
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.pat_created,
payload: {
token: res.id,
},
});
})
.catch((err) => {
setToast({
@@ -80,10 +71,6 @@ export function CreateApiTokenModal(props: Props) {
message: err.message || err.detail,
});
captureError({
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.pat_created,
});
throw err;
});
};
@@ -1,24 +1,16 @@
import React, { useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { ArchiveRestore } from "lucide-react";
// types
import {
PROJECT_AUTOMATION_MONTHS,
EUserPermissions,
EUserPermissionsLevel,
PROJECT_SETTINGS_TRACKER_ELEMENTS,
PROJECT_SETTINGS_TRACKER_EVENTS,
} from "@plane/constants";
import { PROJECT_AUTOMATION_MONTHS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import type { IProject } from "@plane/types";
// ui
import { CustomSelect, Loader, ToggleSwitch } from "@plane/ui";
// component
import { SelectMonthModal } from "@/components/automation";
// constants
// hooks
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
@@ -52,6 +44,14 @@ export const AutoArchiveAutomation = observer(function AutoArchiveAutomation(pro
return currentProjectDetails.archive_in !== 0;
}, [currentProjectDetails]);
const handleToggleArchive = async () => {
if (currentProjectDetails?.archive_in === 0) {
await handleChange({ archive_in: 1 });
} else {
await handleChange({ archive_in: 0 });
}
};
return (
<>
<SelectMonthModal
@@ -74,27 +74,7 @@ export const AutoArchiveAutomation = observer(function AutoArchiveAutomation(pro
</p>
</div>
</div>
<ToggleSwitch
value={autoArchiveStatus}
onChange={async () => {
if (currentProjectDetails?.archive_in === 0) {
await handleChange({ archive_in: 1 });
} else {
await handleChange({ archive_in: 0 });
}
captureElementAndEvent({
element: {
elementName: PROJECT_SETTINGS_TRACKER_ELEMENTS.AUTOMATIONS_ARCHIVE_TOGGLE_BUTTON,
},
event: {
eventName: PROJECT_SETTINGS_TRACKER_EVENTS.auto_archive_workitems,
state: "SUCCESS",
},
});
}}
size="sm"
disabled={!isAdmin}
/>
<ToggleSwitch value={autoArchiveStatus} onChange={handleToggleArchive} size="sm" disabled={!isAdmin} />
</div>
{currentProjectDetails ? (
@@ -1,17 +1,10 @@
import React, { useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { ArchiveX } from "lucide-react";
// types
import {
PROJECT_AUTOMATION_MONTHS,
EUserPermissions,
EUserPermissionsLevel,
EIconSize,
PROJECT_SETTINGS_TRACKER_ELEMENTS,
PROJECT_SETTINGS_TRACKER_EVENTS,
} from "@plane/constants";
import { PROJECT_AUTOMATION_MONTHS, EUserPermissions, EUserPermissionsLevel, EIconSize } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { StateGroupIcon, StatePropertyIcon } from "@plane/propel/icons";
import type { IProject } from "@plane/types";
@@ -21,7 +14,6 @@ import { CustomSelect, CustomSearchSelect, ToggleSwitch, Loader } from "@plane/u
import { SelectMonthModal } from "@/components/automation";
// constants
// hooks
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
import { useProject } from "@/hooks/store/use-project";
import { useProjectState } from "@/hooks/store/use-project-state";
import { useUserPermissions } from "@/hooks/store/user";
@@ -111,15 +103,6 @@ export const AutoCloseAutomation = observer(function AutoCloseAutomation(props:
} else {
await handleChange({ close_in: 0, default_state: null });
}
captureElementAndEvent({
element: {
elementName: PROJECT_SETTINGS_TRACKER_ELEMENTS.AUTOMATIONS_CLOSE_TOGGLE_BUTTON,
},
event: {
eventName: PROJECT_SETTINGS_TRACKER_EVENTS.auto_close_workitems,
state: "SUCCESS",
},
});
}}
size="sm"
disabled={!isAdmin}
@@ -5,10 +5,11 @@ import { useDropzone } from "react-dropzone";
import type { Control } from "react-hook-form";
import { Controller } from "react-hook-form";
import useSWR from "swr";
import { Tab, Popover } from "@headlessui/react";
import { Popover } from "@headlessui/react";
// plane imports
import { ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
import { Tabs } from "@plane/propel/tabs";
import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EFileAssetType } from "@plane/types";
@@ -80,6 +81,8 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
[hasUnsplashConfigured]
);
const enabledTabs = useMemo(() => tabOptions.filter((tab) => tab.isEnabled), [tabOptions]);
const { data: unsplashImages, error: unsplashError } = useSWR(
`UNSPLASH_IMAGES_${searchParams}`,
() => fileService.getUnsplashImages(searchParams),
@@ -191,25 +194,19 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
>
<div
ref={imagePickerRef}
className="flex h-96 w-80 flex-col overflow-auto rounded-sm border border-strong bg-surface-1 p-3 shadow-2xl md:h-[28rem] md:w-[36rem]"
className="flex h-96 w-80 flex-col overflow-auto rounded border border-subtle bg-surface-1 shadow-raised-200 md:h-[36rem] md:w-[36rem]"
>
<Tab.Group>
<Tab.List as="span" className="inline-block rounded-sm bg-layer-1 p-1">
{tabOptions.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`rounded-sm px-4 py-1 text-center text-13 outline-none transition-colors ${
selected ? "bg-accent-primary text-on-color" : "text-primary"
}`
}
>
<Tabs defaultValue={enabledTabs[0]?.key || "images"} className="flex h-full flex-col p-3">
<Tabs.List className="flex rounded bg-layer-3 p-1">
{enabledTabs.map((tab) => (
<Tabs.Trigger key={tab.key} value={tab.key} size="md">
{tab.title}
</Tab>
</Tabs.Trigger>
))}
</Tab.List>
<Tab.Panels className="vertical-scrollbar scrollbar-md h-full w-full flex-1 overflow-y-auto overflow-x-hidden">
<Tab.Panel className="mt-4 h-full w-full space-y-4">
<Tabs.Indicator />
</Tabs.List>
<div className="vertical-scrollbar scrollbar-sm p-3 mt-3 flex-1 overflow-y-auto overflow-x-hidden">
<Tabs.Content value="unsplash" className="h-full w-full space-y-4">
{(unsplashImages || !unsplashError) && (
<>
<div className="flex items-center gap-x-2">
@@ -276,8 +273,8 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
)}
</>
)}
</Tab.Panel>
<Tab.Panel className="mt-4 h-full w-full space-y-4">
</Tabs.Content>
<Tabs.Content value="images" className="h-full w-full space-y-4">
<div className="grid grid-cols-4 gap-4">
{Object.values(STATIC_COVER_IMAGES).map((imageUrl, index) => (
<div
@@ -293,8 +290,8 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
</div>
))}
</div>
</Tab.Panel>
<Tab.Panel className="mt-4 h-full w-full">
</Tabs.Content>
<Tabs.Content value="upload" className="h-full w-full">
<div className="flex h-full w-full flex-col gap-y-2">
<div className="flex w-full flex-1 items-center gap-3">
<div
@@ -357,13 +354,13 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
disabled={!image}
loading={isImageUploading}
>
{isImageUploading ? "Uploading..." : "Upload & Save"}
{isImageUploading ? "Uploading" : "Upload & Save"}
</Button>
</div>
</div>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</Tabs.Content>
</div>
</Tabs>
</div>
</Popover.Panel>
)}
@@ -1,16 +1,9 @@
import type { FC } from "react";
import React, { useEffect } from "react";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import { ArrowRight } from "lucide-react";
// Plane Imports
import {
CYCLE_TRACKER_EVENTS,
CYCLE_STATUS,
EUserPermissions,
EUserPermissionsLevel,
CYCLE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { CYCLE_STATUS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ChevronRightIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
@@ -19,7 +12,6 @@ import { getDate, renderFormattedPayloadDate } from "@plane/utils";
// components
import { DateRangeDropdown } from "@/components/dropdowns/date-range";
// hooks
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
import { useCycle } from "@/hooks/store/use-cycle";
import { useUserPermissions } from "@/hooks/store/user";
import { useTimeZoneConverter } from "@/hooks/use-timezone-converter";
@@ -64,37 +56,7 @@ export const CycleSidebarHeader = observer(function CycleSidebarHeader(props: Pr
const submitChanges = async (data: Partial<ICycle>) => {
if (!workspaceSlug || !projectId || !cycleDetails.id) return;
await updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleDetails.id.toString(), data)
.then(() => {
captureElementAndEvent({
element: {
elementName: CYCLE_TRACKER_ELEMENTS.RIGHT_SIDEBAR,
},
event: {
eventName: CYCLE_TRACKER_EVENTS.update,
state: "SUCCESS",
payload: {
id: cycleDetails.id,
},
},
});
})
.catch(() => {
captureElementAndEvent({
element: {
elementName: CYCLE_TRACKER_ELEMENTS.RIGHT_SIDEBAR,
},
event: {
eventName: CYCLE_TRACKER_EVENTS.update,
state: "ERROR",
payload: {
id: cycleDetails.id,
},
},
});
});
await updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleDetails.id.toString(), data);
};
useEffect(() => {
@@ -1,11 +1,9 @@
import { useState } from "react";
// ui
import { CYCLE_TRACKER_EVENTS } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle } from "@/hooks/store/use-cycle";
import { useAppRouter } from "@/hooks/use-app-router";
@@ -43,12 +41,6 @@ export function ArchiveCycleModal(props: Props) {
title: "Archive success",
message: "Your archives can be found in project archives.",
});
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.archive,
payload: {
id: cycleId,
},
});
onClose();
router.push(`/${workspaceSlug}/projects/${projectId}/cycles`);
return;
@@ -59,12 +51,6 @@ export function ArchiveCycleModal(props: Props) {
title: "Error!",
message: "Cycle could not be archived. Please try again.",
});
captureError({
eventName: CYCLE_TRACKER_EVENTS.archive,
payload: {
id: cycleId,
},
});
})
.finally(() => setIsArchiving(false));
};
@@ -2,14 +2,12 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { useParams, useSearchParams } from "next/navigation";
// types
import { PROJECT_ERROR_MESSAGES, CYCLE_TRACKER_EVENTS } from "@plane/constants";
import { PROJECT_ERROR_MESSAGES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { ICycle } from "@plane/types";
// ui
import { AlertModalCore } from "@plane/ui";
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useCycle } from "@/hooks/store/use-cycle";
import { useAppRouter } from "@/hooks/use-app-router";
@@ -48,12 +46,6 @@ export const CycleDeleteModal = observer(function CycleDeleteModal(props: ICycle
title: "Success!",
message: "Cycle deleted successfully.",
});
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.delete,
payload: {
id: cycle.id,
},
});
})
.catch((errors) => {
const isPermissionError = errors?.error === "You don't have the required permissions.";
@@ -65,13 +57,6 @@ export const CycleDeleteModal = observer(function CycleDeleteModal(props: ICycle
type: TOAST_TYPE.ERROR,
message: currentError.i18n_message && t(currentError.i18n_message),
});
captureError({
eventName: CYCLE_TRACKER_EVENTS.delete,
payload: {
id: cycle.id,
},
error: errors,
});
})
.finally(() => handleClose());
} catch {
@@ -1,17 +1,11 @@
import type { FC, MouseEvent } from "react";
import type { MouseEvent } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react";
import { useParams, usePathname, useSearchParams } from "next/navigation";
import { useForm } from "react-hook-form";
import { Eye, ArrowRight, CalendarDays } from "lucide-react";
// plane imports
import {
CYCLE_TRACKER_EVENTS,
EUserPermissions,
EUserPermissionsLevel,
IS_FAVORITE_MENU_OPEN,
CYCLE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, IS_FAVORITE_MENU_OPEN } from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { TransferIcon, WorkItemsIcon, MembersPropertyIcon } from "@plane/propel/icons";
@@ -25,7 +19,6 @@ import { DateRangeDropdown } from "@/components/dropdowns/date-range";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { MergedDateDisplay } from "@/components/dropdowns/merged-date";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle } from "@/hooks/store/use-cycle";
import { useMember } from "@/hooks/store/use-member";
import { useUserPermissions } from "@/hooks/store/user";
@@ -109,25 +102,11 @@ export const CycleListItemAction = observer(function CycleListItemAction(props:
e.preventDefault();
if (!workspaceSlug || !projectId) return;
const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
.then(() => {
const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then(
() => {
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.favorite,
payload: {
id: cycleId,
},
});
})
.catch((error) => {
captureError({
eventName: CYCLE_TRACKER_EVENTS.favorite,
payload: {
id: cycleId,
},
error,
});
});
}
);
setPromiseToast(addToFavoritePromise, {
loading: t("project_cycles.action.favorite.loading"),
@@ -146,24 +125,11 @@ export const CycleListItemAction = observer(function CycleListItemAction(props:
e.preventDefault();
if (!workspaceSlug || !projectId) return;
const removeFromFavoritePromise = removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
.then(() => {
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.unfavorite,
payload: {
id: cycleId,
},
});
})
.catch((error) => {
captureError({
eventName: CYCLE_TRACKER_EVENTS.unfavorite,
payload: {
id: cycleId,
},
error,
});
});
const removeFromFavoritePromise = removeCycleFromFavorites(
workspaceSlug?.toString(),
projectId.toString(),
cycleId
);
setPromiseToast(removeFromFavoritePromise, {
loading: t("project_cycles.action.unfavorite.loading"),
@@ -319,7 +285,6 @@ export const CycleListItemAction = observer(function CycleListItemAction(props:
)}
{isEditingAllowed && !cycleDetails.archived_at && (
<FavoriteStar
data-ph-element={CYCLE_TRACKER_ELEMENTS.LIST_ITEM}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
+1 -23
View File
@@ -1,14 +1,12 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { mutate } from "swr";
// types
import { CYCLE_TRACKER_EVENTS } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { CycleDateCheckData, ICycle, TCycleTabOptions } from "@plane/types";
// ui
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { renderFormattedPayloadDate } from "@plane/utils";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle } from "@/hooks/store/use-cycle";
import { useProject } from "@/hooks/store/use-project";
import useKeypress from "@/hooks/use-keypress";
@@ -62,12 +60,6 @@ export function CycleCreateUpdateModal(props: CycleModalProps) {
title: "Success!",
message: "Cycle created successfully.",
});
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.create,
payload: {
id: res.id,
},
});
})
.catch((err) => {
setToast({
@@ -75,10 +67,6 @@ export function CycleCreateUpdateModal(props: CycleModalProps) {
title: "Error!",
message: err?.detail ?? "Error in creating cycle. Please try again.",
});
captureError({
eventName: CYCLE_TRACKER_EVENTS.create,
error: err,
});
});
};
@@ -88,12 +76,6 @@ export function CycleCreateUpdateModal(props: CycleModalProps) {
const selectedProjectId = payload.project_id ?? projectId.toString();
await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
.then((res) => {
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.update,
payload: {
id: res.id,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
@@ -106,10 +88,6 @@ export function CycleCreateUpdateModal(props: CycleModalProps) {
title: "Error!",
message: err?.detail ?? "Error in updating cycle. Please try again.",
});
captureError({
eventName: CYCLE_TRACKER_EVENTS.update,
error: err,
});
});
};
@@ -1,24 +1,16 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { MoreHorizontal } from "lucide-react";
// ui
import {
CYCLE_TRACKER_EVENTS,
EUserPermissions,
EUserPermissionsLevel,
CYCLE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IconButton } from "@plane/propel/icon-button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TContextMenuItem } from "@plane/ui";
import { ContextMenu, CustomMenu } from "@plane/ui";
import { copyUrlToClipboard, cn } from "@plane/utils";
// helpers
// hooks
import { useCycleMenuItems } from "@/components/common/quick-actions-helper";
import { captureClick, captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle } from "@/hooks/store/use-cycle";
import { useUserPermissions } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
@@ -76,12 +68,6 @@ export const CycleQuickActions = observer(function CycleQuickActions(props: Prop
title: t("project_cycles.action.restore.success.title"),
message: t("project_cycles.action.restore.success.description"),
});
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.restore,
payload: {
id: cycleId,
},
});
router.push(`/${workspaceSlug}/projects/${projectId}/archives/cycles`);
})
.catch(() => {
@@ -90,12 +76,6 @@ export const CycleQuickActions = observer(function CycleQuickActions(props: Prop
title: t("project_cycles.action.restore.failed.title"),
message: t("project_cycles.action.restore.failed.description"),
});
captureError({
eventName: CYCLE_TRACKER_EVENTS.restore,
payload: {
id: cycleId,
},
});
});
const menuResult = useCycleMenuItems({
@@ -118,11 +98,7 @@ export const CycleQuickActions = observer(function CycleQuickActions(props: Prop
const CONTEXT_MENU_ITEMS = MENU_ITEMS.map(function CONTEXT_MENU_ITEMS(item) {
return {
...item,
action: () => {
captureClick({
elementName: CYCLE_TRACKER_ELEMENTS.CONTEXT_MENU,
});
item.action();
},
};
@@ -170,9 +146,6 @@ export const CycleQuickActions = observer(function CycleQuickActions(props: Prop
<CustomMenu.MenuItem
key={item.key}
onClick={() => {
captureClick({
elementName: CYCLE_TRACKER_ELEMENTS.QUICK_ACTIONS,
});
item.action();
}}
className={cn(
@@ -1,13 +1,10 @@
import type { FC } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
// ui
import { PROJECT_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useProjectEstimates } from "@/hooks/store/estimates";
import { useEstimate } from "@/hooks/store/estimates/use-estimate";
import { useProject } from "@/hooks/store/use-project";
@@ -34,18 +31,11 @@ export const DeleteEstimateModal = observer(function DeleteEstimateModal(props:
try {
if (!workspaceSlug || !projectId || !estimateId) return;
setButtonLoader(true);
await deleteEstimate(workspaceSlug, projectId, estimateId);
if (areEstimateEnabledByProjectId(projectId)) {
await updateProject(workspaceSlug, projectId, { estimate: null });
}
setButtonLoader(false);
captureSuccess({
eventName: PROJECT_SETTINGS_TRACKER_EVENTS.estimate_deleted,
payload: {
id: estimateId,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Estimate deleted",
@@ -54,12 +44,6 @@ export const DeleteEstimateModal = observer(function DeleteEstimateModal(props:
handleClose();
} catch (error) {
setButtonLoader(false);
captureError({
eventName: PROJECT_SETTINGS_TRACKER_EVENTS.estimate_deleted,
payload: {
id: estimateId,
},
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Estimate creation failed",
@@ -1,12 +1,8 @@
import type { FC } from "react";
import { useTheme } from "next-themes";
import { PROJECT_SETTINGS_TRACKER_ELEMENTS, PROJECT_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
// plane imports
import { useTranslation } from "@plane/i18n";
// components
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
// helpers
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
type TEstimateEmptyScreen = {
onButtonClick: () => void;
@@ -30,15 +26,6 @@ export function EstimateEmptyScreen(props: TEstimateEmptyScreen) {
text: t("project_settings.empty_state.estimates.primary_button"),
onClick: () => {
onButtonClick();
captureElementAndEvent({
element: {
elementName: PROJECT_SETTINGS_TRACKER_ELEMENTS.ESTIMATES_EMPTY_STATE_CREATE_BUTTON,
},
event: {
eventName: PROJECT_SETTINGS_TRACKER_EVENTS.estimate_created,
state: "SUCCESS",
},
});
},
}}
/>
@@ -1,11 +1,8 @@
import type { FC } from "react";
import { observer } from "mobx-react";
import { PROJECT_SETTINGS_TRACKER_ELEMENTS, PROJECT_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { ToggleSwitch } from "@plane/ui";
// hooks
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
import { useProjectEstimates } from "@/hooks/store/estimates";
import { useProject } from "@/hooks/store/use-project";
// i18n
@@ -32,15 +29,6 @@ export const EstimateDisableSwitch = observer(function EstimateDisableSwitch(pro
await updateProject(workspaceSlug, projectId, {
estimate: currentProjectActiveEstimate ? null : currentActiveEstimateId,
});
captureElementAndEvent({
element: {
elementName: PROJECT_SETTINGS_TRACKER_ELEMENTS.ESTIMATES_TOGGLE_BUTTON,
},
event: {
eventName: PROJECT_SETTINGS_TRACKER_EVENTS.estimates_toggle,
state: "SUCCESS",
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: currentProjectActiveEstimate
@@ -51,15 +39,6 @@ export const EstimateDisableSwitch = observer(function EstimateDisableSwitch(pro
: t("project_settings.estimates.toasts.enabled.success.message"),
});
} catch (err) {
captureElementAndEvent({
element: {
elementName: PROJECT_SETTINGS_TRACKER_ELEMENTS.ESTIMATES_TOGGLE_BUTTON,
},
event: {
eventName: PROJECT_SETTINGS_TRACKER_EVENTS.estimates_toggle,
state: "ERROR",
},
});
setToast({
type: TOAST_TYPE.ERROR,
title: t("project_settings.estimates.toasts.disabled.error.title"),
@@ -8,8 +8,6 @@ import {
EUserPermissionsLevel,
EXPORTERS_LIST,
// ISSUE_DISPLAY_FILTERS_BY_PAGE,
WORKSPACE_SETTINGS_TRACKER_EVENTS,
WORKSPACE_SETTINGS_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
@@ -20,7 +18,6 @@ import type { TWorkItemFilterExpression } from "@plane/types";
import { CustomSearchSelect, CustomSelect } from "@plane/ui";
// import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level";
// import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useProject } from "@/hooks/store/use-project";
import { useUser, useUserPermissions } from "@/hooks/store/user";
import { ProjectExportService } from "@/services/project/project-export.service";
@@ -105,12 +102,6 @@ export const ExportForm = observer(function ExportForm(props: Props) {
await projectExportService.csvExport(workspaceSlug, payload);
mutateServices();
setExportLoading(false);
captureSuccess({
eventName: WORKSPACE_SETTINGS_TRACKER_EVENTS.csv_exported,
payload: {
provider: formData.provider.provider,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("workspace_settings.settings.exports.modal.toasts.success.title"),
@@ -127,13 +118,6 @@ export const ExportForm = observer(function ExportForm(props: Props) {
});
} catch (error) {
setExportLoading(false);
captureError({
eventName: WORKSPACE_SETTINGS_TRACKER_EVENTS.csv_exported,
payload: {
provider: formData.provider.provider,
},
error: error as Error,
});
setToast({
type: TOAST_TYPE.ERROR,
title: t("error"),
@@ -258,12 +242,7 @@ export const ExportForm = observer(function ExportForm(props: Props) {
/>
</div> */}
<div className="flex items-center justify-between">
<Button
variant="primary"
type="submit"
loading={exportLoading}
data-ph-element={WORKSPACE_SETTINGS_TRACKER_ELEMENTS.EXPORT_BUTTON}
>
<Button variant="primary" type="submit" loading={exportLoading}>
{exportLoading ? `${t("workspace_settings.settings.exports.exporting")}...` : t("export")}
</Button>
</div>
@@ -1,12 +1,8 @@
import { useEffect } from "react";
import { observer } from "mobx-react";
import { USER_TRACKER_ELEMENTS } from "@plane/constants";
// ui
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// components
import { ProductUpdatesFooter } from "@/components/global";
// helpers
import { captureView } from "@/helpers/event-tracker.helper";
// plane web components
import { ProductUpdatesChangelog } from "@/plane-web/components/global/product-updates/changelog";
import { ProductUpdatesHeader } from "@/plane-web/components/global/product-updates/header";
@@ -19,12 +15,6 @@ export type ProductUpdatesModalProps = {
export const ProductUpdatesModal = observer(function ProductUpdatesModal(props: ProductUpdatesModalProps) {
const { isOpen, handleClose } = props;
useEffect(() => {
if (isOpen) {
captureView({ elementName: USER_TRACKER_ELEMENTS.PRODUCT_CHANGELOG_MODAL });
}
}, [isOpen]);
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXXXL}>
<ProductUpdatesHeader />
+6 -16
View File
@@ -2,10 +2,7 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// plane imports
import { PRODUCT_TOUR_TRACKER_EVENTS } from "@plane/constants";
import { ContentWrapper } from "@plane/ui";
// helpers
import { captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useHome } from "@/hooks/store/use-home";
import { useUserProfile, useUser } from "@/hooks/store/user";
@@ -33,19 +30,12 @@ export const WorkspaceHomeView = observer(function WorkspaceHomeView() {
}
);
const handleTourCompleted = () => {
updateTourCompleted()
.then(() => {
captureSuccess({
eventName: PRODUCT_TOUR_TRACKER_EVENTS.complete,
payload: {
user_id: currentUser?.id,
},
});
})
.catch((error) => {
console.error(error);
});
const handleTourCompleted = async () => {
try {
await updateTourCompleted();
} catch (error) {
console.error("Error updating tour completed", error);
}
};
// TODO: refactor loader implementation
@@ -5,14 +5,12 @@ import Link from "next/link";
import { useParams } from "next/navigation";
import { Hotel } from "lucide-react";
// plane ui
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { MembersPropertyIcon, CheckIcon, ProjectIcon, CloseIcon } from "@plane/propel/icons";
import { cn, getFileURL } from "@plane/utils";
// helpers
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useProject } from "@/hooks/store/use-project";
import { useWorkspace } from "@/hooks/store/use-workspace";
@@ -57,7 +55,6 @@ export const NoProjectsEmptyState = observer(function NoProjectsEmptyState() {
e.preventDefault();
e.stopPropagation();
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
},
disabled: !canCreateProject,
},
@@ -2,7 +2,6 @@ import type { Dispatch, SetStateAction } from "react";
import { useEffect, useMemo, useRef } from "react";
import { observer } from "mobx-react";
// plane imports
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import type { EditorRefApi } from "@plane/editor";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TIssue, TNameDescriptionLoader } from "@plane/types";
@@ -17,8 +16,6 @@ import type { TIssueOperations } from "@/components/issues/issue-detail";
import { IssueActivity } from "@/components/issues/issue-detail/issue-activity";
import { IssueReaction } from "@/components/issues/issue-detail/reactions";
import { IssueTitleInput } from "@/components/issues/title-input";
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useMember } from "@/hooks/store/use-member";
@@ -102,10 +99,6 @@ export const InboxIssueMainContent = observer(function InboxIssueMainContent(pro
type: TOAST_TYPE.SUCCESS,
message: "Work item deleted successfully",
});
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: _issueId },
});
} catch (error) {
console.log("Error in deleting work item:", error);
setToast({
@@ -113,47 +106,24 @@ export const InboxIssueMainContent = observer(function InboxIssueMainContent(pro
type: TOAST_TYPE.ERROR,
message: "Work item delete failed",
});
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: _issueId },
error: error as Error,
});
}
},
update: async (_workspaceSlug: string, _projectId: string, _issueId: string, data: Partial<TIssue>) => {
try {
await inboxIssue.updateIssue(data);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: _issueId },
});
} catch (error) {
setToast({
title: "Work item update failed",
type: TOAST_TYPE.ERROR,
message: "Work item update failed",
});
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: _issueId },
error: error as Error,
});
}
},
archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
await archiveIssue(workspaceSlug, projectId, issueId);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId },
});
} catch (error) {
console.error("Error in archiving issue:", error);
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId },
error: error as Error,
});
}
},
}),
@@ -1,8 +1,8 @@
import type { FC, FormEvent } from "react";
import type { FormEvent } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { ETabIndices, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { ETabIndices } from "@plane/constants";
import type { EditorRefApi } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
@@ -10,8 +10,6 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TIssue } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
import { renderFormattedPayloadDate, getTabIndex } from "@plane/utils";
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useProjectInbox } from "@/hooks/store/use-project-inbox";
@@ -170,12 +168,6 @@ export const InboxIssueCreateRoot = observer(function InboxIssueCreateRoot(props
descriptionEditorRef?.current?.clearEditor();
setFormData(defaultIssueData);
}
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.create,
payload: {
id: res?.issue?.id,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: `Success!`,
@@ -184,13 +176,6 @@ export const InboxIssueCreateRoot = observer(function InboxIssueCreateRoot(props
})
.catch((error) => {
console.error(error);
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.create,
payload: {
id: formData?.id,
},
error: error as Error,
});
setToast({
type: TOAST_TYPE.ERROR,
title: `Error!`,
@@ -1,9 +1,6 @@
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useFormContext, Controller } from "react-hook-form";
import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { PlusIcon } from "@plane/propel/icons";
import type { IJiraImporterForm } from "@plane/types";
// hooks
@@ -11,7 +8,6 @@ import type { IJiraImporterForm } from "@plane/types";
import { CustomSelect, Input } from "@plane/ui";
// helpers
import { checkEmailValidity } from "@plane/utils";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useProject } from "@/hooks/store/use-project";
// types
@@ -199,9 +195,7 @@ export const JiraGetImportDetail = observer(function JiraGetImportDetail() {
<div>
<button
type="button"
data-ph-element={PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON}
onClick={() => {
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.CREATE_PROJECT_JIRA_IMPORT_DETAIL_PAGE });
toggleCreateProjectModal(true);
}}
className="flex cursor-pointer select-none items-center space-x-2 truncate rounded-sm px-1 py-1.5 text-secondary"
@@ -1,10 +1,8 @@
import { useMemo } from "react";
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { setPromiseToast, TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TIssueServiceType } from "@plane/types";
import { EIssueServiceType } from "@plane/types";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
// types
import type { TAttachmentUploadStatus } from "@/store/issue/issue-details/attachment.store";
@@ -36,34 +34,21 @@ export const useAttachmentOperations = (
const attachmentOperations: TAttachmentOperations = useMemo(
() => ({
create: async (file) => {
try {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
const attachmentUploadPromise = createAttachment(workspaceSlug, projectId, issueId, file);
setPromiseToast(attachmentUploadPromise, {
loading: "Uploading attachment...",
success: {
title: "Attachment uploaded",
message: () => "The attachment has been successfully uploaded",
},
error: {
title: "Attachment not uploaded",
message: () => "The attachment could not be uploaded",
},
});
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
const attachmentUploadPromise = createAttachment(workspaceSlug, projectId, issueId, file);
setPromiseToast(attachmentUploadPromise, {
loading: "Uploading attachment...",
success: {
title: "Attachment uploaded",
message: () => "The attachment has been successfully uploaded",
},
error: {
title: "Attachment not uploaded",
message: () => "The attachment could not be uploaded",
},
});
await attachmentUploadPromise;
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.attachment.add,
payload: { id: issueId },
});
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.attachment.add,
payload: { id: issueId },
error: error as Error,
});
throw error;
}
await attachmentUploadPromise;
},
remove: async (attachmentId) => {
try {
@@ -74,16 +59,7 @@ export const useAttachmentOperations = (
type: TOAST_TYPE.SUCCESS,
title: "Attachment removed",
});
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.attachment.remove,
payload: { id: issueId },
});
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.attachment.remove,
payload: { id: issueId },
error: error as Error,
});
} catch (_error) {
setToast({
message: "The Attachment could not be removed",
type: TOAST_TYPE.ERROR,
@@ -1,13 +1,11 @@
import { useMemo } from "react";
// plane imports
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TIssue, TIssueServiceType } from "@plane/types";
import { EIssueServiceType } from "@plane/types";
import { copyUrlToClipboard } from "@plane/utils";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
export type TRelationIssueOperations = {
@@ -26,33 +24,23 @@ export const useRelationOperations = (
const issueOperations: TRelationIssueOperations = useMemo(
() => ({
copyLink: (path) => {
copyUrlToClipboard(path).then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("common.link_copied"),
message: t("entity.link_copied_to_clipboard", { entity: entityName }),
});
copyLink: async (path) => {
await copyUrlToClipboard(path);
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("common.link_copied"),
message: t("entity.link_copied_to_clipboard", { entity: entityName }),
});
},
update: async (workspaceSlug, projectId, issueId, data) => {
try {
await updateIssue(workspaceSlug, projectId, issueId, data);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
});
setToast({
title: t("toast.success"),
type: TOAST_TYPE.SUCCESS,
message: t("entity.update.success", { entity: entityName }),
});
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
error: error as Error,
});
} catch (_error) {
setToast({
title: t("toast.error"),
type: TOAST_TYPE.ERROR,
@@ -61,20 +49,7 @@ export const useRelationOperations = (
}
},
remove: async (workspaceSlug, projectId, issueId) => {
try {
return removeIssue(workspaceSlug, projectId, issueId).then(() => {
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId },
});
});
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId },
error: error as Error,
});
}
return removeIssue(workspaceSlug, projectId, issueId);
},
}),
[entityName, removeIssue, t, updateIssue]
@@ -1,14 +1,12 @@
import { useMemo } from "react";
import { useParams } from "next/navigation";
// plane imports
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TIssueServiceType, TSubIssueOperations } from "@plane/types";
import { EIssueServiceType } from "@plane/types";
import { copyUrlToClipboard } from "@plane/utils";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useProjectState } from "@/hooks/store/use-project-state";
// plane web helpers
@@ -39,18 +37,17 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
const subIssueOperations: TSubIssueOperations = useMemo(
() => ({
copyLink: (path) => {
copyUrlToClipboard(path).then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("common.link_copied"),
message: t("entity.link_copied_to_clipboard", {
entity:
issueServiceType === EIssueServiceType.ISSUES
? t("common.sub_work_items", { count: 1 })
: t("issue.label", { count: 1 }),
}),
});
copyLink: async (path) => {
await copyUrlToClipboard(path);
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("common.link_copied"),
message: t("entity.link_copied_to_clipboard", {
entity:
issueServiceType === EIssueServiceType.ISSUES
? t("common.sub_work_items", { count: 1 })
: t("issue.label", { count: 1 }),
}),
});
},
fetchSubIssues: async (workspaceSlug, projectId, parentIssueId) => {
@@ -131,22 +128,13 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
}
}
}
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.sub_issue.update,
payload: { id: issueId, parent_id: parentIssueId },
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("toast.success"),
message: t("sub_work_item.update.success"),
});
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.sub_issue.update,
payload: { id: issueId, parent_id: parentIssueId },
error: error as Error,
});
} catch (_error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
@@ -178,17 +166,8 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
: t("issue.label", { count: 1 }),
}),
});
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.sub_issue.remove,
payload: { id: issueId, parent_id: parentIssueId },
});
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.sub_issue.remove,
payload: { id: issueId, parent_id: parentIssueId },
error: error as Error,
});
} catch (_error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
@@ -205,18 +184,9 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
try {
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
return deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId).then(() => {
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.sub_issue.delete,
payload: { id: issueId, parent_id: parentIssueId },
});
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
});
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.sub_issue.delete,
payload: { id: issueId, parent_id: parentIssueId },
error: error as Error,
});
} catch (_error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
@@ -1,7 +1,5 @@
import type { FC } from "react";
import React from "react";
import { observer } from "mobx-react";
// plane imports
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
@@ -9,7 +7,6 @@ import { PlusIcon, WorkItemsIcon } from "@plane/propel/icons";
import type { TIssue, TIssueServiceType } from "@plane/types";
import { CustomMenu } from "@plane/ui";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
type Props = {
@@ -54,13 +51,11 @@ export const SubIssuesActionButton = observer(function SubIssuesActionButton(pro
};
const handleCreateNew = () => {
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.sub_issue.create });
handleIssueCrudState("create", issueId, null);
toggleCreateIssueModal(true);
};
const handleAddExisting = () => {
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.sub_issue.add_existing });
handleIssueCrudState("existing", issueId, null);
toggleSubIssuesModal(issue.id);
};
@@ -1,17 +1,13 @@
import { useRef } from "react";
import { observer } from "mobx-react";
// plane imports
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { CopyLinkIcon } from "@plane/propel/icons";
import { IconButton } from "@plane/propel/icon-button";
import { LinkIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip";
import { EIssuesStoreType } from "@plane/types";
import { generateWorkItemLink, copyTextToClipboard } from "@plane/utils";
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useIssues } from "@/hooks/store/use-issues";
@@ -70,15 +66,21 @@ export const IssueDetailQuickActions = observer(function IssueDetailQuickActions
});
// handlers
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}${workItemLink}`).then(() => {
const handleCopyText = async () => {
try {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
await copyTextToClipboard(`${originURL}${workItemLink}`);
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("common.link_copied"),
message: t("common.copied_to_clipboard"),
});
});
} catch (_error) {
setToast({
title: t("toast.error"),
type: TOAST_TYPE.ERROR,
});
}
};
const handleDeleteIssue = async () => {
@@ -88,24 +90,14 @@ export const IssueDetailQuickActions = observer(function IssueDetailQuickActions
? `/${workspaceSlug}/projects/${projectId}/archives/issues`
: `/${workspaceSlug}/projects/${projectId}/issues`;
return deleteIssue(workspaceSlug, projectId, issueId).then(() => {
router.push(redirectionPath);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId },
});
});
} catch (error) {
await deleteIssue(workspaceSlug, projectId, issueId);
router.push(redirectionPath);
} catch (_error) {
setToast({
title: t("toast.error "),
type: TOAST_TYPE.ERROR,
message: t("entity.delete.failed", { entity: t("issue.label", { count: 1 }) }),
});
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId },
error: error as Error,
});
}
};
@@ -113,38 +105,32 @@ export const IssueDetailQuickActions = observer(function IssueDetailQuickActions
try {
await archiveIssue(workspaceSlug, projectId, issueId);
router.push(`/${workspaceSlug}/projects/${projectId}/issues`);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId },
});
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId },
error: error as Error,
} catch (_error) {
setToast({
title: t("toast.error"),
type: TOAST_TYPE.ERROR,
message: t("issue.archive.failed.message"),
});
}
};
const handleRestore = async () => {
if (!workspaceSlug || !projectId || !issueId) return;
await restoreIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString())
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("issue.restore.success.title"),
message: t("issue.restore.success.message"),
});
router.push(workItemLink);
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: t("issue.restore.failed.message"),
});
try {
await restoreIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString());
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("issue.restore.success.title"),
message: t("issue.restore.success.message"),
});
router.push(workItemLink);
} catch (_error) {
setToast({
title: t("toast.error"),
type: TOAST_TYPE.ERROR,
message: t("issue.restore.failed.message"),
});
}
};
return (
@@ -1,8 +1,7 @@
import type { FC } from "react";
import { useMemo } from "react";
import { observer } from "mobx-react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/propel/toast";
import type { TIssue } from "@plane/types";
@@ -12,7 +11,6 @@ import emptyIssue from "@/app/assets/empty-state/issue.svg?url";
// components
import { EmptyState } from "@/components/common/empty-state";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useIssues } from "@/hooks/store/use-issues";
@@ -90,17 +88,8 @@ export const IssueDetailRoot = observer(function IssueDetailRoot(props: TIssueDe
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
try {
await updateIssue(workspaceSlug, projectId, issueId, data);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
});
} catch (error) {
console.log("Error in updating issue:", error);
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
error: error as Error,
});
setToast({
title: t("common.error.label"),
type: TOAST_TYPE.ERROR,
@@ -117,10 +106,6 @@ export const IssueDetailRoot = observer(function IssueDetailRoot(props: TIssueDe
type: TOAST_TYPE.SUCCESS,
message: t("entity.delete.success", { entity: t("issue.label") }),
});
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId },
});
} catch (error) {
console.log("Error in deleting issue:", error);
setToast({
@@ -128,67 +113,35 @@ export const IssueDetailRoot = observer(function IssueDetailRoot(props: TIssueDe
type: TOAST_TYPE.ERROR,
message: t("entity.delete.failed", { entity: t("issue.label") }),
});
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId },
error: error as Error,
});
}
},
archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
await archiveIssue(workspaceSlug, projectId, issueId);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId },
});
} catch (error) {
console.log("Error in archiving issue:", error);
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId },
error: error as Error,
});
}
},
addCycleToIssue: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
try {
await addCycleToIssue(workspaceSlug, projectId, cycleId, issueId);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
});
} catch (error) {
} catch (_error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("common.error.label"),
message: t("issue.add.cycle.failed"),
});
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
error: error as Error,
});
}
},
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
try {
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
});
} catch (error) {
} catch (_error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("common.error.label"),
message: t("issue.add.cycle.failed"),
});
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
error: error as Error,
});
}
},
removeIssueFromCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
@@ -206,16 +159,8 @@ export const IssueDetailRoot = observer(function IssueDetailRoot(props: TIssueDe
},
});
await removeFromCyclePromise;
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
});
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
error: error as Error,
});
console.log("Error in removing issue from cycle:", error);
}
},
removeIssueFromModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => {
@@ -233,16 +178,8 @@ export const IssueDetailRoot = observer(function IssueDetailRoot(props: TIssueDe
},
});
await removeFromModulePromise;
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
});
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
error: error as Error,
});
console.log("Error in removing issue from module:", error);
}
},
changeModulesInIssue: async (
@@ -253,10 +190,6 @@ export const IssueDetailRoot = observer(function IssueDetailRoot(props: TIssueDe
removeModuleIds: string[]
) => {
const promise = await changeModulesInIssue(workspaceSlug, projectId, issueId, addModuleIds, removeModuleIds);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
});
return promise;
},
}),
@@ -273,7 +206,6 @@ export const IssueDetailRoot = observer(function IssueDetailRoot(props: TIssueDe
changeModulesInIssue,
removeIssueFromModule,
t,
issueId,
]
);
@@ -11,7 +11,6 @@ import type { ISearchIssueResponse } from "@plane/types";
import { EIssuesStoreType, EUserProjectRoles } from "@plane/types";
// components
import { ExistingIssuesListModal } from "@/components/core/modals/existing-issues-list-modal";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useCycle } from "@/hooks/store/use-cycle";
import { useIssues } from "@/hooks/store/use-issues";
@@ -107,7 +106,6 @@ export const CycleEmptyState = observer(function CycleEmptyState() {
{
label: t("project_empty_state.cycle_work_items.cta_primary"),
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.CYCLE });
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
},
disabled: !canPerformEmptyStateActions,
@@ -1,11 +1,10 @@
import { observer } from "mobx-react";
// plane imports
import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EIssuesStoreType, EUserWorkspaceRoles } from "@plane/types";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
@@ -35,7 +34,6 @@ export const GlobalViewEmptyState = observer(function GlobalViewEmptyState() {
label: t("workspace_projects.empty_state.no_projects.primary_button.text"),
onClick: () => {
toggleCreateProjectModal(true);
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.GLOBAL_VIEW });
},
disabled: !hasMemberLevelPermission,
variant: "primary",
@@ -55,7 +53,6 @@ export const GlobalViewEmptyState = observer(function GlobalViewEmptyState() {
{
label: t(`workspace_empty_state.views.cta_primary`),
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.GLOBAL_VIEW });
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
disabled: !hasMemberLevelPermission,
@@ -2,7 +2,7 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
@@ -10,7 +10,6 @@ import type { ISearchIssueResponse } from "@plane/types";
import { EIssuesStoreType, EUserProjectRoles } from "@plane/types";
// components
import { ExistingIssuesListModal } from "@/components/core/modals/existing-issues-list-modal";
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useIssues } from "@/hooks/store/use-issues";
@@ -94,7 +93,6 @@ export const ModuleEmptyState = observer(function ModuleEmptyState() {
{
label: t("project_empty_state.module_work_items.cta_primary"),
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.MODULE });
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
},
disabled: !canPerformEmptyStateActions,
@@ -1,12 +1,10 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EIssuesStoreType, EUserProjectRoles } from "@plane/types";
// components
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useUserPermissions } from "@/hooks/store/user";
@@ -54,7 +52,6 @@ export const ProjectEmptyState = observer(function ProjectEmptyState() {
{
label: t("project_empty_state.work_items.cta_primary"),
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.WORK_ITEMS });
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
disabled: !canPerformEmptyStateActions,
@@ -3,8 +3,6 @@ import { observer } from "mobx-react";
import { EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import { EIssuesStoreType } from "@plane/types";
// components
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useUserPermissions } from "@/hooks/store/user";
@@ -30,7 +28,6 @@ export const ProjectViewEmptyState = observer(function ProjectViewEmptyState() {
{
label: "New work item",
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.PROJECT_VIEW });
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW);
},
disabled: !isCreatingIssueAllowed,
@@ -5,12 +5,10 @@ import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { EIssueFilterType, EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { EIssueFilterType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import type { EIssuesStoreType } from "@plane/types";
import { EIssueServiceType, EIssueLayoutTypes } from "@plane/types";
//constants
//hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useIssues } from "@/hooks/store/use-issues";
import { useKanbanView } from "@/hooks/store/use-kanban-view";
@@ -200,23 +198,14 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa
if (!draggedIssueId || !draggedIssue) return;
await removeIssue(draggedIssue.project_id, draggedIssueId)
.then(() => {
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: draggedIssueId },
});
})
.catch(() => {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: draggedIssueId },
});
})
.finally(() => {
setDeleteIssueModal(false);
setDraggedIssueId(undefined);
});
try {
await removeIssue(draggedIssue.project_id, draggedIssueId);
setDeleteIssueModal(false);
setDraggedIssueId(undefined);
} catch (_error) {
setDeleteIssueModal(false);
setDraggedIssueId(undefined);
}
};
const handleCollapsedGroups = useCallback(
@@ -3,7 +3,6 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// lucide icons
import { Minimize2, Maximize2, Circle } from "lucide-react";
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { PlusIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TIssue, ISearchIssueResponse, TIssueKanbanFilters, TIssueGroupByOptions } from "@plane/types";
@@ -13,7 +12,6 @@ import { CustomMenu } from "@plane/ui";
import { ExistingIssuesListModal } from "@/components/core/modals/existing-issues-list-modal";
import { CreateUpdateIssueModal } from "@/components/issues/issue-modal/modal";
// constants
import { captureClick } from "@/helpers/event-tracker.helper";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { CreateUpdateEpicModal } from "@/plane-web/components/epics/epic-modal";
// types
@@ -75,7 +73,7 @@ export const HeaderGroupByCard = observer(function HeaderGroupByCard(props: IHea
title: "Success!",
message: "Work items added to the cycle successfully.",
});
} catch (error) {
} catch (_error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -162,7 +160,6 @@ export const HeaderGroupByCard = observer(function HeaderGroupByCard(props: IHea
>
<CustomMenu.MenuItem
onClick={() => {
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.create });
setIsOpen(true);
}}
>
@@ -170,7 +167,6 @@ export const HeaderGroupByCard = observer(function HeaderGroupByCard(props: IHea
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.add_existing });
setOpenExistingIssueListModal(true);
}}
>
@@ -181,7 +177,6 @@ export const HeaderGroupByCard = observer(function HeaderGroupByCard(props: IHea
<button
className="flex h-[20px] w-[20px] flex-shrink-0 cursor-pointer overflow-hidden transition-all hover:bg-layer-transparent-hover bg-layer-transparent rounded-sm items-center justify-center"
onClick={() => {
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.create });
setIsOpen(true);
}}
>
@@ -2,7 +2,6 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { CircleDashed } from "lucide-react";
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { PlusIcon } from "@plane/propel/icons";
// types
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
@@ -15,7 +14,6 @@ import { ExistingIssuesListModal } from "@/components/core/modals/existing-issue
import { MultipleSelectGroupAction } from "@/components/core/multiple-select";
import { CreateUpdateIssueModal } from "@/components/issues/issue-modal/modal";
// constants
import { captureClick } from "@/helpers/event-tracker.helper";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import type { TSelectionHelper } from "@/hooks/use-multiple-select";
// plane-web
@@ -132,7 +130,6 @@ export const HeaderGroupByCard = observer(function HeaderGroupByCard(props: IHea
>
<CustomMenu.MenuItem
onClick={() => {
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.create });
setIsOpen(true);
}}
>
@@ -140,7 +137,6 @@ export const HeaderGroupByCard = observer(function HeaderGroupByCard(props: IHea
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.add_existing });
setOpenExistingIssueListModal(true);
}}
>
@@ -151,7 +147,6 @@ export const HeaderGroupByCard = observer(function HeaderGroupByCard(props: IHea
<div
className="flex h-5 w-5 flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-xs transition-all hover:bg-layer-1"
onClick={() => {
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.create });
setIsOpen(true);
}}
>
@@ -5,8 +5,6 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { Paperclip } from "lucide-react";
// types
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
import { LinkIcon, StartDatePropertyIcon, ViewsIcon, DueDatePropertyIcon } from "@plane/propel/icons";
@@ -29,8 +27,6 @@ import { MemberDropdown } from "@/components/dropdowns/member/dropdown";
import { ModuleDropdown } from "@/components/dropdowns/module/dropdown";
import { PriorityDropdown } from "@/components/dropdowns/priority";
import { StateDropdown } from "@/components/dropdowns/state/dropdown";
// helpers
import { captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useProjectEstimates } from "@/hooks/store/estimates";
import { useIssues } from "@/hooks/store/use-issues";
@@ -105,44 +101,20 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
[workspaceSlug, issue, changeModulesInIssue, addCycleToIssue, removeCycleFromIssue]
);
const handleState = (stateId: string) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { state_id: stateId }).then(() => {
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issue.id },
});
});
const handleState = async (stateId: string) => {
if (updateIssue) await updateIssue(issue.project_id, issue.id, { state_id: stateId });
};
const handlePriority = (value: TIssuePriorities) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { priority: value }).then(() => {
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issue.id },
});
});
const handlePriority = async (value: TIssuePriorities) => {
if (updateIssue) await updateIssue(issue.project_id, issue.id, { priority: value });
};
const handleLabel = (ids: string[]) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { label_ids: ids }).then(() => {
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issue.id },
});
});
const handleLabel = async (ids: string[]) => {
if (updateIssue) await updateIssue(issue.project_id, issue.id, { label_ids: ids });
};
const handleAssignee = (ids: string[]) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { assignee_ids: ids }).then(() => {
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issue.id },
});
});
const handleAssignee = async (ids: string[]) => {
if (updateIssue) await updateIssue(issue.project_id, issue.id, { assignee_ids: ids });
};
const handleModule = useCallback(
@@ -157,11 +129,6 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
else modulesToAdd.push(moduleId);
if (modulesToAdd.length > 0) issueOperations.addModulesToIssue(modulesToAdd);
if (modulesToRemove.length > 0) issueOperations.removeModulesFromIssue(modulesToRemove);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issue.id },
});
},
[issueOperations, issue]
);
@@ -171,47 +138,22 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
if (!issue || issue.cycle_id === cycleId) return;
if (cycleId) issueOperations.addIssueToCycle?.(cycleId);
else issueOperations.removeIssueFromCycle?.();
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issue.id },
});
},
[issue, issueOperations]
);
const handleStartDate = (date: Date | null) => {
const handleStartDate = async (date: Date | null) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { start_date: date ? renderFormattedPayloadDate(date) : null }).then(
() => {
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issue.id },
});
}
);
await updateIssue(issue.project_id, issue.id, { start_date: date ? renderFormattedPayloadDate(date) : null });
};
const handleTargetDate = (date: Date | null) => {
const handleTargetDate = async (date: Date | null) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { target_date: date ? renderFormattedPayloadDate(date) : null }).then(
() => {
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issue.id },
});
}
);
await updateIssue(issue.project_id, issue.id, { target_date: date ? renderFormattedPayloadDate(date) : null });
};
const handleEstimate = (value: string | undefined) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { estimate_point: value }).then(() => {
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issue.id },
});
});
const handleEstimate = async (value: string | undefined) => {
if (updateIssue) await updateIssue(issue.project_id, issue.id, { estimate_point: value });
};
const workItemLink = generateWorkItemLink({
@@ -3,14 +3,12 @@ import { omit } from "lodash-es";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { ARCHIVABLE_STATE_GROUPS, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { ARCHIVABLE_STATE_GROUPS } from "@plane/constants";
import type { TIssue } from "@plane/types";
import { EIssuesStoreType } from "@plane/types";
import type { TContextMenuItem } from "@plane/ui";
import { ContextMenu, CustomMenu } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useProject } from "@/hooks/store/use-project";
import { useProjectState } from "@/hooks/store/use-project-state";
// plane-web components
@@ -88,9 +86,7 @@ export const AllIssueQuickActions = observer(function AllIssueQuickActions(props
const CONTEXT_MENU_ITEMS = MENU_ITEMS.map(function CONTEXT_MENU_ITEMS(item) {
return {
...item,
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.GLOBAL_VIEW });
item.action();
},
};
@@ -180,7 +176,6 @@ export const AllIssueQuickActions = observer(function AllIssueQuickActions(props
<CustomMenu.MenuItem
key={nestedItem.key}
onClick={() => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.GLOBAL_VIEW });
nestedItem.action();
}}
className={cn(
@@ -216,7 +211,6 @@ export const AllIssueQuickActions = observer(function AllIssueQuickActions(props
<CustomMenu.MenuItem
key={item.key}
onClick={() => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.GLOBAL_VIEW });
item.action();
}}
className={cn(

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