mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of github.com:makeplane/plane into update-file-uploads
This commit is contained in:
commit
c25ac3b3a6
@ -401,8 +401,8 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
email=email,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign up",
|
||||||
medium="MAGIC_LINK",
|
medium="Magic link",
|
||||||
first_time=True,
|
first_time=True,
|
||||||
)
|
)
|
||||||
key, token, current_attempt = generate_magic_token(email=email)
|
key, token, current_attempt = generate_magic_token(email=email)
|
||||||
@ -438,8 +438,8 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
email=email,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign in",
|
||||||
medium="MAGIC_LINK",
|
medium="Magic link",
|
||||||
first_time=False,
|
first_time=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -468,8 +468,8 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
email=email,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign in",
|
||||||
medium="EMAIL",
|
medium="Email",
|
||||||
first_time=False,
|
first_time=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -274,8 +274,8 @@ class SignInEndpoint(BaseAPIView):
|
|||||||
email=email,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign in",
|
||||||
medium="EMAIL",
|
medium="Email",
|
||||||
first_time=False,
|
first_time=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -349,8 +349,8 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
email=email,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign in",
|
||||||
medium="MAGIC_LINK",
|
medium="Magic link",
|
||||||
first_time=False,
|
first_time=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ from django.core import serializers
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -312,6 +313,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"labels": label_distribution,
|
"labels": label_distribution,
|
||||||
"completion_chart": {},
|
"completion_chart": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if data[0]["start_date"] and data[0]["end_date"]:
|
if data[0]["start_date"] and data[0]["end_date"]:
|
||||||
data[0]["distribution"][
|
data[0]["distribution"][
|
||||||
"completion_chart"
|
"completion_chart"
|
||||||
@ -840,9 +842,229 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
new_cycle = Cycle.objects.get(
|
new_cycle = Cycle.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
|
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
old_cycle = (
|
||||||
|
Cycle.objects.filter(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"issue_cycle",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="completed",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
cancelled_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="cancelled",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
started_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="started",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
unstarted_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="unstarted",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
backlog_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="backlog",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
total_estimates=Sum("issue_cycle__issue__estimate_point")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_estimates=Sum(
|
||||||
|
"issue_cycle__issue__estimate_point",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="completed",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
started_estimates=Sum(
|
||||||
|
"issue_cycle__issue__estimate_point",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="started",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pass the new_cycle queryset to burndown_plot
|
||||||
|
completion_chart = burndown_plot(
|
||||||
|
queryset=old_cycle.first(),
|
||||||
|
slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
cycle_id=cycle_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
assignee_distribution = (
|
||||||
|
Issue.objects.filter(
|
||||||
|
issue_cycle__cycle_id=cycle_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
.annotate(display_name=F("assignees__display_name"))
|
||||||
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
|
.values("display_name", "assignee_id", "avatar")
|
||||||
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"id",
|
||||||
|
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_issues=Count(
|
||||||
|
"id",
|
||||||
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
pending_issues=Count(
|
||||||
|
"id",
|
||||||
|
filter=Q(
|
||||||
|
completed_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("display_name")
|
||||||
|
)
|
||||||
|
|
||||||
|
label_distribution = (
|
||||||
|
Issue.objects.filter(
|
||||||
|
issue_cycle__cycle_id=cycle_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
.annotate(label_name=F("labels__name"))
|
||||||
|
.annotate(color=F("labels__color"))
|
||||||
|
.annotate(label_id=F("labels__id"))
|
||||||
|
.values("label_name", "color", "label_id")
|
||||||
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"id",
|
||||||
|
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_issues=Count(
|
||||||
|
"id",
|
||||||
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
pending_issues=Count(
|
||||||
|
"id",
|
||||||
|
filter=Q(
|
||||||
|
completed_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("label_name")
|
||||||
|
)
|
||||||
|
|
||||||
|
assignee_distribution_data = [
|
||||||
|
{
|
||||||
|
"display_name": item["display_name"],
|
||||||
|
"assignee_id": str(item["assignee_id"]) if item["assignee_id"] else None,
|
||||||
|
"avatar": item["avatar"],
|
||||||
|
"total_issues": item["total_issues"],
|
||||||
|
"completed_issues": item["completed_issues"],
|
||||||
|
"pending_issues": item["pending_issues"],
|
||||||
|
}
|
||||||
|
for item in assignee_distribution
|
||||||
|
]
|
||||||
|
|
||||||
|
label_distribution_data = [
|
||||||
|
{
|
||||||
|
"label_name": item["label_name"],
|
||||||
|
"color": item["color"],
|
||||||
|
"label_id": str(item["label_id"]) if item["label_id"] else None,
|
||||||
|
"total_issues": item["total_issues"],
|
||||||
|
"completed_issues": item["completed_issues"],
|
||||||
|
"pending_issues": item["pending_issues"],
|
||||||
|
}
|
||||||
|
for item in label_distribution
|
||||||
|
]
|
||||||
|
|
||||||
|
current_cycle = Cycle.objects.filter(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
current_cycle.progress_snapshot = {
|
||||||
|
"total_issues": old_cycle.first().total_issues,
|
||||||
|
"completed_issues": old_cycle.first().completed_issues,
|
||||||
|
"cancelled_issues": old_cycle.first().cancelled_issues,
|
||||||
|
"started_issues": old_cycle.first().started_issues,
|
||||||
|
"unstarted_issues": old_cycle.first().unstarted_issues,
|
||||||
|
"backlog_issues": old_cycle.first().backlog_issues,
|
||||||
|
"total_estimates": old_cycle.first().total_estimates,
|
||||||
|
"completed_estimates": old_cycle.first().completed_estimates,
|
||||||
|
"started_estimates": old_cycle.first().started_estimates,
|
||||||
|
"distribution":{
|
||||||
|
"labels": label_distribution_data,
|
||||||
|
"assignees": assignee_distribution_data,
|
||||||
|
"completion_chart": completion_chart,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
current_cycle.save(update_fields=["progress_snapshot"])
|
||||||
|
|
||||||
if (
|
if (
|
||||||
new_cycle.end_date is not None
|
new_cycle.end_date is not None
|
||||||
|
@ -296,7 +296,7 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
email=email,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign in",
|
||||||
medium=medium.upper(),
|
medium=medium.upper(),
|
||||||
first_time=False,
|
first_time=False,
|
||||||
)
|
)
|
||||||
@ -427,7 +427,7 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
email=email,
|
email=email,
|
||||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
ip=request.META.get("REMOTE_ADDR"),
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
event_name="SIGN_IN",
|
event_name="Sign up",
|
||||||
medium=medium.upper(),
|
medium=medium.upper(),
|
||||||
first_time=True,
|
first_time=True,
|
||||||
)
|
)
|
||||||
|
33
apiserver/plane/db/migrations/0059_auto_20240208_0957.py
Normal file
33
apiserver/plane/db/migrations/0059_auto_20240208_0957.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2024-02-08 09:57
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def widgets_filter_change(apps, schema_editor):
|
||||||
|
Widget = apps.get_model("db", "Widget")
|
||||||
|
widgets_to_update = []
|
||||||
|
|
||||||
|
# Define the filter dictionaries for each widget key
|
||||||
|
filters_mapping = {
|
||||||
|
"assigned_issues": {"duration": "none", "tab": "pending"},
|
||||||
|
"created_issues": {"duration": "none", "tab": "pending"},
|
||||||
|
"issues_by_state_groups": {"duration": "none"},
|
||||||
|
"issues_by_priority": {"duration": "none"},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Iterate over widgets and update filters if applicable
|
||||||
|
for widget in Widget.objects.all():
|
||||||
|
if widget.key in filters_mapping:
|
||||||
|
widget.filters = filters_mapping[widget.key]
|
||||||
|
widgets_to_update.append(widget)
|
||||||
|
|
||||||
|
# Bulk update the widgets
|
||||||
|
Widget.objects.bulk_update(widgets_to_update, ["filters"], batch_size=10)
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('db', '0058_alter_moduleissue_issue_and_more'),
|
||||||
|
]
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(widgets_filter_change)
|
||||||
|
]
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2024-02-08 09:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0059_auto_20240208_0957'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cycle',
|
||||||
|
name='progress_snapshot',
|
||||||
|
field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
]
|
@ -68,6 +68,7 @@ class Cycle(ProjectBaseModel):
|
|||||||
sort_order = models.FloatField(default=65535)
|
sort_order = models.FloatField(default=65535)
|
||||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
progress_snapshot = models.JSONField(default=dict)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Cycle"
|
verbose_name = "Cycle"
|
||||||
|
18
packages/types/src/cycles.d.ts
vendored
18
packages/types/src/cycles.d.ts
vendored
@ -31,6 +31,7 @@ export interface ICycle {
|
|||||||
issue: string;
|
issue: string;
|
||||||
name: string;
|
name: string;
|
||||||
owned_by: string;
|
owned_by: string;
|
||||||
|
progress_snapshot: TProgressSnapshot;
|
||||||
project: string;
|
project: string;
|
||||||
project_detail: IProjectLite;
|
project_detail: IProjectLite;
|
||||||
status: TCycleGroups;
|
status: TCycleGroups;
|
||||||
@ -49,6 +50,23 @@ export interface ICycle {
|
|||||||
workspace_detail: IWorkspaceLite;
|
workspace_detail: IWorkspaceLite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TProgressSnapshot = {
|
||||||
|
backlog_issues: number;
|
||||||
|
cancelled_issues: number;
|
||||||
|
completed_estimates: number | null;
|
||||||
|
completed_issues: number;
|
||||||
|
distribution?: {
|
||||||
|
assignees: TAssigneesDistribution[];
|
||||||
|
completion_chart: TCompletionChartDistribution;
|
||||||
|
labels: TLabelsDistribution[];
|
||||||
|
};
|
||||||
|
started_estimates: number | null;
|
||||||
|
started_issues: number;
|
||||||
|
total_estimates: number | null;
|
||||||
|
total_issues: number;
|
||||||
|
unstarted_issues: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type TAssigneesDistribution = {
|
export type TAssigneesDistribution = {
|
||||||
assignee_id: string | null;
|
assignee_id: string | null;
|
||||||
avatar: string | null;
|
avatar: string | null;
|
||||||
|
8
packages/types/src/dashboard.d.ts
vendored
8
packages/types/src/dashboard.d.ts
vendored
@ -24,21 +24,21 @@ export type TDurationFilterOptions =
|
|||||||
|
|
||||||
// widget filters
|
// widget filters
|
||||||
export type TAssignedIssuesWidgetFilters = {
|
export type TAssignedIssuesWidgetFilters = {
|
||||||
target_date?: TDurationFilterOptions;
|
duration?: TDurationFilterOptions;
|
||||||
tab?: TIssuesListTypes;
|
tab?: TIssuesListTypes;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreatedIssuesWidgetFilters = {
|
export type TCreatedIssuesWidgetFilters = {
|
||||||
target_date?: TDurationFilterOptions;
|
duration?: TDurationFilterOptions;
|
||||||
tab?: TIssuesListTypes;
|
tab?: TIssuesListTypes;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIssuesByStateGroupsWidgetFilters = {
|
export type TIssuesByStateGroupsWidgetFilters = {
|
||||||
target_date?: TDurationFilterOptions;
|
duration?: TDurationFilterOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIssuesByPriorityWidgetFilters = {
|
export type TIssuesByPriorityWidgetFilters = {
|
||||||
target_date?: TDurationFilterOptions;
|
duration?: TDurationFilterOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TWidgetFiltersFormData =
|
export type TWidgetFiltersFormData =
|
||||||
|
9
packages/types/src/issues.d.ts
vendored
9
packages/types/src/issues.d.ts
vendored
@ -221,3 +221,12 @@ export interface IGroupByColumn {
|
|||||||
export interface IIssueMap {
|
export interface IIssueMap {
|
||||||
[key: string]: TIssue;
|
[key: string]: TIssue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IIssueListRow {
|
||||||
|
id: string;
|
||||||
|
groupId: string;
|
||||||
|
type: "HEADER" | "NO_ISSUES" | "QUICK_ADD" | "ISSUE";
|
||||||
|
name?: string;
|
||||||
|
icon?: ReactElement | undefined;
|
||||||
|
payload?: Partial<TIssue>;
|
||||||
|
}
|
||||||
|
@ -10,7 +10,7 @@ type BreadcrumbsProps = {
|
|||||||
const Breadcrumbs = ({ children }: BreadcrumbsProps) => (
|
const Breadcrumbs = ({ children }: BreadcrumbsProps) => (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{React.Children.map(children, (child, index) => (
|
{React.Children.map(children, (child, index) => (
|
||||||
<div key={index} className="flex flex-wrap items-center gap-2.5">
|
<div key={index} className="flex items-center gap-2.5">
|
||||||
{child}
|
{child}
|
||||||
{index !== React.Children.count(children) - 1 && (
|
{index !== React.Children.count(children) - 1 && (
|
||||||
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" />
|
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" />
|
||||||
|
@ -54,10 +54,6 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnChange = () => {
|
|
||||||
if (closeOnSelect) closeDropdown();
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectActiveItem = () => {
|
const selectActiveItem = () => {
|
||||||
const activeItem: HTMLElement | undefined | null = dropdownRef.current?.querySelector(
|
const activeItem: HTMLElement | undefined | null = dropdownRef.current?.querySelector(
|
||||||
`[data-headlessui-state="active"] button`
|
`[data-headlessui-state="active"] button`
|
||||||
@ -66,6 +62,11 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen, selectActiveItem);
|
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen, selectActiveItem);
|
||||||
|
|
||||||
|
const handleOnClick = () => {
|
||||||
|
if (closeOnSelect) closeDropdown();
|
||||||
|
};
|
||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||||
|
|
||||||
let menuItems = (
|
let menuItems = (
|
||||||
@ -101,7 +102,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
className={cn("relative w-min text-left", className)}
|
className={cn("relative w-min text-left", className)}
|
||||||
onKeyDownCapture={handleKeyDown}
|
onKeyDownCapture={handleKeyDown}
|
||||||
onChange={handleOnChange}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
@ -110,7 +111,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
openDropdown();
|
openDropdown();
|
||||||
if (menuButtonOnClick) menuButtonOnClick();
|
if (menuButtonOnClick) menuButtonOnClick();
|
||||||
}}
|
}}
|
||||||
@ -127,7 +129,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
openDropdown();
|
openDropdown();
|
||||||
if (menuButtonOnClick) menuButtonOnClick();
|
if (menuButtonOnClick) menuButtonOnClick();
|
||||||
}}
|
}}
|
||||||
@ -152,7 +155,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
? "cursor-not-allowed text-custom-text-200"
|
? "cursor-not-allowed text-custom-text-200"
|
||||||
: "cursor-pointer hover:bg-custom-background-80"
|
: "cursor-pointer hover:bg-custom-background-80"
|
||||||
} ${buttonClassName}`}
|
} ${buttonClassName}`}
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
openDropdown();
|
openDropdown();
|
||||||
if (menuButtonOnClick) menuButtonOnClick();
|
if (menuButtonOnClick) menuButtonOnClick();
|
||||||
}}
|
}}
|
||||||
|
67
packages/ui/src/form-fields/checkbox.tsx
Normal file
67
packages/ui/src/form-fields/checkbox.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
intermediate?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => {
|
||||||
|
const { id, name, checked, intermediate = false, disabled, className = "", ...rest } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative w-full flex gap-2 ${className}`}>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
ref={ref}
|
||||||
|
type="checkbox"
|
||||||
|
name={name}
|
||||||
|
checked={checked}
|
||||||
|
className={`
|
||||||
|
appearance-none shrink-0 w-4 h-4 border rounded-[3px] focus:outline-1 focus:outline-offset-4 focus:outline-custom-primary-50
|
||||||
|
${
|
||||||
|
disabled
|
||||||
|
? "border-custom-border-200 bg-custom-background-80 cursor-not-allowed"
|
||||||
|
: `cursor-pointer ${
|
||||||
|
checked || intermediate
|
||||||
|
? "border-custom-primary-40 bg-custom-primary-100 hover:bg-custom-primary-200"
|
||||||
|
: "border-custom-border-300 hover:border-custom-border-400 bg-white"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
disabled={disabled}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
className={`absolute w-4 h-4 p-0.5 pointer-events-none outline-none ${
|
||||||
|
disabled ? "stroke-custom-text-400 opacity-40" : "stroke-white"
|
||||||
|
} ${checked ? "block" : "hidden"}`}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="20 6 9 17 4 12" />
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
className={`absolute w-4 h-4 p-0.5 pointer-events-none outline-none ${
|
||||||
|
disabled ? "stroke-custom-text-400 opacity-40" : "stroke-white"
|
||||||
|
} ${intermediate && !checked ? "block" : "hidden"}`}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 8 8"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M5.75 4H2.25" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Checkbox.displayName = "form-checkbox-field";
|
||||||
|
|
||||||
|
export { Checkbox };
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./input";
|
export * from "./input";
|
||||||
export * from "./textarea";
|
export * from "./textarea";
|
||||||
export * from "./input-color-picker";
|
export * from "./input-color-picker";
|
||||||
|
export * from "./checkbox";
|
||||||
|
@ -4,12 +4,14 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import { AuthService } from "services/auth.service";
|
import { AuthService } from "services/auth.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// icons
|
// icons
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -34,6 +36,8 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
|||||||
// states
|
// states
|
||||||
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
|
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// form info
|
// form info
|
||||||
@ -63,21 +67,34 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
|||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "Password created successfully.",
|
message: "Password created successfully.",
|
||||||
});
|
});
|
||||||
|
captureEvent(PASSWORD_CREATE_SELECTED, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
first_time: false,
|
||||||
|
});
|
||||||
await handleSignInRedirection();
|
await handleSignInRedirection();
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) => {
|
||||||
|
captureEvent(PASSWORD_CREATE_SELECTED, {
|
||||||
|
state: "FAILED",
|
||||||
|
first_time: false,
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoToWorkspace = async () => {
|
const handleGoToWorkspace = async () => {
|
||||||
setIsGoingToWorkspace(true);
|
setIsGoingToWorkspace(true);
|
||||||
|
await handleSignInRedirection().finally(() => {
|
||||||
await handleSignInRedirection().finally(() => setIsGoingToWorkspace(false));
|
captureEvent(PASSWORD_CREATE_SKIPPED, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
first_time: false,
|
||||||
|
});
|
||||||
|
setIsGoingToWorkspace(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -7,7 +7,7 @@ import { Eye, EyeOff, XCircle } from "lucide-react";
|
|||||||
import { AuthService } from "services/auth.service";
|
import { AuthService } from "services/auth.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import { useApplication } from "hooks/store";
|
import { useApplication, useEventTracker } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { ESignInSteps, ForgotPasswordPopover } from "components/account";
|
import { ESignInSteps, ForgotPasswordPopover } from "components/account";
|
||||||
// ui
|
// ui
|
||||||
@ -16,6 +16,8 @@ import { Button, Input } from "@plane/ui";
|
|||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IPasswordSignInData } from "@plane/types";
|
import { IPasswordSignInData } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { FORGOT_PASSWORD, SIGN_IN_WITH_PASSWORD } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -46,6 +48,7 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
config: { envConfig },
|
config: { envConfig },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// derived values
|
// derived values
|
||||||
const isSmtpConfigured = envConfig?.is_smtp_configured;
|
const isSmtpConfigured = envConfig?.is_smtp_configured;
|
||||||
// form info
|
// form info
|
||||||
@ -72,7 +75,13 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
await authService
|
await authService
|
||||||
.passwordSignIn(payload)
|
.passwordSignIn(payload)
|
||||||
.then(async () => await onSubmit())
|
.then(async () => {
|
||||||
|
captureEvent(SIGN_IN_WITH_PASSWORD, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
first_time: false,
|
||||||
|
});
|
||||||
|
await onSubmit();
|
||||||
|
})
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
@ -182,9 +191,10 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="w-full text-right mt-2 pb-3">
|
<div className="mt-2 w-full pb-3 text-right">
|
||||||
{isSmtpConfigured ? (
|
{isSmtpConfigured ? (
|
||||||
<Link
|
<Link
|
||||||
|
onClick={() => captureEvent(FORGOT_PASSWORD)}
|
||||||
href={`/accounts/forgot-password?email=${email}`}
|
href={`/accounts/forgot-password?email=${email}`}
|
||||||
className="text-xs font-medium text-custom-primary-100"
|
className="text-xs font-medium text-custom-primary-100"
|
||||||
>
|
>
|
||||||
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication } from "hooks/store";
|
import { useApplication, useEventTracker } from "hooks/store";
|
||||||
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||||
// components
|
// components
|
||||||
import { LatestFeatureBlock } from "components/common";
|
import { LatestFeatureBlock } from "components/common";
|
||||||
@ -13,6 +13,8 @@ import {
|
|||||||
OAuthOptions,
|
OAuthOptions,
|
||||||
SignInOptionalSetPasswordForm,
|
SignInOptionalSetPasswordForm,
|
||||||
} from "components/account";
|
} from "components/account";
|
||||||
|
// constants
|
||||||
|
import { NAVIGATE_TO_SIGNUP } from "constants/event-tracker";
|
||||||
|
|
||||||
export enum ESignInSteps {
|
export enum ESignInSteps {
|
||||||
EMAIL = "EMAIL",
|
EMAIL = "EMAIL",
|
||||||
@ -32,6 +34,7 @@ export const SignInRoot = observer(() => {
|
|||||||
const {
|
const {
|
||||||
config: { envConfig },
|
config: { envConfig },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// derived values
|
// derived values
|
||||||
const isSmtpConfigured = envConfig?.is_smtp_configured;
|
const isSmtpConfigured = envConfig?.is_smtp_configured;
|
||||||
|
|
||||||
@ -110,7 +113,11 @@ export const SignInRoot = observer(() => {
|
|||||||
<OAuthOptions handleSignInRedirection={handleRedirection} type="sign_in" />
|
<OAuthOptions handleSignInRedirection={handleRedirection} type="sign_in" />
|
||||||
<p className="text-xs text-onboarding-text-300 text-center mt-6">
|
<p className="text-xs text-onboarding-text-300 text-center mt-6">
|
||||||
Don{"'"}t have an account?{" "}
|
Don{"'"}t have an account?{" "}
|
||||||
<Link href="/accounts/sign-up" className="text-custom-primary-100 font-medium underline">
|
<Link
|
||||||
|
href="/accounts/sign-up"
|
||||||
|
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
|
||||||
|
className="text-custom-primary-100 font-medium underline"
|
||||||
|
>
|
||||||
Sign up
|
Sign up
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
@ -7,12 +7,15 @@ import { UserService } from "services/user.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useTimer from "hooks/use-timer";
|
import useTimer from "hooks/use-timer";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
|
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { CODE_VERIFIED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -41,6 +44,8 @@ export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// timer
|
// timer
|
||||||
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30);
|
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30);
|
||||||
// form info
|
// form info
|
||||||
@ -69,17 +74,22 @@ export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
await authService
|
await authService
|
||||||
.magicSignIn(payload)
|
.magicSignIn(payload)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
captureEvent(CODE_VERIFIED, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
const currentUser = await userService.currentUser();
|
const currentUser = await userService.currentUser();
|
||||||
|
|
||||||
await onSubmit(currentUser.is_password_autoset);
|
await onSubmit(currentUser.is_password_autoset);
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) => {
|
||||||
|
captureEvent(CODE_VERIFIED, {
|
||||||
|
state: "FAILED",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendNewCode = async (formData: TUniqueCodeFormValues) => {
|
const handleSendNewCode = async (formData: TUniqueCodeFormValues) => {
|
||||||
|
@ -4,12 +4,14 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import { AuthService } from "services/auth.service";
|
import { AuthService } from "services/auth.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// constants
|
// constants
|
||||||
import { ESignUpSteps } from "components/account";
|
import { ESignUpSteps } from "components/account";
|
||||||
|
import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker";
|
||||||
// icons
|
// icons
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
|
||||||
@ -37,6 +39,8 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
|||||||
// states
|
// states
|
||||||
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
|
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// form info
|
// form info
|
||||||
@ -66,21 +70,34 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
|||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "Password created successfully.",
|
message: "Password created successfully.",
|
||||||
});
|
});
|
||||||
|
captureEvent(SETUP_PASSWORD, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
first_time: true,
|
||||||
|
});
|
||||||
await handleSignInRedirection();
|
await handleSignInRedirection();
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) => {
|
||||||
|
captureEvent(SETUP_PASSWORD, {
|
||||||
|
state: "FAILED",
|
||||||
|
first_time: true,
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoToWorkspace = async () => {
|
const handleGoToWorkspace = async () => {
|
||||||
setIsGoingToWorkspace(true);
|
setIsGoingToWorkspace(true);
|
||||||
|
await handleSignInRedirection().finally(() => {
|
||||||
await handleSignInRedirection().finally(() => setIsGoingToWorkspace(false));
|
captureEvent(PASSWORD_CREATE_SKIPPED, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
first_time: true,
|
||||||
|
});
|
||||||
|
setIsGoingToWorkspace(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication } from "hooks/store";
|
import { useApplication, useEventTracker } from "hooks/store";
|
||||||
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
@ -12,6 +12,8 @@ import {
|
|||||||
SignUpUniqueCodeForm,
|
SignUpUniqueCodeForm,
|
||||||
} from "components/account";
|
} from "components/account";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
// constants
|
||||||
|
import { NAVIGATE_TO_SIGNIN } from "constants/event-tracker";
|
||||||
|
|
||||||
export enum ESignUpSteps {
|
export enum ESignUpSteps {
|
||||||
EMAIL = "EMAIL",
|
EMAIL = "EMAIL",
|
||||||
@ -32,6 +34,7 @@ export const SignUpRoot = observer(() => {
|
|||||||
const {
|
const {
|
||||||
config: { envConfig },
|
config: { envConfig },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
|
|
||||||
// step 1 submit handler- email verification
|
// step 1 submit handler- email verification
|
||||||
const handleEmailVerification = () => setSignInStep(ESignUpSteps.UNIQUE_CODE);
|
const handleEmailVerification = () => setSignInStep(ESignUpSteps.UNIQUE_CODE);
|
||||||
@ -86,7 +89,11 @@ export const SignUpRoot = observer(() => {
|
|||||||
<OAuthOptions handleSignInRedirection={handleRedirection} type="sign_up" />
|
<OAuthOptions handleSignInRedirection={handleRedirection} type="sign_up" />
|
||||||
<p className="text-xs text-onboarding-text-300 text-center mt-6">
|
<p className="text-xs text-onboarding-text-300 text-center mt-6">
|
||||||
Already using Plane?{" "}
|
Already using Plane?{" "}
|
||||||
<Link href="/" className="text-custom-primary-100 font-medium underline">
|
<Link
|
||||||
|
href="/"
|
||||||
|
onClick={() => captureEvent(NAVIGATE_TO_SIGNIN, {})}
|
||||||
|
className="text-custom-primary-100 font-medium underline"
|
||||||
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
@ -8,12 +8,15 @@ import { UserService } from "services/user.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useTimer from "hooks/use-timer";
|
import useTimer from "hooks/use-timer";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { checkEmailValidity } from "helpers/string.helper";
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
|
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { CODE_VERIFIED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -39,6 +42,8 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
const { email, handleEmailClear, onSubmit } = props;
|
const { email, handleEmailClear, onSubmit } = props;
|
||||||
// states
|
// states
|
||||||
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// timer
|
// timer
|
||||||
@ -69,17 +74,22 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
await authService
|
await authService
|
||||||
.magicSignIn(payload)
|
.magicSignIn(payload)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
captureEvent(CODE_VERIFIED, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
const currentUser = await userService.currentUser();
|
const currentUser = await userService.currentUser();
|
||||||
|
|
||||||
await onSubmit(currentUser.is_password_autoset);
|
await onSubmit(currentUser.is_password_autoset);
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) => {
|
||||||
|
captureEvent(CODE_VERIFIED, {
|
||||||
|
state: "FAILED",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error ?? "Something went wrong. Please try again.",
|
message: err?.error ?? "Something went wrong. Please try again.",
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendNewCode = async (formData: TUniqueCodeFormValues) => {
|
const handleSendNewCode = async (formData: TUniqueCodeFormValues) => {
|
||||||
@ -96,7 +106,6 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "A new unique code has been sent to your email.",
|
message: "A new unique code has been sent to your email.",
|
||||||
});
|
});
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
token: "",
|
token: "",
|
||||||
|
@ -10,6 +10,8 @@ import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSi
|
|||||||
import { IAnalyticsParams } from "@plane/types";
|
import { IAnalyticsParams } from "@plane/types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { ANALYTICS } from "constants/fetch-keys";
|
import { ANALYTICS } from "constants/fetch-keys";
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
import { useApplication } from "hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
additionalParams?: Partial<IAnalyticsParams>;
|
additionalParams?: Partial<IAnalyticsParams>;
|
||||||
@ -46,11 +48,13 @@ export const CustomAnalytics: React.FC<Props> = observer((props) => {
|
|||||||
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
|
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { theme: themeStore } = useApplication();
|
||||||
|
|
||||||
const isProjectLevel = projectId ? true : false;
|
const isProjectLevel = projectId ? true : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col-reverse overflow-hidden ${fullScreen ? "md:grid md:h-full md:grid-cols-4" : ""}`}>
|
<div className={cn("relative w-full h-full flex overflow-hidden", isProjectLevel ? "flex-col-reverse" : "")}>
|
||||||
<div className="col-span-3 flex h-full flex-col overflow-hidden">
|
<div className="w-full flex h-full flex-col overflow-hidden">
|
||||||
<CustomAnalyticsSelectBar
|
<CustomAnalyticsSelectBar
|
||||||
control={control}
|
control={control}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
@ -61,16 +65,22 @@ export const CustomAnalytics: React.FC<Props> = observer((props) => {
|
|||||||
<CustomAnalyticsMainContent
|
<CustomAnalyticsMainContent
|
||||||
analytics={analytics}
|
analytics={analytics}
|
||||||
error={analyticsError}
|
error={analyticsError}
|
||||||
fullScreen={fullScreen}
|
|
||||||
params={params}
|
params={params}
|
||||||
|
fullScreen={fullScreen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CustomAnalyticsSidebar
|
|
||||||
analytics={analytics}
|
<div
|
||||||
params={params}
|
className={cn(
|
||||||
fullScreen={fullScreen}
|
"border-l border-custom-border-200 transition-all",
|
||||||
isProjectLevel={isProjectLevel}
|
!isProjectLevel
|
||||||
/>
|
? "absolute right-0 top-0 bottom-0 md:relative flex-shrink-0 h-full max-w-[250px] sm:max-w-full"
|
||||||
|
: ""
|
||||||
|
)}
|
||||||
|
style={themeStore.workspaceAnalyticsSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
|
||||||
|
>
|
||||||
|
<CustomAnalyticsSidebar analytics={analytics} params={params} isProjectLevel={isProjectLevel} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -22,8 +22,7 @@ export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`grid items-center gap-4 px-5 py-2.5 ${isProjectLevel ? "grid-cols-3" : "grid-cols-2"} ${
|
className={`grid items-center gap-4 px-5 py-2.5 ${isProjectLevel ? "grid-cols-1 sm:grid-cols-3" : "grid-cols-2"} ${fullScreen ? "md:py-5 lg:grid-cols-4" : ""
|
||||||
fullScreen ? "md:py-5 lg:grid-cols-4" : ""
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{!isProjectLevel && (
|
{!isProjectLevel && (
|
||||||
|
@ -17,9 +17,9 @@ export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((pro
|
|||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hidden h-full overflow-hidden md:flex md:flex-col">
|
<div className="relative flex flex-col gap-4 h-full">
|
||||||
<h4 className="font-medium">Selected Projects</h4>
|
<h4 className="font-medium">Selected Projects</h4>
|
||||||
<div className="mt-4 h-full space-y-6 overflow-y-auto">
|
<div className="relative space-y-6 overflow-hidden overflow-y-auto">
|
||||||
{projectIds.map((projectId) => {
|
{projectIds.map((projectId) => {
|
||||||
const project = getProjectById(projectId);
|
const project = getProjectById(projectId);
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
|
|||||||
<>
|
<>
|
||||||
{projectId ? (
|
{projectId ? (
|
||||||
cycleDetails ? (
|
cycleDetails ? (
|
||||||
<div className="hidden h-full overflow-y-auto md:block">
|
<div className="h-full overflow-y-auto">
|
||||||
<h4 className="break-words font-medium">Analytics for {cycleDetails.name}</h4>
|
<h4 className="break-words font-medium">Analytics for {cycleDetails.name}</h4>
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
@ -52,7 +52,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : moduleDetails ? (
|
) : moduleDetails ? (
|
||||||
<div className="hidden h-full overflow-y-auto md:block">
|
<div className="h-full overflow-y-auto">
|
||||||
<h4 className="break-words font-medium">Analytics for {moduleDetails.name}</h4>
|
<h4 className="break-words font-medium">Analytics for {moduleDetails.name}</h4>
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
@ -78,7 +78,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="hidden h-full overflow-y-auto md:flex md:flex-col">
|
<div className="h-full overflow-y-auto">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{projectDetails?.emoji ? (
|
{projectDetails?.emoji ? (
|
||||||
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(projectDetails.emoji)}</div>
|
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(projectDetails.emoji)}</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
@ -19,18 +19,18 @@ import { renderFormattedDate } from "helpers/date-time.helper";
|
|||||||
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types";
|
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { ANALYTICS } from "constants/fetch-keys";
|
import { ANALYTICS } from "constants/fetch-keys";
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
analytics: IAnalyticsResponse | undefined;
|
analytics: IAnalyticsResponse | undefined;
|
||||||
params: IAnalyticsParams;
|
params: IAnalyticsParams;
|
||||||
fullScreen: boolean;
|
|
||||||
isProjectLevel: boolean;
|
isProjectLevel: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const analyticsService = new AnalyticsService();
|
const analyticsService = new AnalyticsService();
|
||||||
|
|
||||||
export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||||
const { analytics, params, fullScreen, isProjectLevel = false } = props;
|
const { analytics, params, isProjectLevel = false } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
@ -138,18 +138,14 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjectIds;
|
const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjectIds;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn("relative h-full flex w-full gap-2 justify-between items-start px-5 py-4 bg-custom-sidebar-background-100", !isProjectLevel ? "flex-col" : "")}
|
||||||
className={`flex items-center justify-between space-y-2 px-5 py-2.5 ${
|
|
||||||
fullScreen
|
|
||||||
? "overflow-hidden border-l border-custom-border-200 md:h-full md:flex-col md:items-start md:space-y-4 md:border-l md:border-custom-border-200 md:py-5"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
||||||
<LayersIcon height={14} width={14} />
|
<LayersIcon height={14} width={14} />
|
||||||
{analytics ? analytics.total : "..."} Issues
|
{analytics ? analytics.total : "..."} <div className={cn(isProjectLevel ? "hidden md:block" : "")}>Issues</div>
|
||||||
</div>
|
</div>
|
||||||
{isProjectLevel && (
|
{isProjectLevel && (
|
||||||
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
||||||
@ -164,30 +160,30 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-full overflow-hidden">
|
|
||||||
{fullScreen ? (
|
<div className={cn("h-full w-full overflow-hidden", isProjectLevel ? "hidden" : "block")}>
|
||||||
<>
|
<>
|
||||||
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
||||||
<CustomAnalyticsSidebarProjectsList projectIds={selectedProjects} />
|
<CustomAnalyticsSidebarProjectsList projectIds={selectedProjects} />
|
||||||
)}
|
)}
|
||||||
<CustomAnalyticsSidebarHeader />
|
<CustomAnalyticsSidebarHeader />
|
||||||
</>
|
</>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2 justify-self-end">
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2 justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="neutral-primary"
|
variant="neutral-primary"
|
||||||
prependIcon={<RefreshCw className="h-3.5 w-3.5" />}
|
prependIcon={<RefreshCw className="h-3 md:h-3.5 w-3 md:w-3.5" />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Refresh
|
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Refresh</div>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" prependIcon={<Download className="h-3.5 w-3.5" />} onClick={exportAnalytics}>
|
<Button variant="primary" prependIcon={<Download className="h-3.5 w-3.5" />} onClick={exportAnalytics}>
|
||||||
Export as CSV
|
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Export as CSV</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,16 +20,15 @@ export const ProjectAnalyticsModalMainContent: React.FC<Props> = observer((props
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Group as={React.Fragment}>
|
<Tab.Group as={React.Fragment}>
|
||||||
<Tab.List as="div" className="space-x-2 border-b border-custom-border-200 p-5 pt-0">
|
<Tab.List as="div" className="flex space-x-2 border-b border-custom-border-200 px-0 md:px-5 py-0 md:py-3">
|
||||||
{ANALYTICS_TABS.map((tab) => (
|
{ANALYTICS_TABS.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
`rounded-0 w-full md:w-max md:rounded-3xl border-b md:border border-custom-border-200 focus:outline-none px-0 md:px-4 py-2 text-xs hover:bg-custom-background-80 ${selected ? "border-custom-primary-100 text-custom-primary-100 md:bg-custom-background-80 md:text-custom-text-200 md:border-custom-border-200" : "border-transparent"
|
||||||
selected ? "bg-custom-background-80" : ""
|
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
onClick={() => {}}
|
onClick={() => { }}
|
||||||
>
|
>
|
||||||
{tab.title}
|
{tab.title}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
80
web/components/core/render-if-visible-HOC.tsx
Normal file
80
web/components/core/render-if-visible-HOC.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
import React, { useState, useRef, useEffect, ReactNode, MutableRefObject } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultHeight?: string;
|
||||||
|
verticalOffset?: number;
|
||||||
|
horizonatlOffset?: number;
|
||||||
|
root?: MutableRefObject<HTMLElement | null>;
|
||||||
|
children: ReactNode;
|
||||||
|
as?: keyof JSX.IntrinsicElements;
|
||||||
|
classNames?: string;
|
||||||
|
alwaysRender?: boolean;
|
||||||
|
placeholderChildren?: ReactNode;
|
||||||
|
pauseHeightUpdateWhileRendering?: boolean;
|
||||||
|
changingReference?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RenderIfVisible: React.FC<Props> = (props) => {
|
||||||
|
const {
|
||||||
|
defaultHeight = "300px",
|
||||||
|
root,
|
||||||
|
verticalOffset = 50,
|
||||||
|
horizonatlOffset = 0,
|
||||||
|
as = "div",
|
||||||
|
children,
|
||||||
|
classNames = "",
|
||||||
|
alwaysRender = false, //render the children even if it is not visble in root
|
||||||
|
placeholderChildren = null, //placeholder children
|
||||||
|
pauseHeightUpdateWhileRendering = false, //while this is true the height of the blocks are maintained
|
||||||
|
changingReference, //This is to force render when this reference is changed
|
||||||
|
} = props;
|
||||||
|
const [shouldVisible, setShouldVisible] = useState<boolean>(alwaysRender);
|
||||||
|
const placeholderHeight = useRef<string>(defaultHeight);
|
||||||
|
const intersectionRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const isVisible = alwaysRender || shouldVisible;
|
||||||
|
|
||||||
|
// Set visibility with intersection observer
|
||||||
|
useEffect(() => {
|
||||||
|
if (intersectionRef.current) {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (typeof window !== undefined && window.requestIdleCallback) {
|
||||||
|
window.requestIdleCallback(() => setShouldVisible(entries[0].isIntersecting), {
|
||||||
|
timeout: 300,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setShouldVisible(entries[0].isIntersecting);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: root?.current,
|
||||||
|
rootMargin: `${verticalOffset}% ${horizonatlOffset}% ${verticalOffset}% ${horizonatlOffset}%`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
observer.observe(intersectionRef.current);
|
||||||
|
return () => {
|
||||||
|
if (intersectionRef.current) {
|
||||||
|
observer.unobserve(intersectionRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [root?.current, intersectionRef, children, changingReference]);
|
||||||
|
|
||||||
|
//Set height after render
|
||||||
|
useEffect(() => {
|
||||||
|
if (intersectionRef.current && isVisible) {
|
||||||
|
placeholderHeight.current = `${intersectionRef.current.offsetHeight}px`;
|
||||||
|
}
|
||||||
|
}, [isVisible, intersectionRef, alwaysRender, pauseHeightUpdateWhileRendering]);
|
||||||
|
|
||||||
|
const child = isVisible ? <>{children}</> : placeholderChildren;
|
||||||
|
const style =
|
||||||
|
isVisible && !pauseHeightUpdateWhileRendering ? {} : { height: placeholderHeight.current, width: "100%" };
|
||||||
|
const className = isVisible ? classNames : cn(classNames, "bg-custom-background-80");
|
||||||
|
|
||||||
|
return React.createElement(as, { ref: intersectionRef, style, className }, child);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RenderIfVisible;
|
@ -3,7 +3,7 @@ import { Menu } from "lucide-react";
|
|||||||
import { useApplication } from "hooks/store";
|
import { useApplication } from "hooks/store";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
|
||||||
export const SidebarHamburgerToggle: FC = observer (() => {
|
export const SidebarHamburgerToggle: FC = observer(() => {
|
||||||
const { theme: themStore } = useApplication();
|
const { theme: themStore } = useApplication();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -16,6 +16,7 @@ import { copyTextToClipboard } from "helpers/string.helper";
|
|||||||
// constants
|
// constants
|
||||||
import { CYCLE_STATUS } from "constants/cycle";
|
import { CYCLE_STATUS } from "constants/cycle";
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker";
|
||||||
//.types
|
//.types
|
||||||
import { TCycleGroups } from "@plane/types";
|
import { TCycleGroups } from "@plane/types";
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// store
|
// store
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
@ -90,7 +91,15 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
|
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
|
||||||
|
.then(() => {
|
||||||
|
captureEvent(CYCLE_FAVORITED, {
|
||||||
|
cycle_id: cycleId,
|
||||||
|
element: "Grid layout",
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -103,7 +112,15 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
|
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
|
||||||
|
.then(() => {
|
||||||
|
captureEvent(CYCLE_UNFAVORITED, {
|
||||||
|
cycle_id: cycleId,
|
||||||
|
element: "Grid layout",
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -115,14 +132,14 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
const handleEditCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleEditCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setTrackElement("Cycles page board layout");
|
setTrackElement("Cycles page grid layout");
|
||||||
setUpdateModal(true);
|
setUpdateModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleDeleteCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setTrackElement("Cycles page board layout");
|
setTrackElement("Cycles page grid layout");
|
||||||
setDeleteModal(true);
|
setDeleteModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import { CYCLE_STATUS } from "constants/cycle";
|
|||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
// types
|
// types
|
||||||
import { TCycleGroups } from "@plane/types";
|
import { TCycleGroups } from "@plane/types";
|
||||||
|
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker";
|
||||||
|
|
||||||
type TCyclesListItem = {
|
type TCyclesListItem = {
|
||||||
cycleId: string;
|
cycleId: string;
|
||||||
@ -37,7 +38,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
@ -63,7 +64,15 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
|
addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
|
||||||
|
.then(() => {
|
||||||
|
captureEvent(CYCLE_FAVORITED, {
|
||||||
|
cycle_id: cycleId,
|
||||||
|
element: "List layout",
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -76,7 +85,15 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
|
removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
|
||||||
|
.then(() => {
|
||||||
|
captureEvent(CYCLE_UNFAVORITED, {
|
||||||
|
cycle_id: cycleId,
|
||||||
|
element: "List layout",
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -159,9 +176,9 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
/>
|
/>
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
|
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
|
||||||
<div className="group flex flex-col md:flex-row w-full items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90">
|
<div className="group flex w-full flex-col items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90 md:flex-row">
|
||||||
<div className="relative w-full flex items-center justify-between gap-3 overflow-hidden">
|
<div className="relative flex w-full items-center justify-between gap-3 overflow-hidden">
|
||||||
<div className="relative w-full flex items-center gap-3 overflow-hidden">
|
<div className="relative flex w-full items-center gap-3 overflow-hidden">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<CircularProgressIndicator size={38} percentage={progress}>
|
<CircularProgressIndicator size={38} percentage={progress}>
|
||||||
{isCompleted ? (
|
{isCompleted ? (
|
||||||
@ -181,20 +198,20 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
<div className="relative flex items-center gap-2.5 overflow-hidden">
|
<div className="relative flex items-center gap-2.5 overflow-hidden">
|
||||||
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
|
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
<Tooltip tooltipContent={cycleDetails.name} position="top">
|
<Tooltip tooltipContent={cycleDetails.name} position="top">
|
||||||
<span className="truncate line-clamp-1 inline-block overflow-hidden text-base font-medium">
|
<span className="line-clamp-1 inline-block overflow-hidden truncate text-base font-medium">
|
||||||
{cycleDetails.name}
|
{cycleDetails.name}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onClick={openCycleOverview} className="flex-shrink-0 z-10 invisible group-hover:visible">
|
<button onClick={openCycleOverview} className="invisible z-10 flex-shrink-0 group-hover:visible">
|
||||||
<Info className="h-4 w-4 text-custom-text-400" />
|
<Info className="h-4 w-4 text-custom-text-400" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{currentCycle && (
|
{currentCycle && (
|
||||||
<div
|
<div
|
||||||
className="flex-shrink-0 relative flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs"
|
className="relative flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs"
|
||||||
style={{
|
style={{
|
||||||
color: currentCycle.color,
|
color: currentCycle.color,
|
||||||
backgroundColor: `${currentCycle.color}20`,
|
backgroundColor: `${currentCycle.color}20`,
|
||||||
@ -206,12 +223,12 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 relative overflow-hidden flex w-full items-center justify-between md:justify-end gap-2.5 md:w-auto md:flex-shrink-0 ">
|
<div className="relative flex w-full flex-shrink-0 items-center justify-between gap-2.5 overflow-hidden md:w-auto md:flex-shrink-0 md:justify-end ">
|
||||||
<div className="text-xs text-custom-text-300">
|
<div className="text-xs text-custom-text-300">
|
||||||
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
|
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-shrink-0 relative flex items-center gap-3">
|
<div className="relative flex flex-shrink-0 items-center gap-3">
|
||||||
<Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
|
<Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
|
||||||
<div className="flex w-10 cursor-default items-center justify-center">
|
<div className="flex w-10 cursor-default items-center justify-center">
|
||||||
{cycleDetails.assignees.length > 0 ? (
|
{cycleDetails.assignees.length > 0 ? (
|
||||||
|
@ -10,6 +10,8 @@ import useToast from "hooks/use-toast";
|
|||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { ICycle } from "@plane/types";
|
import { ICycle } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { CYCLE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
interface ICycleDelete {
|
interface ICycleDelete {
|
||||||
cycle: ICycle;
|
cycle: ICycle;
|
||||||
@ -45,13 +47,13 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
|||||||
message: "Cycle deleted successfully.",
|
message: "Cycle deleted successfully.",
|
||||||
});
|
});
|
||||||
captureCycleEvent({
|
captureCycleEvent({
|
||||||
eventName: "Cycle deleted",
|
eventName: CYCLE_DELETED,
|
||||||
payload: { ...cycle, state: "SUCCESS" },
|
payload: { ...cycle, state: "SUCCESS" },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
captureCycleEvent({
|
captureCycleEvent({
|
||||||
eventName: "Cycle deleted",
|
eventName: CYCLE_DELETED,
|
||||||
payload: { ...cycle, state: "FAILED" },
|
payload: { ...cycle, state: "FAILED" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
|||||||
import { ICycle } from "@plane/types";
|
import { ICycle } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
|
handleFormSubmit: (values: Partial<ICycle>, dirtyFields: any) => Promise<void>;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
status: boolean;
|
status: boolean;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -29,7 +29,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
|||||||
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props;
|
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props;
|
||||||
// form data
|
// form data
|
||||||
const {
|
const {
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting, dirtyFields },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
@ -61,7 +61,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
|||||||
maxDate?.setDate(maxDate.getDate() - 1);
|
maxDate?.setDate(maxDate.getDate() - 1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit((formData)=>handleFormSubmit(formData,dirtyFields))}>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
{!status && (
|
{!status && (
|
||||||
|
@ -10,6 +10,8 @@ import useLocalStorage from "hooks/use-local-storage";
|
|||||||
import { CycleForm } from "components/cycles";
|
import { CycleForm } from "components/cycles";
|
||||||
// types
|
// types
|
||||||
import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types";
|
import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { CYCLE_CREATED, CYCLE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type CycleModalProps = {
|
type CycleModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -47,7 +49,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
message: "Cycle created successfully.",
|
message: "Cycle created successfully.",
|
||||||
});
|
});
|
||||||
captureCycleEvent({
|
captureCycleEvent({
|
||||||
eventName: "Cycle created",
|
eventName: CYCLE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS" },
|
payload: { ...res, state: "SUCCESS" },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -58,18 +60,23 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
message: err.detail ?? "Error in creating cycle. Please try again.",
|
message: err.detail ?? "Error in creating cycle. Please try again.",
|
||||||
});
|
});
|
||||||
captureCycleEvent({
|
captureCycleEvent({
|
||||||
eventName: "Cycle created",
|
eventName: CYCLE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED" },
|
payload: { ...payload, state: "FAILED" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>) => {
|
const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>, dirtyFields: any) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const selectedProjectId = payload.project ?? projectId.toString();
|
const selectedProjectId = payload.project ?? projectId.toString();
|
||||||
await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
|
await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
|
||||||
.then(() => {
|
.then((res) => {
|
||||||
|
const changed_properties = Object.keys(dirtyFields);
|
||||||
|
captureCycleEvent({
|
||||||
|
eventName: CYCLE_UPDATED,
|
||||||
|
payload: { ...res, changed_properties: changed_properties, state: "SUCCESS" },
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -77,6 +84,10 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
captureCycleEvent({
|
||||||
|
eventName: CYCLE_UPDATED,
|
||||||
|
payload: { ...payload, state: "FAILED" },
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -95,7 +106,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
return status;
|
return status;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: Partial<ICycle>) => {
|
const handleFormSubmit = async (formData: Partial<ICycle>, dirtyFields: any) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const payload: Partial<ICycle> = {
|
const payload: Partial<ICycle> = {
|
||||||
@ -119,7 +130,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDateValid) {
|
if (isDateValid) {
|
||||||
if (data) await handleUpdateCycle(data.id, payload);
|
if (data) await handleUpdateCycle(data.id, payload, dirtyFields);
|
||||||
else {
|
else {
|
||||||
await handleCreateCycle(payload).then(() => {
|
await handleCreateCycle(payload).then(() => {
|
||||||
setCycleTab("all");
|
setCycleTab("all");
|
||||||
|
@ -3,6 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
||||||
|
import isEmpty from "lodash/isEmpty";
|
||||||
// services
|
// services
|
||||||
import { CycleService } from "services/cycle.service";
|
import { CycleService } from "services/cycle.service";
|
||||||
// hooks
|
// hooks
|
||||||
@ -38,6 +39,7 @@ import {
|
|||||||
import { ICycle } from "@plane/types";
|
import { ICycle } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { CYCLE_UPDATED } from "constants/event-tracker";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { CYCLE_STATUS } from "constants/cycle";
|
import { CYCLE_STATUS } from "constants/cycle";
|
||||||
|
|
||||||
@ -66,7 +68,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, peekCycle } = router.query;
|
const { workspaceSlug, projectId, peekCycle } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureCycleEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
@ -82,10 +84,32 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitChanges = (data: Partial<ICycle>) => {
|
const submitChanges = (data: Partial<ICycle>, changedProperty: string) => {
|
||||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||||
|
|
||||||
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
|
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data)
|
||||||
|
.then((res) => {
|
||||||
|
captureCycleEvent({
|
||||||
|
eventName: CYCLE_UPDATED,
|
||||||
|
payload: {
|
||||||
|
...res,
|
||||||
|
changed_properties: [changedProperty],
|
||||||
|
element: "Right side-peek",
|
||||||
|
state: "SUCCESS",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
.catch((_) => {
|
||||||
|
captureCycleEvent({
|
||||||
|
eventName: CYCLE_UPDATED,
|
||||||
|
payload: {
|
||||||
|
...data,
|
||||||
|
element: "Right side-peek",
|
||||||
|
state: "FAILED",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const handleCopyText = () => {
|
||||||
@ -145,10 +169,13 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isDateValidForExistingCycle) {
|
if (isDateValidForExistingCycle) {
|
||||||
submitChanges({
|
submitChanges(
|
||||||
|
{
|
||||||
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
||||||
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
||||||
});
|
},
|
||||||
|
"start_date"
|
||||||
|
);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -173,10 +200,13 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isDateValid) {
|
if (isDateValid) {
|
||||||
submitChanges({
|
submitChanges(
|
||||||
|
{
|
||||||
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
||||||
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
||||||
});
|
},
|
||||||
|
"start_date"
|
||||||
|
);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -218,10 +248,13 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isDateValidForExistingCycle) {
|
if (isDateValidForExistingCycle) {
|
||||||
submitChanges({
|
submitChanges(
|
||||||
|
{
|
||||||
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
||||||
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
||||||
});
|
},
|
||||||
|
"end_date"
|
||||||
|
);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -245,10 +278,13 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isDateValid) {
|
if (isDateValid) {
|
||||||
submitChanges({
|
submitChanges(
|
||||||
|
{
|
||||||
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
||||||
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
|
||||||
});
|
},
|
||||||
|
"end_date"
|
||||||
|
);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -293,7 +329,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const isEndValid = new Date(`${cycleDetails?.end_date}`) >= new Date(`${cycleDetails?.start_date}`);
|
const isEndValid = new Date(`${cycleDetails?.end_date}`) >= new Date(`${cycleDetails?.start_date}`);
|
||||||
|
|
||||||
const progressPercentage = cycleDetails
|
const progressPercentage = cycleDetails
|
||||||
? Math.round((cycleDetails.completed_issues / cycleDetails.total_issues) * 100)
|
? isCompleted
|
||||||
|
? Math.round(
|
||||||
|
(cycleDetails.progress_snapshot.completed_issues / cycleDetails.progress_snapshot.total_issues) * 100
|
||||||
|
)
|
||||||
|
: Math.round((cycleDetails.completed_issues / cycleDetails.total_issues) * 100)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!cycleDetails)
|
if (!cycleDetails)
|
||||||
@ -317,7 +357,14 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
||||||
|
|
||||||
const issueCount =
|
const issueCount =
|
||||||
cycleDetails.total_issues === 0 ? "0 Issue" : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
|
isCompleted && !isEmpty(cycleDetails.progress_snapshot)
|
||||||
|
? cycleDetails.progress_snapshot.total_issues === 0
|
||||||
|
? "0 Issue"
|
||||||
|
: `${cycleDetails.progress_snapshot.completed_issues}/${cycleDetails.progress_snapshot.total_issues}`
|
||||||
|
: cycleDetails.total_issues === 0
|
||||||
|
? "0 Issue"
|
||||||
|
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
|
||||||
|
|
||||||
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date);
|
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date);
|
||||||
|
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
@ -568,9 +615,40 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<Transition show={open}>
|
<Transition show={open}>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
{isCompleted && !isEmpty(cycleDetails.progress_snapshot) ? (
|
||||||
|
<>
|
||||||
|
{cycleDetails.progress_snapshot.distribution?.completion_chart &&
|
||||||
|
cycleDetails.start_date &&
|
||||||
|
cycleDetails.end_date && (
|
||||||
|
<div className="h-full w-full pt-4">
|
||||||
|
<div className="flex items-start gap-4 py-2 text-xs">
|
||||||
|
<div className="flex items-center gap-3 text-custom-text-100">
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
|
||||||
|
<span>Ideal</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
|
||||||
|
<span>Current</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative h-40 w-80">
|
||||||
|
<ProgressChart
|
||||||
|
distribution={cycleDetails.progress_snapshot.distribution?.completion_chart}
|
||||||
|
startDate={cycleDetails.start_date}
|
||||||
|
endDate={cycleDetails.end_date}
|
||||||
|
totalIssues={cycleDetails.progress_snapshot.total_issues}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{cycleDetails.distribution?.completion_chart &&
|
{cycleDetails.distribution?.completion_chart &&
|
||||||
cycleDetails.start_date &&
|
cycleDetails.start_date &&
|
||||||
cycleDetails.end_date ? (
|
cycleDetails.end_date && (
|
||||||
<div className="h-full w-full pt-4">
|
<div className="h-full w-full pt-4">
|
||||||
<div className="flex items-start gap-4 py-2 text-xs">
|
<div className="flex items-start gap-4 py-2 text-xs">
|
||||||
<div className="flex items-center gap-3 text-custom-text-100">
|
<div className="flex items-center gap-3 text-custom-text-100">
|
||||||
@ -593,9 +671,32 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* stats */}
|
||||||
|
{isCompleted && !isEmpty(cycleDetails.progress_snapshot) ? (
|
||||||
|
<>
|
||||||
|
{cycleDetails.progress_snapshot.total_issues > 0 &&
|
||||||
|
cycleDetails.progress_snapshot.distribution && (
|
||||||
|
<div className="h-full w-full border-t border-custom-border-200 pt-5">
|
||||||
|
<SidebarProgressStats
|
||||||
|
distribution={cycleDetails.progress_snapshot.distribution}
|
||||||
|
groupedIssues={{
|
||||||
|
backlog: cycleDetails.progress_snapshot.backlog_issues,
|
||||||
|
unstarted: cycleDetails.progress_snapshot.unstarted_issues,
|
||||||
|
started: cycleDetails.progress_snapshot.started_issues,
|
||||||
|
completed: cycleDetails.progress_snapshot.completed_issues,
|
||||||
|
cancelled: cycleDetails.progress_snapshot.cancelled_issues,
|
||||||
|
}}
|
||||||
|
totalIssues={cycleDetails.progress_snapshot.total_issues}
|
||||||
|
isPeekView={Boolean(peekCycle)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{cycleDetails.total_issues > 0 && cycleDetails.distribution && (
|
{cycleDetails.total_issues > 0 && cycleDetails.distribution && (
|
||||||
<div className="h-full w-full border-t border-custom-border-200 pt-5">
|
<div className="h-full w-full border-t border-custom-border-200 pt-5">
|
||||||
<SidebarProgressStats
|
<SidebarProgressStats
|
||||||
@ -612,6 +713,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
WidgetProps,
|
WidgetProps,
|
||||||
} from "components/dashboard/widgets";
|
} from "components/dashboard/widgets";
|
||||||
// helpers
|
// helpers
|
||||||
import { getCustomDates, getRedirectionFilters } from "helpers/dashboard.helper";
|
import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper";
|
||||||
// types
|
// types
|
||||||
import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types";
|
import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
@ -30,8 +30,8 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
|||||||
// derived values
|
// derived values
|
||||||
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
|
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||||
const widgetStats = getWidgetStats<TAssignedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
|
const widgetStats = getWidgetStats<TAssignedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||||
const selectedTab = widgetDetails?.widget_filters.tab ?? "pending";
|
const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? "none";
|
||||||
const selectedDurationFilter = widgetDetails?.widget_filters.target_date ?? "none";
|
const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab);
|
||||||
|
|
||||||
const handleUpdateFilters = async (filters: Partial<TAssignedIssuesWidgetFilters>) => {
|
const handleUpdateFilters = async (filters: Partial<TAssignedIssuesWidgetFilters>) => {
|
||||||
if (!widgetDetails) return;
|
if (!widgetDetails) return;
|
||||||
@ -43,7 +43,7 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
|||||||
filters,
|
filters,
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterDates = getCustomDates(filters.target_date ?? selectedDurationFilter);
|
const filterDates = getCustomDates(filters.duration ?? selectedDurationFilter);
|
||||||
fetchWidgetStats(workspaceSlug, dashboardId, {
|
fetchWidgetStats(workspaceSlug, dashboardId, {
|
||||||
widget_key: WIDGET_KEY,
|
widget_key: WIDGET_KEY,
|
||||||
issue_type: filters.tab ?? selectedTab,
|
issue_type: filters.tab ?? selectedTab,
|
||||||
@ -86,19 +86,19 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
|||||||
|
|
||||||
// switch to pending tab if target date is changed to none
|
// switch to pending tab if target date is changed to none
|
||||||
if (val === "none" && selectedTab !== "completed") {
|
if (val === "none" && selectedTab !== "completed") {
|
||||||
handleUpdateFilters({ target_date: val, tab: "pending" });
|
handleUpdateFilters({ duration: val, tab: "pending" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// switch to upcoming tab if target date is changed to other than none
|
// switch to upcoming tab if target date is changed to other than none
|
||||||
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") {
|
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") {
|
||||||
handleUpdateFilters({
|
handleUpdateFilters({
|
||||||
target_date: val,
|
duration: val,
|
||||||
tab: "upcoming",
|
tab: "upcoming",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateFilters({ target_date: val });
|
handleUpdateFilters({ duration: val });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
WidgetProps,
|
WidgetProps,
|
||||||
} from "components/dashboard/widgets";
|
} from "components/dashboard/widgets";
|
||||||
// helpers
|
// helpers
|
||||||
import { getCustomDates, getRedirectionFilters } from "helpers/dashboard.helper";
|
import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper";
|
||||||
// types
|
// types
|
||||||
import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types";
|
import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
@ -30,8 +30,8 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
|||||||
// derived values
|
// derived values
|
||||||
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
|
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||||
const widgetStats = getWidgetStats<TCreatedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
|
const widgetStats = getWidgetStats<TCreatedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||||
const selectedTab = widgetDetails?.widget_filters.tab ?? "pending";
|
const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? "none";
|
||||||
const selectedDurationFilter = widgetDetails?.widget_filters.target_date ?? "none";
|
const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab);
|
||||||
|
|
||||||
const handleUpdateFilters = async (filters: Partial<TCreatedIssuesWidgetFilters>) => {
|
const handleUpdateFilters = async (filters: Partial<TCreatedIssuesWidgetFilters>) => {
|
||||||
if (!widgetDetails) return;
|
if (!widgetDetails) return;
|
||||||
@ -43,7 +43,7 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
|||||||
filters,
|
filters,
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterDates = getCustomDates(filters.target_date ?? selectedDurationFilter);
|
const filterDates = getCustomDates(filters.duration ?? selectedDurationFilter);
|
||||||
fetchWidgetStats(workspaceSlug, dashboardId, {
|
fetchWidgetStats(workspaceSlug, dashboardId, {
|
||||||
widget_key: WIDGET_KEY,
|
widget_key: WIDGET_KEY,
|
||||||
issue_type: filters.tab ?? selectedTab,
|
issue_type: filters.tab ?? selectedTab,
|
||||||
@ -83,19 +83,19 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
|||||||
|
|
||||||
// switch to pending tab if target date is changed to none
|
// switch to pending tab if target date is changed to none
|
||||||
if (val === "none" && selectedTab !== "completed") {
|
if (val === "none" && selectedTab !== "completed") {
|
||||||
handleUpdateFilters({ target_date: val, tab: "pending" });
|
handleUpdateFilters({ duration: val, tab: "pending" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// switch to upcoming tab if target date is changed to other than none
|
// switch to upcoming tab if target date is changed to other than none
|
||||||
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") {
|
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") {
|
||||||
handleUpdateFilters({
|
handleUpdateFilters({
|
||||||
target_date: val,
|
duration: val,
|
||||||
tab: "upcoming",
|
tab: "upcoming",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateFilters({ target_date: val });
|
handleUpdateFilters({ duration: val });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,42 +16,40 @@ export const TabsList: React.FC<Props> = observer((props) => {
|
|||||||
const { durationFilter, selectedTab } = props;
|
const { durationFilter, selectedTab } = props;
|
||||||
|
|
||||||
const tabsList = durationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
|
const tabsList = durationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
|
||||||
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === (selectedTab ?? "pending"));
|
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.List
|
<Tab.List
|
||||||
as="div"
|
as="div"
|
||||||
className="relative border-[0.5px] border-custom-border-200 rounded bg-custom-background-80 grid"
|
className="relative border-[0.5px] border-custom-border-200 rounded bg-custom-background-80 p-[1px] grid"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: `repeat(${tabsList.length}, 1fr)`,
|
gridTemplateColumns: `repeat(${tabsList.length}, 1fr)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn("absolute bg-custom-background-100 rounded transition-all duration-500 ease-in-out", {
|
className={cn(
|
||||||
|
"absolute top-1/2 left-[1px] bg-custom-background-100 rounded-[3px] transition-all duration-500 ease-in-out",
|
||||||
|
{
|
||||||
// right shadow
|
// right shadow
|
||||||
"shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== tabsList.length - 1,
|
"shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== tabsList.length - 1,
|
||||||
// left shadow
|
// left shadow
|
||||||
"shadow-[-2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== 0,
|
"shadow-[-2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== 0,
|
||||||
})}
|
}
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
height: "calc(100% - 1px)",
|
height: "calc(100% - 2px)",
|
||||||
width: `${100 / tabsList.length}%`,
|
width: `calc(${100 / tabsList.length}% - 1px)`,
|
||||||
transform: `translateX(${selectedTabIndex * 100}%)`,
|
transform: `translate(${selectedTabIndex * 100}%, -50%)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{tabsList.map((tab) => (
|
{tabsList.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-[1] font-semibold text-xs rounded py-1.5 text-custom-text-400 focus:outline-none",
|
"relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500",
|
||||||
"transition duration-500",
|
|
||||||
{
|
{
|
||||||
"text-custom-text-100 bg-custom-background-100": selectedTab === tab.key,
|
"text-custom-text-100 bg-custom-background-100": selectedTab === tab.key,
|
||||||
"hover:text-custom-text-300": selectedTab !== tab.key,
|
"hover:text-custom-text-300": selectedTab !== tab.key,
|
||||||
// // right shadow
|
|
||||||
// "shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== tabsList.length - 1,
|
|
||||||
// // left shadow
|
|
||||||
// "shadow-[-2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== 0,
|
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -73,8 +73,10 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
|
|||||||
const { dashboardId, workspaceSlug } = props;
|
const { dashboardId, workspaceSlug } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard();
|
const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard();
|
||||||
|
// derived values
|
||||||
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
|
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||||
const widgetStats = getWidgetStats<TIssuesByPriorityWidgetResponse[]>(workspaceSlug, dashboardId, WIDGET_KEY);
|
const widgetStats = getWidgetStats<TIssuesByPriorityWidgetResponse[]>(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||||
|
const selectedDuration = widgetDetails?.widget_filters.duration ?? "none";
|
||||||
|
|
||||||
const handleUpdateFilters = async (filters: Partial<TIssuesByPriorityWidgetFilters>) => {
|
const handleUpdateFilters = async (filters: Partial<TIssuesByPriorityWidgetFilters>) => {
|
||||||
if (!widgetDetails) return;
|
if (!widgetDetails) return;
|
||||||
@ -84,7 +86,7 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
|
|||||||
filters,
|
filters,
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterDates = getCustomDates(filters.target_date ?? widgetDetails.widget_filters.target_date ?? "none");
|
const filterDates = getCustomDates(filters.duration ?? selectedDuration);
|
||||||
fetchWidgetStats(workspaceSlug, dashboardId, {
|
fetchWidgetStats(workspaceSlug, dashboardId, {
|
||||||
widget_key: WIDGET_KEY,
|
widget_key: WIDGET_KEY,
|
||||||
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
|
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
|
||||||
@ -92,7 +94,7 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filterDates = getCustomDates(widgetDetails?.widget_filters.target_date ?? "none");
|
const filterDates = getCustomDates(selectedDuration);
|
||||||
fetchWidgetStats(workspaceSlug, dashboardId, {
|
fetchWidgetStats(workspaceSlug, dashboardId, {
|
||||||
widget_key: WIDGET_KEY,
|
widget_key: WIDGET_KEY,
|
||||||
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
|
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
|
||||||
@ -139,10 +141,10 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
|
|||||||
Assigned by priority
|
Assigned by priority
|
||||||
</Link>
|
</Link>
|
||||||
<DurationFilterDropdown
|
<DurationFilterDropdown
|
||||||
value={widgetDetails.widget_filters.target_date ?? "none"}
|
value={selectedDuration}
|
||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
handleUpdateFilters({
|
handleUpdateFilters({
|
||||||
target_date: val,
|
duration: val,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -34,6 +34,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
|||||||
// derived values
|
// derived values
|
||||||
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
|
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||||
const widgetStats = getWidgetStats<TIssuesByStateGroupsWidgetResponse[]>(workspaceSlug, dashboardId, WIDGET_KEY);
|
const widgetStats = getWidgetStats<TIssuesByStateGroupsWidgetResponse[]>(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||||
|
const selectedDuration = widgetDetails?.widget_filters.duration ?? "none";
|
||||||
|
|
||||||
const handleUpdateFilters = async (filters: Partial<TIssuesByStateGroupsWidgetFilters>) => {
|
const handleUpdateFilters = async (filters: Partial<TIssuesByStateGroupsWidgetFilters>) => {
|
||||||
if (!widgetDetails) return;
|
if (!widgetDetails) return;
|
||||||
@ -43,7 +44,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
|||||||
filters,
|
filters,
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterDates = getCustomDates(filters.target_date ?? widgetDetails.widget_filters.target_date ?? "none");
|
const filterDates = getCustomDates(filters.duration ?? selectedDuration);
|
||||||
fetchWidgetStats(workspaceSlug, dashboardId, {
|
fetchWidgetStats(workspaceSlug, dashboardId, {
|
||||||
widget_key: WIDGET_KEY,
|
widget_key: WIDGET_KEY,
|
||||||
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
|
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
|
||||||
@ -52,7 +53,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
|||||||
|
|
||||||
// fetch widget stats
|
// fetch widget stats
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filterDates = getCustomDates(widgetDetails?.widget_filters.target_date ?? "none");
|
const filterDates = getCustomDates(selectedDuration);
|
||||||
fetchWidgetStats(workspaceSlug, dashboardId, {
|
fetchWidgetStats(workspaceSlug, dashboardId, {
|
||||||
widget_key: WIDGET_KEY,
|
widget_key: WIDGET_KEY,
|
||||||
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
|
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
|
||||||
@ -138,10 +139,10 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
|||||||
Assigned by state
|
Assigned by state
|
||||||
</Link>
|
</Link>
|
||||||
<DurationFilterDropdown
|
<DurationFilterDropdown
|
||||||
value={widgetDetails.widget_filters.target_date ?? "none"}
|
value={selectedDuration}
|
||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
handleUpdateFilters({
|
handleUpdateFilters({
|
||||||
target_date: val,
|
duration: val,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -2,7 +2,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { FileText, Plus } from "lucide-react";
|
import { FileText, Plus } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useProject, useUser } from "hooks/store";
|
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button } from "@plane/ui";
|
import { Breadcrumbs, Button } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -25,6 +25,7 @@ export const PagesHeader = observer(() => {
|
|||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
|
const { setTrackElement } = useEventTracker();
|
||||||
|
|
||||||
const canUserCreatePage =
|
const canUserCreatePage =
|
||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
@ -64,7 +65,15 @@ export const PagesHeader = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
{canUserCreatePage && (
|
{canUserCreatePage && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="primary" prependIcon={<Plus />} size="sm" onClick={() => toggleCreatePageModal(true)}>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
prependIcon={<Plus />}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setTrackElement("Project pages page");
|
||||||
|
toggleCreatePageModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Create Page
|
Create Page
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,18 +1,78 @@
|
|||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs, CustomMenu } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
// components
|
// components
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { useApplication, useUser } from "hooks/store";
|
||||||
|
import { ChevronDown, PanelRight } from "lucide-react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "constants/profile";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
export const UserProfileHeader = () => (
|
type TUserProfileHeader = {
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
type?: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
||||||
|
const { type = undefined } = props
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, userId } = router.query;
|
||||||
|
|
||||||
|
const AUTHORIZED_ROLES = [20, 15, 10];
|
||||||
|
const {
|
||||||
|
membership: { currentWorkspaceRole },
|
||||||
|
} = useUser();
|
||||||
|
|
||||||
|
if (!currentWorkspaceRole) return null;
|
||||||
|
|
||||||
|
const isAuthorized = AUTHORIZED_ROLES.includes(currentWorkspaceRole);
|
||||||
|
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
||||||
|
|
||||||
|
const { theme: themStore } = useApplication();
|
||||||
|
|
||||||
|
return (<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
<SidebarHamburgerToggle />
|
||||||
<div>
|
<div className="flex justify-between w-full">
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink href="/profile" label="Activity Overview" />} />
|
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink href="/profile" label="Activity Overview" />} />
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
|
<div className="flex gap-4 md:hidden">
|
||||||
|
<CustomMenu
|
||||||
|
maxHeight={"md"}
|
||||||
|
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||||
|
placement="bottom-start"
|
||||||
|
customButton={
|
||||||
|
<div className="flex gap-2 items-center px-2 py-1.5 border border-custom-border-400 rounded-md">
|
||||||
|
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{type}</span>
|
||||||
|
<ChevronDown className="w-4 h-4 text-custom-text-400" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||||
|
closeOnSelect
|
||||||
|
>
|
||||||
|
<></>
|
||||||
|
{tabsList.map((tab) => (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`} className="text-custom-text-300 w-full">{tab.label}</Link>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomMenu>
|
||||||
|
<button className="transition-all block md:hidden" onClick={() => { themStore.toggleProfileSidebar() }}>
|
||||||
|
<PanelRight className={
|
||||||
|
cn("w-4 h-4 block md:hidden", !themStore.profileSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")
|
||||||
|
} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,13 +1,35 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ArrowLeft, BarChart2 } from "lucide-react";
|
import { BarChart2, PanelRight } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
|
import { useApplication } from "hooks/store";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export const WorkspaceAnalyticsHeader = () => {
|
export const WorkspaceAnalyticsHeader = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { analytics_tab } = router.query;
|
||||||
|
|
||||||
|
const { theme: themeStore } = useApplication();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleToggleWorkspaceAnalyticsSidebar = () => {
|
||||||
|
if (window && window.innerWidth < 768) {
|
||||||
|
themeStore.toggleWorkspaceAnalyticsSidebar(true);
|
||||||
|
}
|
||||||
|
if (window && themeStore.workspaceAnalyticsSidebarCollapsed && window.innerWidth >= 768) {
|
||||||
|
themeStore.toggleWorkspaceAnalyticsSidebar(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleToggleWorkspaceAnalyticsSidebar);
|
||||||
|
handleToggleWorkspaceAnalyticsSidebar();
|
||||||
|
return () => window.removeEventListener("resize", handleToggleWorkspaceAnalyticsSidebar);
|
||||||
|
}, [themeStore]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -16,7 +38,7 @@ export const WorkspaceAnalyticsHeader = () => {
|
|||||||
>
|
>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
<SidebarHamburgerToggle />
|
||||||
<div>
|
<div className="flex items-center justify-between w-full">
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
@ -25,9 +47,14 @@ export const WorkspaceAnalyticsHeader = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
|
{analytics_tab === 'custom' &&
|
||||||
|
<button className="block md:hidden" onClick={() => { themeStore.toggleWorkspaceAnalyticsSidebar() }}>
|
||||||
|
<PanelRight className={cn("w-4 h-4 block md:hidden", !themeStore.workspaceAnalyticsSidebarCollapsed ? "text-custom-primary-100" : "text-custom-text-200")} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -4,13 +4,18 @@ import { useTheme } from "next-themes";
|
|||||||
// images
|
// images
|
||||||
import githubBlackImage from "/public/logos/github-black.png";
|
import githubBlackImage from "/public/logos/github-black.png";
|
||||||
import githubWhiteImage from "/public/logos/github-white.png";
|
import githubWhiteImage from "/public/logos/github-white.png";
|
||||||
|
// hooks
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs } from "@plane/ui";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
|
// constants
|
||||||
|
import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "constants/event-tracker";
|
||||||
|
|
||||||
export const WorkspaceDashboardHeader = () => {
|
export const WorkspaceDashboardHeader = () => {
|
||||||
// hooks
|
// hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -31,16 +36,26 @@ export const WorkspaceDashboardHeader = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 px-3">
|
<div className="flex items-center gap-3 px-3">
|
||||||
<a
|
<a
|
||||||
|
onClick={() =>
|
||||||
|
captureEvent(CHANGELOG_REDIRECTED, {
|
||||||
|
element: "navbar",
|
||||||
|
})
|
||||||
|
}
|
||||||
href="https://plane.so/changelog"
|
href="https://plane.so/changelog"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5"
|
className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5"
|
||||||
>
|
>
|
||||||
<Zap size={14} strokeWidth={2} fill="rgb(var(--color-text-100))" />
|
<Zap size={14} strokeWidth={2} fill="rgb(var(--color-text-100))" />
|
||||||
<span className="text-xs hidden sm:hidden md:block font-medium">{"What's new?"}</span>
|
<span className="hidden text-xs font-medium sm:hidden md:block">{"What's new?"}</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5 "
|
onClick={() =>
|
||||||
|
captureEvent(GITHUB_REDIRECTED, {
|
||||||
|
element: "navbar",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5"
|
||||||
href="https://github.com/makeplane/plane"
|
href="https://github.com/makeplane/plane"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@ -51,7 +66,7 @@ export const WorkspaceDashboardHeader = () => {
|
|||||||
width={16}
|
width={16}
|
||||||
alt="GitHub Logo"
|
alt="GitHub Logo"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs font-medium hidden sm:hidden md:block">Star us on GitHub</span>
|
<span className="hidden text-xs font-medium sm:hidden md:block">Star us on GitHub</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,6 +20,7 @@ import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle
|
|||||||
// types
|
// types
|
||||||
import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types";
|
import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { ISSUE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
type TInboxIssueActionsHeader = {
|
type TInboxIssueActionsHeader = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -86,17 +87,12 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
|||||||
throw new Error("Missing required parameters");
|
throw new Error("Missing required parameters");
|
||||||
await removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
|
await removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: {
|
payload: {
|
||||||
id: inboxIssueId,
|
id: inboxIssueId,
|
||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
element: "Inbox page",
|
element: "Inbox page",
|
||||||
},
|
}
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
router.push({
|
router.push({
|
||||||
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||||
@ -108,17 +104,12 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
|||||||
message: "Something went wrong while deleting inbox issue. Please try again.",
|
message: "Something went wrong while deleting inbox issue. Please try again.",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: {
|
payload: {
|
||||||
id: inboxIssueId,
|
id: inboxIssueId,
|
||||||
state: "FAILED",
|
state: "FAILED",
|
||||||
element: "Inbox page",
|
element: "Inbox page",
|
||||||
},
|
},
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -18,6 +18,8 @@ import { GptAssistantPopover } from "components/core";
|
|||||||
import { Button, Input, ToggleSwitch } from "@plane/ui";
|
import { Button, Input, ToggleSwitch } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -65,7 +67,6 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
|||||||
config: { envConfig },
|
config: { envConfig },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { captureIssueEvent } = useEventTracker();
|
const { captureIssueEvent } = useEventTracker();
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@ -94,34 +95,24 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
|||||||
handleClose();
|
handleClose();
|
||||||
} else reset(defaultValues);
|
} else reset(defaultValues);
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: {
|
payload: {
|
||||||
...formData,
|
...formData,
|
||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
element: "Inbox page",
|
element: "Inbox page",
|
||||||
},
|
},
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
path: router.pathname,
|
path: router.pathname,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: {
|
payload: {
|
||||||
...formData,
|
...formData,
|
||||||
state: "FAILED",
|
state: "FAILED",
|
||||||
element: "Inbox page",
|
element: "Inbox page",
|
||||||
},
|
},
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
path: router.pathname,
|
path: router.pathname,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -38,7 +38,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
|||||||
title: "Attachment uploaded",
|
title: "Attachment uploaded",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: "Issue attachment added",
|
||||||
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "attachment",
|
changed_property: "attachment",
|
||||||
@ -47,7 +47,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: "Issue attachment added",
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -67,7 +67,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
|||||||
title: "Attachment removed",
|
title: "Attachment removed",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: "Issue attachment deleted",
|
||||||
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "attachment",
|
changed_property: "attachment",
|
||||||
@ -76,7 +76,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: "Issue attachment deleted",
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "attachment",
|
changed_property: "attachment",
|
||||||
|
@ -16,6 +16,7 @@ import { TIssue } from "@plane/types";
|
|||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
export type TIssueOperations = {
|
export type TIssueOperations = {
|
||||||
fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
@ -102,7 +103,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: Object.keys(data).join(","),
|
changed_property: Object.keys(data).join(","),
|
||||||
@ -112,7 +113,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { state: "FAILED", element: "Issue detail page" },
|
payload: { state: "FAILED", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: Object.keys(data).join(","),
|
changed_property: Object.keys(data).join(","),
|
||||||
@ -138,7 +139,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
message: "Issue deleted successfully",
|
message: "Issue deleted successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -149,7 +150,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
message: "Issue delete failed",
|
message: "Issue delete failed",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -164,7 +165,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
message: "Issue added to issue successfully",
|
message: "Issue added to issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -174,7 +175,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { state: "FAILED", element: "Issue detail page" },
|
payload: { state: "FAILED", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -198,7 +199,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
message: "Cycle removed from issue successfully",
|
message: "Cycle removed from issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -208,7 +209,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { state: "FAILED", element: "Issue detail page" },
|
payload: { state: "FAILED", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -232,7 +233,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
message: "Module added to issue successfully",
|
message: "Module added to issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
@ -242,7 +243,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
@ -266,7 +267,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
message: "Module removed from issue successfully",
|
message: "Module removed from issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
@ -276,7 +277,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
|
@ -13,6 +13,8 @@ import { createIssuePayload } from "helpers/issue.helper";
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
formKey: keyof TIssue;
|
formKey: keyof TIssue;
|
||||||
@ -129,7 +131,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
|||||||
viewId
|
viewId
|
||||||
).then((res) => {
|
).then((res) => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS", element: "Calendar quick add" },
|
payload: { ...res, state: "SUCCESS", element: "Calendar quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -142,7 +144,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED", element: "Calendar quick add" },
|
payload: { ...payload, state: "FAILED", element: "Calendar quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
// hooks
|
// hooks
|
||||||
import { useGlobalView, useIssues, useLabel, useUser } from "hooks/store";
|
import { useEventTracker, useGlobalView, useIssues, useLabel, useUser } from "hooks/store";
|
||||||
//ui
|
//ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
@ -11,6 +11,8 @@ import { AppliedFiltersList } from "components/issues";
|
|||||||
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
|
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
|
||||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||||
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace";
|
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
// constants
|
||||||
|
import { GLOBAL_VIEW_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
globalViewId: string;
|
globalViewId: string;
|
||||||
@ -27,6 +29,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
|||||||
} = useIssues(EIssuesStoreType.GLOBAL);
|
} = useIssues(EIssuesStoreType.GLOBAL);
|
||||||
const { workspaceLabels } = useLabel();
|
const { workspaceLabels } = useLabel();
|
||||||
const { globalViewMap, updateGlobalView } = useGlobalView();
|
const { globalViewMap, updateGlobalView } = useGlobalView();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
@ -91,6 +94,13 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
|||||||
filters: {
|
filters: {
|
||||||
...(appliedFilters ?? {}),
|
...(appliedFilters ?? {}),
|
||||||
},
|
},
|
||||||
|
}).then((res) => {
|
||||||
|
captureEvent(GLOBAL_VIEW_UPDATED, {
|
||||||
|
view_id: res.id,
|
||||||
|
applied_filters: res.filters,
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Spreadsheet view",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
|||||||
import { createIssuePayload } from "helpers/issue.helper";
|
import { createIssuePayload } from "helpers/issue.helper";
|
||||||
// types
|
// types
|
||||||
import { IProject, TIssue } from "@plane/types";
|
import { IProject, TIssue } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
interface IInputProps {
|
interface IInputProps {
|
||||||
formKey: string;
|
formKey: string;
|
||||||
@ -111,7 +113,7 @@ export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observe
|
|||||||
quickAddCallback &&
|
quickAddCallback &&
|
||||||
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => {
|
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS", element: "Gantt quick add" },
|
payload: { ...res, state: "SUCCESS", element: "Gantt quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -123,7 +125,7 @@ export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observe
|
|||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED", element: "Gantt quick add" },
|
payload: { ...payload, state: "FAILED", element: "Gantt quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, useCallback, useState } from "react";
|
import { FC, useCallback, useRef, useState } from "react";
|
||||||
import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd";
|
import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
@ -25,6 +25,7 @@ import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile";
|
|||||||
import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module";
|
import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module";
|
||||||
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
|
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
|
||||||
import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue";
|
import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue";
|
||||||
|
import { ISSUE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
export interface IBaseKanBanLayout {
|
export interface IBaseKanBanLayout {
|
||||||
issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues;
|
issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues;
|
||||||
@ -94,6 +95,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
|
|
||||||
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
|
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
|
||||||
|
|
||||||
|
const scrollableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// states
|
// states
|
||||||
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
|
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
|
||||||
const [dragState, setDragState] = useState<KanbanDragState>({});
|
const [dragState, setDragState] = useState<KanbanDragState>({});
|
||||||
@ -210,7 +213,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
setDeleteIssueModal(false);
|
setDeleteIssueModal(false);
|
||||||
setDragState({});
|
setDragState({});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: { id: dragState.draggedIssueId!, state: "FAILED", element: "Kanban layout drag & drop" },
|
payload: { id: dragState.draggedIssueId!, state: "FAILED", element: "Kanban layout drag & drop" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -245,7 +248,10 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="horizontal-scroll-enable relative h-full w-full overflow-auto bg-custom-background-90">
|
<div
|
||||||
|
className="flex horizontal-scroll-enable relative h-full w-full overflow-auto bg-custom-background-90"
|
||||||
|
ref={scrollableContainerRef}
|
||||||
|
>
|
||||||
<div className="relative h-max w-max min-w-full bg-custom-background-90 px-2">
|
<div className="relative h-max w-max min-w-full bg-custom-background-90 px-2">
|
||||||
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
||||||
{/* drag and delete component */}
|
{/* drag and delete component */}
|
||||||
@ -289,6 +295,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
storeType={storeType}
|
storeType={storeType}
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { memo } from "react";
|
import { MutableRefObject, memo } from "react";
|
||||||
import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
@ -13,6 +13,7 @@ import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
|||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
// helper
|
// helper
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
|
import RenderIfVisible from "components/core/render-if-visible-HOC";
|
||||||
|
|
||||||
interface IssueBlockProps {
|
interface IssueBlockProps {
|
||||||
peekIssueId?: string;
|
peekIssueId?: string;
|
||||||
@ -25,6 +26,9 @@ interface IssueBlockProps {
|
|||||||
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
||||||
quickActions: (issue: TIssue) => React.ReactNode;
|
quickActions: (issue: TIssue) => React.ReactNode;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
isDragStarted?: boolean;
|
||||||
|
issueIds: string[]; //DO NOT REMOVE< needed to force render for virtualization
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IssueDetailsBlockProps {
|
interface IssueDetailsBlockProps {
|
||||||
@ -107,6 +111,9 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
|
|||||||
handleIssues,
|
handleIssues,
|
||||||
quickActions,
|
quickActions,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
scrollableContainerRef,
|
||||||
|
isDragStarted,
|
||||||
|
issueIds,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const issue = issuesMap[issueId];
|
const issue = issuesMap[issueId];
|
||||||
@ -129,16 +136,22 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
|
|||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
{issue.tempId !== undefined && (
|
|
||||||
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm transition-all hover:border-custom-border-400",
|
"rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm transition-all hover:border-custom-border-400",
|
||||||
{ "hover:cursor-grab": !isDragDisabled },
|
{ "hover:cursor-grab": !isDragDisabled },
|
||||||
{ "border-custom-primary-100": snapshot.isDragging },
|
{ "border-custom-primary-100": snapshot.isDragging },
|
||||||
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id }
|
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id }
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<RenderIfVisible
|
||||||
|
classNames="space-y-2"
|
||||||
|
root={scrollableContainerRef}
|
||||||
|
defaultHeight="100px"
|
||||||
|
horizonatlOffset={50}
|
||||||
|
alwaysRender={snapshot.isDragging}
|
||||||
|
pauseHeightUpdateWhileRendering={isDragStarted}
|
||||||
|
changingReference={issueIds}
|
||||||
>
|
>
|
||||||
<KanbanIssueDetailsBlock
|
<KanbanIssueDetailsBlock
|
||||||
issue={issue}
|
issue={issue}
|
||||||
@ -147,6 +160,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
|
|||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
isReadOnly={!canEditIssueProperties}
|
isReadOnly={!canEditIssueProperties}
|
||||||
/>
|
/>
|
||||||
|
</RenderIfVisible>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { memo } from "react";
|
import { MutableRefObject, memo } from "react";
|
||||||
//types
|
//types
|
||||||
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
@ -16,6 +16,8 @@ interface IssueBlocksListProps {
|
|||||||
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
||||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
isDragStarted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
|
const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
|
||||||
@ -30,6 +32,8 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
|
|||||||
handleIssues,
|
handleIssues,
|
||||||
quickActions,
|
quickActions,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
scrollableContainerRef,
|
||||||
|
isDragStarted,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -56,6 +60,9 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
|
|||||||
index={index}
|
index={index}
|
||||||
isDragDisabled={isDragDisabled}
|
isDragDisabled={isDragDisabled}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
|
issueIds={issueIds} //passing to force render for virtualization whenever parent rerenders
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
import { getGroupByColumns } from "../utils";
|
import { getGroupByColumns } from "../utils";
|
||||||
import { TCreateModalStoreTypes } from "constants/issue";
|
import { TCreateModalStoreTypes } from "constants/issue";
|
||||||
|
import { MutableRefObject } from "react";
|
||||||
|
|
||||||
export interface IGroupByKanBan {
|
export interface IGroupByKanBan {
|
||||||
issuesMap: IIssueMap;
|
issuesMap: IIssueMap;
|
||||||
@ -45,6 +46,8 @@ export interface IGroupByKanBan {
|
|||||||
storeType?: TCreateModalStoreTypes;
|
storeType?: TCreateModalStoreTypes;
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
isDragStarted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||||
@ -67,6 +70,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
storeType,
|
storeType,
|
||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
scrollableContainerRef,
|
||||||
|
isDragStarted,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const member = useMember();
|
const member = useMember();
|
||||||
@ -92,11 +97,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
const groupByVisibilityToggle = visibilityGroupBy(_list);
|
const groupByVisibilityToggle = visibilityGroupBy(_list);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`relative flex flex-shrink-0 flex-col group ${groupByVisibilityToggle ? `` : `w-[340px]`}`}>
|
||||||
className={`relative flex flex-shrink-0 flex-col h-full group ${
|
|
||||||
groupByVisibilityToggle ? `` : `w-[340px]`
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{sub_group_by === null && (
|
{sub_group_by === null && (
|
||||||
<div className="flex-shrink-0 sticky top-0 z-[2] w-full bg-custom-background-90 py-1">
|
<div className="flex-shrink-0 sticky top-0 z-[2] w-full bg-custom-background-90 py-1">
|
||||||
<HeaderGroupByCard
|
<HeaderGroupByCard
|
||||||
@ -135,6 +136,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
disableIssueCreation={disableIssueCreation}
|
disableIssueCreation={disableIssueCreation}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
groupByVisibilityToggle={groupByVisibilityToggle}
|
groupByVisibilityToggle={groupByVisibilityToggle}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -168,6 +171,8 @@ export interface IKanBan {
|
|||||||
storeType?: TCreateModalStoreTypes;
|
storeType?: TCreateModalStoreTypes;
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
isDragStarted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanBan: React.FC<IKanBan> = observer((props) => {
|
export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||||
@ -189,6 +194,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
storeType,
|
storeType,
|
||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
scrollableContainerRef,
|
||||||
|
isDragStarted,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const issueKanBanView = useKanbanView();
|
const issueKanBanView = useKanbanView();
|
||||||
@ -213,6 +220,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
storeType={storeType}
|
storeType={storeType}
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { MutableRefObject } from "react";
|
||||||
import { Droppable } from "@hello-pangea/dnd";
|
import { Droppable } from "@hello-pangea/dnd";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProjectState } from "hooks/store";
|
import { useProjectState } from "hooks/store";
|
||||||
@ -37,6 +38,8 @@ interface IKanbanGroup {
|
|||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
groupByVisibilityToggle: boolean;
|
groupByVisibilityToggle: boolean;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
isDragStarted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanbanGroup = (props: IKanbanGroup) => {
|
export const KanbanGroup = (props: IKanbanGroup) => {
|
||||||
@ -57,6 +60,8 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
|||||||
disableIssueCreation,
|
disableIssueCreation,
|
||||||
quickAddCallback,
|
quickAddCallback,
|
||||||
viewId,
|
viewId,
|
||||||
|
scrollableContainerRef,
|
||||||
|
isDragStarted,
|
||||||
} = props;
|
} = props;
|
||||||
// hooks
|
// hooks
|
||||||
const projectState = useProjectState();
|
const projectState = useProjectState();
|
||||||
@ -127,6 +132,8 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
|||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
|
@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|||||||
import { createIssuePayload } from "helpers/issue.helper";
|
import { createIssuePayload } from "helpers/issue.helper";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
const Inputs = (props: any) => {
|
const Inputs = (props: any) => {
|
||||||
const { register, setFocus, projectDetail } = props;
|
const { register, setFocus, projectDetail } = props;
|
||||||
@ -106,7 +108,7 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
|
|||||||
viewId
|
viewId
|
||||||
).then((res) => {
|
).then((res) => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS", element: "Kanban quick add" },
|
payload: { ...res, state: "SUCCESS", element: "Kanban quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -118,7 +120,7 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
|
|||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED", element: "Kanban quick add" },
|
payload: { ...payload, state: "FAILED", element: "Kanban quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { MutableRefObject } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
@ -80,6 +81,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
|||||||
viewId?: string
|
viewId?: string
|
||||||
) => Promise<TIssue | undefined>;
|
) => Promise<TIssue | undefined>;
|
||||||
viewId?: string;
|
viewId?: string;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
@ -99,6 +101,8 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
quickAddCallback,
|
quickAddCallback,
|
||||||
viewId,
|
viewId,
|
||||||
|
scrollableContainerRef,
|
||||||
|
isDragStarted,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const calculateIssueCount = (column_id: string) => {
|
const calculateIssueCount = (column_id: string) => {
|
||||||
@ -150,6 +154,8 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -183,6 +189,7 @@ export interface IKanBanSwimLanes {
|
|||||||
) => Promise<TIssue | undefined>;
|
) => Promise<TIssue | undefined>;
|
||||||
viewId?: string;
|
viewId?: string;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||||
@ -204,6 +211,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
quickAddCallback,
|
quickAddCallback,
|
||||||
viewId,
|
viewId,
|
||||||
|
scrollableContainerRef,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const member = useMember();
|
const member = useMember();
|
||||||
@ -249,6 +257,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -122,7 +122,6 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<div className={`relative h-full w-full bg-custom-background-90`}>
|
<div className={`relative h-full w-full bg-custom-background-90`}>
|
||||||
<List
|
<List
|
||||||
issuesMap={issueMap}
|
issuesMap={issueMap}
|
||||||
@ -142,6 +141,5 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
|||||||
isCompletedCycle={isCompletedCycle}
|
isCompletedCycle={isCompletedCycle}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -48,16 +48,12 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
|||||||
const projectDetails = getProjectById(issue.project_id);
|
const projectDetails = getProjectById(issue.project_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("min-h-12 relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm", {
|
||||||
"relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm border border-transparent border-b-custom-border-200",
|
|
||||||
{
|
|
||||||
"border border-custom-primary-70 hover:border-custom-primary-70":
|
"border border-custom-primary-70 hover:border-custom-primary-70":
|
||||||
peekIssue && peekIssue.issueId === issue.id,
|
peekIssue && peekIssue.issueId === issue.id,
|
||||||
"last:border-b-transparent": peekIssue?.issueId !== issue.id,
|
"last:border-b-transparent": peekIssue?.issueId !== issue.id
|
||||||
}
|
})}
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{displayProperties && displayProperties?.key && (
|
{displayProperties && displayProperties?.key && (
|
||||||
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
||||||
@ -106,6 +102,5 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { FC } from "react";
|
import { FC, MutableRefObject } from "react";
|
||||||
// components
|
// components
|
||||||
import { IssueBlock } from "components/issues";
|
import { IssueBlock } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types";
|
import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
|
import RenderIfVisible from "components/core/render-if-visible-HOC";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
||||||
@ -12,20 +13,26 @@ interface Props {
|
|||||||
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
|
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
|
||||||
quickActions: (issue: TIssue) => React.ReactNode;
|
quickActions: (issue: TIssue) => React.ReactNode;
|
||||||
displayProperties: IIssueDisplayProperties | undefined;
|
displayProperties: IIssueDisplayProperties | undefined;
|
||||||
|
containerRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IssueBlocksList: FC<Props> = (props) => {
|
export const IssueBlocksList: FC<Props> = (props) => {
|
||||||
const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties } = props;
|
const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties, containerRef } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full">
|
<div className="relative h-full w-full">
|
||||||
{issueIds && issueIds.length > 0 ? (
|
{issueIds && issueIds.length > 0 ? (
|
||||||
issueIds.map((issueId: string) => {
|
issueIds.map((issueId: string) => {
|
||||||
if (!issueId) return null;
|
if (!issueId) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RenderIfVisible
|
||||||
|
key={`${issueId}`}
|
||||||
|
defaultHeight="3rem"
|
||||||
|
root={containerRef}
|
||||||
|
classNames={"relative border border-transparent border-b-custom-border-200 last:border-b-transparent"}
|
||||||
|
changingReference={issueIds}
|
||||||
|
>
|
||||||
<IssueBlock
|
<IssueBlock
|
||||||
key={issueId}
|
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
issuesMap={issuesMap}
|
issuesMap={issuesMap}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
@ -33,6 +40,7 @@ export const IssueBlocksList: FC<Props> = (props) => {
|
|||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
/>
|
/>
|
||||||
|
</RenderIfVisible>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
// components
|
// components
|
||||||
import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues";
|
import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues";
|
||||||
|
import { HeaderGroupByCard } from "./headers/group-by-card";
|
||||||
// hooks
|
// hooks
|
||||||
import { useLabel, useMember, useProject, useProjectState } from "hooks/store";
|
import { useLabel, useMember, useProject, useProjectState } from "hooks/store";
|
||||||
// types
|
// types
|
||||||
@ -10,12 +12,12 @@ import {
|
|||||||
IIssueDisplayProperties,
|
IIssueDisplayProperties,
|
||||||
TIssueMap,
|
TIssueMap,
|
||||||
TUnGroupedIssues,
|
TUnGroupedIssues,
|
||||||
|
IGroupByColumn,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
// constants
|
// constants
|
||||||
import { HeaderGroupByCard } from "./headers/group-by-card";
|
|
||||||
import { getGroupByColumns } from "../utils";
|
|
||||||
import { TCreateModalStoreTypes } from "constants/issue";
|
import { TCreateModalStoreTypes } from "constants/issue";
|
||||||
|
import { getGroupByColumns } from "../utils";
|
||||||
|
|
||||||
export interface IGroupByList {
|
export interface IGroupByList {
|
||||||
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
||||||
@ -64,9 +66,11 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
|||||||
const label = useLabel();
|
const label = useLabel();
|
||||||
const projectState = useProjectState();
|
const projectState = useProjectState();
|
||||||
|
|
||||||
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member, true);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
if (!list) return null;
|
const groups = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member, true);
|
||||||
|
|
||||||
|
if (!groups) return null;
|
||||||
|
|
||||||
const prePopulateQuickAddData = (groupByKey: string | null, value: any) => {
|
const prePopulateQuickAddData = (groupByKey: string | null, value: any) => {
|
||||||
const defaultState = projectState.projectStates?.find((state) => state.default);
|
const defaultState = projectState.projectStates?.find((state) => state.default);
|
||||||
@ -104,11 +108,11 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
|||||||
const isGroupByCreatedBy = group_by === "created_by";
|
const isGroupByCreatedBy = group_by === "created_by";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full">
|
<div ref={containerRef} className="relative overflow-auto h-full w-full">
|
||||||
{list &&
|
{groups &&
|
||||||
list.length > 0 &&
|
groups.length > 0 &&
|
||||||
list.map(
|
groups.map(
|
||||||
(_list: any) =>
|
(_list: IGroupByColumn) =>
|
||||||
validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[_list.id]) && (
|
validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[_list.id]) && (
|
||||||
<div key={_list.id} className={`flex flex-shrink-0 flex-col`}>
|
<div key={_list.id} className={`flex flex-shrink-0 flex-col`}>
|
||||||
<div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 py-1">
|
<div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 py-1">
|
||||||
@ -131,6 +135,7 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
|||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
containerRef={containerRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|||||||
import { TIssue, IProject } from "@plane/types";
|
import { TIssue, IProject } from "@plane/types";
|
||||||
// types
|
// types
|
||||||
import { createIssuePayload } from "helpers/issue.helper";
|
import { createIssuePayload } from "helpers/issue.helper";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
interface IInputProps {
|
interface IInputProps {
|
||||||
formKey: string;
|
formKey: string;
|
||||||
@ -103,7 +105,7 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
|
|||||||
quickAddCallback &&
|
quickAddCallback &&
|
||||||
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => {
|
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS", element: "List quick add" },
|
payload: { ...res, state: "SUCCESS", element: "List quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -115,7 +117,7 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
|
|||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED", element: "List quick add" },
|
payload: { ...payload, state: "FAILED", element: "List quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,8 @@ import {
|
|||||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types";
|
import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
export interface IIssueProperties {
|
export interface IIssueProperties {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -40,7 +42,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handleState = (stateId: string) => {
|
const handleState = (stateId: string) => {
|
||||||
handleIssues({ ...issue, state_id: stateId }).then(() => {
|
handleIssues({ ...issue, state_id: stateId }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
@ -54,7 +56,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handlePriority = (value: TIssuePriorities) => {
|
const handlePriority = (value: TIssuePriorities) => {
|
||||||
handleIssues({ ...issue, priority: value }).then(() => {
|
handleIssues({ ...issue, priority: value }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
@ -68,7 +70,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handleLabel = (ids: string[]) => {
|
const handleLabel = (ids: string[]) => {
|
||||||
handleIssues({ ...issue, label_ids: ids }).then(() => {
|
handleIssues({ ...issue, label_ids: ids }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
@ -82,7 +84,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handleAssignee = (ids: string[]) => {
|
const handleAssignee = (ids: string[]) => {
|
||||||
handleIssues({ ...issue, assignee_ids: ids }).then(() => {
|
handleIssues({ ...issue, assignee_ids: ids }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
@ -96,7 +98,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handleStartDate = (date: Date | null) => {
|
const handleStartDate = (date: Date | null) => {
|
||||||
handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }).then(() => {
|
handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
@ -110,7 +112,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handleTargetDate = (date: Date | null) => {
|
const handleTargetDate = (date: Date | null) => {
|
||||||
handleIssues({ ...issue, target_date: date ? renderFormattedPayloadDate(date) : null }).then(() => {
|
handleIssues({ ...issue, target_date: date ? renderFormattedPayloadDate(date) : null }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
@ -124,7 +126,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
const handleEstimate = (value: number | null) => {
|
const handleEstimate = (value: number | null) => {
|
||||||
handleIssues({ ...issue, estimate_point: value }).then(() => {
|
handleIssues({ ...issue, estimate_point: value }).then(() => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
updates: {
|
updates: {
|
||||||
|
@ -21,6 +21,7 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props)
|
|||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||||
<DateDropdown
|
<DateDropdown
|
||||||
value={issue.target_date}
|
value={issue.target_date}
|
||||||
|
minDate={issue.start_date ? new Date(issue.start_date) : undefined}
|
||||||
onChange={(data) => {
|
onChange={(data) => {
|
||||||
const targetDate = data ? renderFormattedPayloadDate(data) : null;
|
const targetDate = data ? renderFormattedPayloadDate(data) : null;
|
||||||
onChange(
|
onChange(
|
||||||
|
@ -21,6 +21,7 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Prop
|
|||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||||
<DateDropdown
|
<DateDropdown
|
||||||
value={issue.start_date}
|
value={issue.start_date}
|
||||||
|
maxDate={issue.target_date ? new Date(issue.target_date) : undefined}
|
||||||
onChange={(data) => {
|
onChange={(data) => {
|
||||||
const startDate = data ? renderFormattedPayloadDate(data) : null;
|
const startDate = data ? renderFormattedPayloadDate(data) : null;
|
||||||
onChange(
|
onChange(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from "react";
|
import { Dispatch, MutableRefObject, SetStateAction, useRef, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// icons
|
// icons
|
||||||
@ -7,6 +7,7 @@ import { ChevronRight, MoreHorizontal } from "lucide-react";
|
|||||||
import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
|
import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
|
||||||
// components
|
// components
|
||||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||||
|
import RenderIfVisible from "components/core/render-if-visible-HOC";
|
||||||
import { IssueColumn } from "./issue-column";
|
import { IssueColumn } from "./issue-column";
|
||||||
// ui
|
// ui
|
||||||
import { ControlLink, Tooltip } from "@plane/ui";
|
import { ControlLink, Tooltip } from "@plane/ui";
|
||||||
@ -32,6 +33,9 @@ interface Props {
|
|||||||
portalElement: React.MutableRefObject<HTMLDivElement | null>;
|
portalElement: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
nestingLevel: number;
|
nestingLevel: number;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
|
isScrolled: MutableRefObject<boolean>;
|
||||||
|
containerRef: MutableRefObject<HTMLTableElement | null>;
|
||||||
|
issueIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpreadsheetIssueRow = observer((props: Props) => {
|
export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||||
@ -44,8 +48,96 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
handleIssues,
|
handleIssues,
|
||||||
quickActions,
|
quickActions,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
isScrolled,
|
||||||
|
containerRef,
|
||||||
|
issueIds,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const [isExpanded, setExpanded] = useState<boolean>(false);
|
||||||
|
const { subIssues: subIssuesStore } = useIssueDetail();
|
||||||
|
|
||||||
|
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* first column/ issue name and key column */}
|
||||||
|
<RenderIfVisible
|
||||||
|
as="tr"
|
||||||
|
defaultHeight="calc(2.75rem - 1px)"
|
||||||
|
root={containerRef}
|
||||||
|
placeholderChildren={<td colSpan={100} className="border-b-[0.5px]" />}
|
||||||
|
changingReference={issueIds}
|
||||||
|
>
|
||||||
|
<IssueRowDetails
|
||||||
|
issueId={issueId}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
quickActions={quickActions}
|
||||||
|
canEditProperties={canEditProperties}
|
||||||
|
nestingLevel={nestingLevel}
|
||||||
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
|
handleIssues={handleIssues}
|
||||||
|
portalElement={portalElement}
|
||||||
|
isScrolled={isScrolled}
|
||||||
|
isExpanded={isExpanded}
|
||||||
|
setExpanded={setExpanded}
|
||||||
|
/>
|
||||||
|
</RenderIfVisible>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssueId: string) => (
|
||||||
|
<SpreadsheetIssueRow
|
||||||
|
key={subIssueId}
|
||||||
|
issueId={subIssueId}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
quickActions={quickActions}
|
||||||
|
canEditProperties={canEditProperties}
|
||||||
|
nestingLevel={nestingLevel + 1}
|
||||||
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
|
handleIssues={handleIssues}
|
||||||
|
portalElement={portalElement}
|
||||||
|
isScrolled={isScrolled}
|
||||||
|
containerRef={containerRef}
|
||||||
|
issueIds={issueIds}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IssueRowDetailsProps {
|
||||||
|
displayProperties: IIssueDisplayProperties;
|
||||||
|
isEstimateEnabled: boolean;
|
||||||
|
quickActions: (
|
||||||
|
issue: TIssue,
|
||||||
|
customActionButton?: React.ReactElement,
|
||||||
|
portalElement?: HTMLDivElement | null
|
||||||
|
) => React.ReactNode;
|
||||||
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
|
||||||
|
portalElement: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
|
nestingLevel: number;
|
||||||
|
issueId: string;
|
||||||
|
isScrolled: MutableRefObject<boolean>;
|
||||||
|
isExpanded: boolean;
|
||||||
|
setExpanded: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||||
|
const {
|
||||||
|
displayProperties,
|
||||||
|
issueId,
|
||||||
|
isEstimateEnabled,
|
||||||
|
nestingLevel,
|
||||||
|
portalElement,
|
||||||
|
handleIssues,
|
||||||
|
quickActions,
|
||||||
|
canEditProperties,
|
||||||
|
isScrolled,
|
||||||
|
isExpanded,
|
||||||
|
setExpanded,
|
||||||
|
} = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
@ -54,8 +146,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
const { peekIssue, setPeekIssue } = useIssueDetail();
|
const { peekIssue, setPeekIssue } = useIssueDetail();
|
||||||
// states
|
// states
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
const [isExpanded, setExpanded] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const handleIssuePeekOverview = (issue: TIssue) => {
|
const handleIssuePeekOverview = (issue: TIssue) => {
|
||||||
@ -66,7 +156,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
const issueDetail = issue.getIssueById(issueId);
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
const paddingLeft = `${nestingLevel * 54}px`;
|
const paddingLeft = `${nestingLevel * 54}px`;
|
||||||
|
|
||||||
@ -91,24 +180,23 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!issueDetail) return null;
|
if (!issueDetail) return null;
|
||||||
|
|
||||||
const disableUserActions = !canEditProperties(issueDetail.project_id);
|
const disableUserActions = !canEditProperties(issueDetail.project_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr
|
|
||||||
className={cn({
|
|
||||||
"border border-custom-primary-70 hover:border-custom-primary-70": peekIssue?.issueId === issueDetail.id,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{/* first column/ issue name and key column */}
|
|
||||||
<td
|
<td
|
||||||
className={cn(
|
className={cn(
|
||||||
"sticky group left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] border-custom-border-200 focus:border-custom-primary-70",
|
"sticky group left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] border-custom-border-200",
|
||||||
{
|
{
|
||||||
"border-b-[0.5px]": peekIssue?.issueId !== issueDetail.id,
|
"border-b-[0.5px]": peekIssue?.issueId !== issueDetail.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"border border-custom-primary-70 hover:border-custom-primary-70": peekIssue?.issueId === issueDetail.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shadow-[8px_22px_22px_10px_rgba(0,0,0,0.05)]": isScrolled.current,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@ -154,10 +242,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
>
|
>
|
||||||
<div className="w-full overflow-hidden">
|
<div className="w-full overflow-hidden">
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={issueDetail.name}>
|
<Tooltip tooltipHeading="Title" tooltipContent={issueDetail.name}>
|
||||||
<div
|
<div className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100" tabIndex={-1}>
|
||||||
className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
{issueDetail.name}
|
{issueDetail.name}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -175,24 +260,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
isEstimateEnabled={isEstimateEnabled}
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tr>
|
|
||||||
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId: string) => (
|
|
||||||
<SpreadsheetIssueRow
|
|
||||||
key={subIssueId}
|
|
||||||
issueId={subIssueId}
|
|
||||||
displayProperties={displayProperties}
|
|
||||||
quickActions={quickActions}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
nestingLevel={nestingLevel + 1}
|
|
||||||
isEstimateEnabled={isEstimateEnabled}
|
|
||||||
handleIssues={handleIssues}
|
|
||||||
portalElement={portalElement}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|||||||
import { createIssuePayload } from "helpers/issue.helper";
|
import { createIssuePayload } from "helpers/issue.helper";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
formKey: keyof TIssue;
|
formKey: keyof TIssue;
|
||||||
@ -162,7 +164,7 @@ export const SpreadsheetQuickAddIssueForm: React.FC<Props> = observer((props) =>
|
|||||||
(await quickAddCallback(currentWorkspace.slug, currentProjectDetails.id, { ...payload } as TIssue, viewId).then(
|
(await quickAddCallback(currentWorkspace.slug, currentProjectDetails.id, { ...payload } as TIssue, viewId).then(
|
||||||
(res) => {
|
(res) => {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" },
|
payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -175,7 +177,7 @@ export const SpreadsheetQuickAddIssueForm: React.FC<Props> = observer((props) =>
|
|||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" },
|
payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { MutableRefObject, useEffect, useRef } from "react";
|
||||||
//types
|
//types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
@ -21,6 +22,7 @@ type Props = {
|
|||||||
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
|
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
portalElement: React.MutableRefObject<HTMLDivElement | null>;
|
portalElement: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
|
containerRef: MutableRefObject<HTMLTableElement | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetTable = observer((props: Props) => {
|
export const SpreadsheetTable = observer((props: Props) => {
|
||||||
@ -34,8 +36,45 @@ export const SpreadsheetTable = observer((props: Props) => {
|
|||||||
quickActions,
|
quickActions,
|
||||||
handleIssues,
|
handleIssues,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
containerRef,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
// states
|
||||||
|
const isScrolled = useRef(false);
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const scrollLeft = containerRef.current.scrollLeft;
|
||||||
|
|
||||||
|
const columnShadow = "8px 22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for regular columns
|
||||||
|
const headerShadow = "8px -22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for headers
|
||||||
|
|
||||||
|
//The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly
|
||||||
|
if (scrollLeft > 0 !== isScrolled.current) {
|
||||||
|
const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child");
|
||||||
|
|
||||||
|
for (let i = 0; i < firtColumns.length; i++) {
|
||||||
|
const shadow = i === 0 ? headerShadow : columnShadow;
|
||||||
|
if (scrollLeft > 0) {
|
||||||
|
(firtColumns[i] as HTMLElement).style.boxShadow = shadow;
|
||||||
|
} else {
|
||||||
|
(firtColumns[i] as HTMLElement).style.boxShadow = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isScrolled.current = scrollLeft > 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentContainerRef = containerRef.current;
|
||||||
|
|
||||||
|
if (currentContainerRef) currentContainerRef.addEventListener("scroll", handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (currentContainerRef) currentContainerRef.removeEventListener("scroll", handleScroll);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleKeyBoardNavigation = useTableKeyboardNavigation();
|
const handleKeyBoardNavigation = useTableKeyboardNavigation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -58,6 +97,9 @@ export const SpreadsheetTable = observer((props: Props) => {
|
|||||||
isEstimateEnabled={isEstimateEnabled}
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
portalElement={portalElement}
|
portalElement={portalElement}
|
||||||
|
containerRef={containerRef}
|
||||||
|
isScrolled={isScrolled}
|
||||||
|
issueIds={issueIds}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
@ -48,8 +48,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
enableQuickCreateIssue,
|
enableQuickCreateIssue,
|
||||||
disableIssueCreation,
|
disableIssueCreation,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
|
||||||
const isScrolled = useRef(false);
|
|
||||||
// refs
|
// refs
|
||||||
const containerRef = useRef<HTMLTableElement | null>(null);
|
const containerRef = useRef<HTMLTableElement | null>(null);
|
||||||
const portalRef = useRef<HTMLDivElement | null>(null);
|
const portalRef = useRef<HTMLDivElement | null>(null);
|
||||||
@ -58,39 +56,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const isEstimateEnabled: boolean = currentProjectDetails?.estimate !== null;
|
const isEstimateEnabled: boolean = currentProjectDetails?.estimate !== null;
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (!containerRef.current) return;
|
|
||||||
const scrollLeft = containerRef.current.scrollLeft;
|
|
||||||
|
|
||||||
const columnShadow = "8px 22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for regular columns
|
|
||||||
const headerShadow = "8px -22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for headers
|
|
||||||
|
|
||||||
//The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly
|
|
||||||
if (scrollLeft > 0 !== isScrolled.current) {
|
|
||||||
const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child");
|
|
||||||
|
|
||||||
for (let i = 0; i < firtColumns.length; i++) {
|
|
||||||
const shadow = i === 0 ? headerShadow : columnShadow;
|
|
||||||
if (scrollLeft > 0) {
|
|
||||||
(firtColumns[i] as HTMLElement).style.boxShadow = shadow;
|
|
||||||
} else {
|
|
||||||
(firtColumns[i] as HTMLElement).style.boxShadow = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isScrolled.current = scrollLeft > 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const currentContainerRef = containerRef.current;
|
|
||||||
|
|
||||||
if (currentContainerRef) currentContainerRef.addEventListener("scroll", handleScroll);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (currentContainerRef) currentContainerRef.removeEventListener("scroll", handleScroll);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!issueIds || issueIds.length === 0)
|
if (!issueIds || issueIds.length === 0)
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full w-full place-items-center">
|
<div className="grid h-full w-full place-items-center">
|
||||||
@ -112,6 +77,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
containerRef={containerRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-custom-border-100">
|
<div className="border-t border-custom-border-100">
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
||||||
import { ISSUE_PRIORITIES } from "constants/issue";
|
import { EIssueListRow, ISSUE_PRIORITIES } from "constants/issue";
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
import { IMemberRootStore } from "store/member";
|
import { IMemberRootStore } from "store/member";
|
||||||
import { IProjectStore } from "store/project/project.store";
|
import { IProjectStore } from "store/project/project.store";
|
||||||
import { IStateStore } from "store/state.store";
|
import { IStateStore } from "store/state.store";
|
||||||
import { GroupByColumnTypes, IGroupByColumn } from "@plane/types";
|
import { GroupByColumnTypes, IGroupByColumn, IIssueListRow, TGroupedIssues, TUnGroupedIssues } from "@plane/types";
|
||||||
import { STATE_GROUPS } from "constants/state";
|
import { STATE_GROUPS } from "constants/state";
|
||||||
import { ILabelStore } from "store/label.store";
|
import { ILabelStore } from "store/label.store";
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ import { IssueFormRoot } from "./form";
|
|||||||
import type { TIssue } from "@plane/types";
|
import type { TIssue } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue";
|
import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue";
|
||||||
|
import { ISSUE_CREATED, ISSUE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
export interface IssuesModalProps {
|
export interface IssuesModalProps {
|
||||||
data?: Partial<TIssue>;
|
data?: Partial<TIssue>;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -157,14 +159,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
message: "Issue created successfully.",
|
message: "Issue created successfully.",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...response, state: "SUCCESS" },
|
payload: { ...response, state: "SUCCESS" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
!createMore && handleClose();
|
!createMore && handleClose();
|
||||||
return response;
|
return response;
|
||||||
@ -175,14 +172,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
message: "Issue could not be created. Please try again.",
|
message: "Issue could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue created",
|
eventName: ISSUE_CREATED,
|
||||||
payload: { ...payload, state: "FAILED" },
|
payload: { ...payload, state: "FAILED" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -198,14 +190,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
message: "Issue updated successfully.",
|
message: "Issue updated successfully.",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS" },
|
payload: { ...response, state: "SUCCESS" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
handleClose();
|
handleClose();
|
||||||
return response;
|
return response;
|
||||||
@ -216,14 +203,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
message: "Issue could not be created. Please try again.",
|
message: "Issue could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...payload, state: "FAILED" },
|
payload: { ...payload, state: "FAILED" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@ import { TIssue } from "@plane/types";
|
|||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
interface IIssuePeekOverview {
|
interface IIssuePeekOverview {
|
||||||
is_archived?: boolean;
|
is_archived?: boolean;
|
||||||
@ -103,7 +104,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Issue updated successfully",
|
message: "Issue updated successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: Object.keys(data).join(","),
|
changed_property: Object.keys(data).join(","),
|
||||||
@ -113,7 +114,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { state: "FAILED", element: "Issue peek-overview" },
|
payload: { state: "FAILED", element: "Issue peek-overview" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -135,7 +136,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Issue deleted successfully",
|
message: "Issue deleted successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
|
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -146,7 +147,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Issue delete failed",
|
message: "Issue delete failed",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue deleted",
|
eventName: ISSUE_DELETED,
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
||||||
path: router.asPath,
|
path: router.asPath,
|
||||||
});
|
});
|
||||||
@ -161,7 +162,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Issue added to issue successfully",
|
message: "Issue added to issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -171,7 +172,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { state: "FAILED", element: "Issue peek-overview" },
|
payload: { state: "FAILED", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -195,7 +196,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Cycle removed from issue successfully",
|
message: "Cycle removed from issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -210,7 +211,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Cycle remove from issue failed",
|
message: "Cycle remove from issue failed",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { state: "FAILED", element: "Issue peek-overview" },
|
payload: { state: "FAILED", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "cycle_id",
|
changed_property: "cycle_id",
|
||||||
@ -229,7 +230,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Module added to issue successfully",
|
message: "Module added to issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
@ -239,7 +240,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
@ -263,7 +264,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
message: "Module removed from issue successfully",
|
message: "Module removed from issue successfully",
|
||||||
});
|
});
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
|
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
@ -273,7 +274,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Issue updated",
|
eventName: ISSUE_UPDATED,
|
||||||
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: "module_id",
|
changed_property: "module_id",
|
||||||
|
@ -11,6 +11,8 @@ import { Button } from "@plane/ui";
|
|||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import type { IModule } from "@plane/types";
|
import type { IModule } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { MODULE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: IModule;
|
data: IModule;
|
||||||
@ -51,7 +53,7 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
message: "Module deleted successfully.",
|
message: "Module deleted successfully.",
|
||||||
});
|
});
|
||||||
captureModuleEvent({
|
captureModuleEvent({
|
||||||
eventName: "Module deleted",
|
eventName: MODULE_DELETED,
|
||||||
payload: { ...data, state: "SUCCESS" },
|
payload: { ...data, state: "SUCCESS" },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -62,7 +64,7 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
message: "Module could not be deleted. Please try again.",
|
message: "Module could not be deleted. Please try again.",
|
||||||
});
|
});
|
||||||
captureModuleEvent({
|
captureModuleEvent({
|
||||||
eventName: "Module deleted",
|
eventName: MODULE_DELETED,
|
||||||
payload: { ...data, state: "FAILED" },
|
payload: { ...data, state: "FAILED" },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -11,7 +11,7 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
|||||||
import { IModule } from "@plane/types";
|
import { IModule } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleFormSubmit: (values: Partial<IModule>) => Promise<void>;
|
handleFormSubmit: (values: Partial<IModule>, dirtyFields: any) => Promise<void>;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
status: boolean;
|
status: boolean;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -36,7 +36,7 @@ export const ModuleForm: React.FC<Props> = ({
|
|||||||
data,
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting, dirtyFields },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
control,
|
control,
|
||||||
@ -53,7 +53,7 @@ export const ModuleForm: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleCreateUpdateModule = async (formData: Partial<IModule>) => {
|
const handleCreateUpdateModule = async (formData: Partial<IModule>) => {
|
||||||
await handleFormSubmit(formData);
|
await handleFormSubmit(formData, dirtyFields);
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
|
@ -9,6 +9,8 @@ import useToast from "hooks/use-toast";
|
|||||||
import { ModuleForm } from "components/modules";
|
import { ModuleForm } from "components/modules";
|
||||||
// types
|
// types
|
||||||
import type { IModule } from "@plane/types";
|
import type { IModule } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { MODULE_CREATED, MODULE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -59,7 +61,7 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
message: "Module created successfully.",
|
message: "Module created successfully.",
|
||||||
});
|
});
|
||||||
captureModuleEvent({
|
captureModuleEvent({
|
||||||
eventName: "Module created",
|
eventName: MODULE_CREATED,
|
||||||
payload: { ...res, state: "SUCCESS" },
|
payload: { ...res, state: "SUCCESS" },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -70,13 +72,13 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
message: err.detail ?? "Module could not be created. Please try again.",
|
message: err.detail ?? "Module could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
captureModuleEvent({
|
captureModuleEvent({
|
||||||
eventName: "Module created",
|
eventName: MODULE_CREATED,
|
||||||
payload: { ...data, state: "FAILED" },
|
payload: { ...data, state: "FAILED" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateModule = async (payload: Partial<IModule>) => {
|
const handleUpdateModule = async (payload: Partial<IModule>, dirtyFields: any) => {
|
||||||
if (!workspaceSlug || !projectId || !data) return;
|
if (!workspaceSlug || !projectId || !data) return;
|
||||||
|
|
||||||
const selectedProjectId = payload.project ?? projectId.toString();
|
const selectedProjectId = payload.project ?? projectId.toString();
|
||||||
@ -90,8 +92,8 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
message: "Module updated successfully.",
|
message: "Module updated successfully.",
|
||||||
});
|
});
|
||||||
captureModuleEvent({
|
captureModuleEvent({
|
||||||
eventName: "Module updated",
|
eventName: MODULE_UPDATED,
|
||||||
payload: { ...res, state: "SUCCESS" },
|
payload: { ...res, changed_properties: Object.keys(dirtyFields), state: "SUCCESS" },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -101,20 +103,20 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
message: err.detail ?? "Module could not be updated. Please try again.",
|
message: err.detail ?? "Module could not be updated. Please try again.",
|
||||||
});
|
});
|
||||||
captureModuleEvent({
|
captureModuleEvent({
|
||||||
eventName: "Module updated",
|
eventName: MODULE_UPDATED,
|
||||||
payload: { ...data, state: "FAILED" },
|
payload: { ...data, state: "FAILED" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: Partial<IModule>) => {
|
const handleFormSubmit = async (formData: Partial<IModule>, dirtyFields: any) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const payload: Partial<IModule> = {
|
const payload: Partial<IModule> = {
|
||||||
...formData,
|
...formData,
|
||||||
};
|
};
|
||||||
if (!data) await handleCreateModule(payload);
|
if (!data) await handleCreateModule(payload);
|
||||||
else await handleUpdateModule(payload);
|
else await handleUpdateModule(payload, dirtyFields);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -16,6 +16,7 @@ import { renderFormattedDate } from "helpers/date-time.helper";
|
|||||||
// constants
|
// constants
|
||||||
import { MODULE_STATUS } from "constants/module";
|
import { MODULE_STATUS } from "constants/module";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
moduleId: string;
|
moduleId: string;
|
||||||
@ -36,7 +37,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureEvent } = useEventTracker();
|
||||||
// derived values
|
// derived values
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
@ -46,7 +47,15 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => {
|
addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
|
||||||
|
.then(() => {
|
||||||
|
captureEvent(MODULE_FAVORITED, {
|
||||||
|
module_id: moduleId,
|
||||||
|
element: "Grid layout",
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -60,7 +69,15 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => {
|
removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
|
||||||
|
.then(() => {
|
||||||
|
captureEvent(MODULE_UNFAVORITED, {
|
||||||
|
module_id: moduleId,
|
||||||
|
element: "Grid layout",
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -84,14 +101,14 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
const handleEditModule = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleEditModule = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setTrackElement("Modules page board layout");
|
setTrackElement("Modules page grid layout");
|
||||||
setEditModal(true);
|
setEditModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteModule = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleDeleteModule = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setTrackElement("Modules page board layout");
|
setTrackElement("Modules page grid layout");
|
||||||
setDeleteModal(true);
|
setDeleteModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import { renderFormattedDate } from "helpers/date-time.helper";
|
|||||||
// constants
|
// constants
|
||||||
import { MODULE_STATUS } from "constants/module";
|
import { MODULE_STATUS } from "constants/module";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
moduleId: string;
|
moduleId: string;
|
||||||
@ -36,7 +37,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureEvent } = useEventTracker();
|
||||||
// derived values
|
// derived values
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
@ -46,7 +47,15 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => {
|
addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
|
||||||
|
.then(() => {
|
||||||
|
captureEvent(MODULE_FAVORITED, {
|
||||||
|
module_id: moduleId,
|
||||||
|
element: "Grid layout",
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
@ -60,7 +69,15 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => {
|
removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
|
||||||
|
.then(() => {
|
||||||
|
captureEvent(MODULE_UNFAVORITED, {
|
||||||
|
module_id: moduleId,
|
||||||
|
element: "Grid layout",
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
|
@ -34,6 +34,7 @@ import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
|
|||||||
// constant
|
// constant
|
||||||
import { MODULE_STATUS } from "constants/module";
|
import { MODULE_STATUS } from "constants/module";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
const defaultValues: Partial<IModule> = {
|
const defaultValues: Partial<IModule> = {
|
||||||
lead: "",
|
lead: "",
|
||||||
@ -66,7 +67,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule();
|
const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker();
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -77,7 +78,19 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const submitChanges = (data: Partial<IModule>) => {
|
const submitChanges = (data: Partial<IModule>) => {
|
||||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||||
updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), data);
|
updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), data)
|
||||||
|
.then((res) => {
|
||||||
|
captureModuleEvent({
|
||||||
|
eventName: MODULE_UPDATED,
|
||||||
|
payload: { ...res, changed_properties: Object.keys(data)[0], element: "Right side-peek", state: "SUCCESS" },
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((_) => {
|
||||||
|
captureModuleEvent({
|
||||||
|
eventName: MODULE_UPDATED,
|
||||||
|
payload: { ...data, state: "FAILED" },
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateLink = async (formData: ModuleLink) => {
|
const handleCreateLink = async (formData: ModuleLink) => {
|
||||||
@ -87,6 +100,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload)
|
createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
captureEvent(MODULE_LINK_CREATED, {
|
||||||
|
module_id: moduleId,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Module link created",
|
title: "Module link created",
|
||||||
@ -109,6 +126,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload)
|
updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
captureEvent(MODULE_LINK_UPDATED, {
|
||||||
|
module_id: moduleId,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Module link updated",
|
title: "Module link updated",
|
||||||
@ -129,6 +150,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
deleteModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId)
|
deleteModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
captureEvent(MODULE_LINK_DELETED, {
|
||||||
|
module_id: moduleId,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Module link deleted",
|
title: "Module link deleted",
|
||||||
@ -187,8 +212,8 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") {
|
if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") {
|
||||||
submitChanges({
|
submitChanges({
|
||||||
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
|
||||||
target_date: renderFormattedPayloadDate(`${watch("target_date")}`),
|
target_date: renderFormattedPayloadDate(`${watch("target_date")}`),
|
||||||
|
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -294,7 +319,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="status"
|
name="status"
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
customButton={
|
customButton={
|
||||||
<span
|
<span
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ArchiveRestore, Clock, MessageSquare, User2 } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Menu } from "@headlessui/react";
|
||||||
|
import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// icons
|
// icons
|
||||||
import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
|
import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
@ -13,9 +15,12 @@ import { snoozeOptions } from "constants/notification";
|
|||||||
import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
|
||||||
import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
|
import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
|
||||||
// type
|
// type
|
||||||
import type { IUserNotification } from "@plane/types";
|
import type { IUserNotification, NotificationType } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_OPENED, NOTIFICATIONS_READ, NOTIFICATION_ARCHIVED, NOTIFICATION_SNOOZED } from "constants/event-tracker";
|
||||||
|
|
||||||
type NotificationCardProps = {
|
type NotificationCardProps = {
|
||||||
|
selectedTab: NotificationType;
|
||||||
notification: IUserNotification;
|
notification: IUserNotification;
|
||||||
isSnoozedTabOpen: boolean;
|
isSnoozedTabOpen: boolean;
|
||||||
closePopover: () => void;
|
closePopover: () => void;
|
||||||
@ -28,6 +33,7 @@ type NotificationCardProps = {
|
|||||||
|
|
||||||
export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
|
selectedTab,
|
||||||
notification,
|
notification,
|
||||||
isSnoozedTabOpen,
|
isSnoozedTabOpen,
|
||||||
closePopover,
|
closePopover,
|
||||||
@ -37,11 +43,78 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
setSelectedNotificationForSnooze,
|
setSelectedNotificationForSnooze,
|
||||||
markSnoozeNotification,
|
markSnoozeNotification,
|
||||||
} = props;
|
} = props;
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
// states
|
||||||
|
const [showSnoozeOptions, setshowSnoozeOptions] = React.useState(false);
|
||||||
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
// refs
|
||||||
|
const snoozeRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const moreOptions = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: notification.read_at ? "Mark as unread" : "Mark as read",
|
||||||
|
icon: <MessageSquare className="h-3.5 w-3.5 text-custom-text-300" />,
|
||||||
|
onClick: () => {
|
||||||
|
markNotificationReadStatusToggle(notification.id).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
title: notification.read_at ? "Notification marked as read" : "Notification marked as unread",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: notification.archived_at ? "Unarchive" : "Archive",
|
||||||
|
icon: notification.archived_at ? (
|
||||||
|
<ArchiveRestore className="h-3.5 w-3.5 text-custom-text-300" />
|
||||||
|
) : (
|
||||||
|
<ArchiveIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||||
|
),
|
||||||
|
onClick: () => {
|
||||||
|
markNotificationArchivedStatus(notification.id).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
title: notification.archived_at ? "Notification un-archived" : "Notification archived",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const snoozeOptionOnClick = (date: Date | null) => {
|
||||||
|
if (!date) {
|
||||||
|
setSelectedNotificationForSnooze(notification.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
markSnoozeNotification(notification.id, date).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
title: `Notification snoozed till ${renderFormattedDate(date)}`,
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// close snooze options on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: any) => {
|
||||||
|
if (snoozeRef.current && !snoozeRef.current?.contains(event.target)) {
|
||||||
|
setshowSnoozeOptions(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside, true);
|
||||||
|
document.addEventListener("touchend", handleClickOutside, true);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside, true);
|
||||||
|
document.removeEventListener("touchend", handleClickOutside, true);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (isSnoozedTabOpen && new Date(notification.snoozed_till!) < new Date()) return null;
|
if (isSnoozedTabOpen && new Date(notification.snoozed_till!) < new Date()) return null;
|
||||||
|
|
||||||
@ -49,6 +122,10 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
<Link
|
<Link
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
markNotificationReadStatus(notification.id);
|
markNotificationReadStatus(notification.id);
|
||||||
|
captureEvent(ISSUE_OPENED, {
|
||||||
|
issue_id: notification.data.issue.id,
|
||||||
|
element: "notification",
|
||||||
|
});
|
||||||
closePopover();
|
closePopover();
|
||||||
}}
|
}}
|
||||||
href={`/${workspaceSlug}/projects/${notification.project}/${
|
href={`/${workspaceSlug}/projects/${notification.project}/${
|
||||||
@ -87,6 +164,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full space-y-2.5 overflow-hidden">
|
<div className="w-full space-y-2.5 overflow-hidden">
|
||||||
|
<div className="flex items-start">
|
||||||
{!notification.message ? (
|
{!notification.message ? (
|
||||||
<div className="w-full break-words text-sm">
|
<div className="w-full break-words text-sm">
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
@ -135,9 +213,87 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
<span className="semi-bold">{notification.message}</span>
|
<span className="semi-bold">{notification.message}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="flex md:hidden items-start">
|
||||||
|
<Menu as="div" className={" w-min text-left"}>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Menu.Button as={React.Fragment}>
|
||||||
|
<button
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="flex w-full items-center gap-x-2 rounded p-0.5 text-sm"
|
||||||
|
>
|
||||||
|
<MoreVertical className="h-3.5 w-3.5 text-custom-text-300" />
|
||||||
|
</button>
|
||||||
|
</Menu.Button>
|
||||||
|
{open && (
|
||||||
|
<Menu.Items className={"absolute right-0 z-10"} static>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-[12rem] whitespace-nowrap"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{moreOptions.map((item) => (
|
||||||
|
<Menu.Item as="div">
|
||||||
|
{({ close }) => (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
item.onClick();
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
className="flex gap-x-2 items-center p-1.5"
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
{item.name}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
<Menu.Item as="div">
|
||||||
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
setshowSnoozeOptions(true);
|
||||||
|
}}
|
||||||
|
className="flex gap-x-2 items-center p-1.5"
|
||||||
|
>
|
||||||
|
<Clock className="h-3.5 w-3.5 text-custom-text-300" />
|
||||||
|
Snooze
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
{showSnoozeOptions && (
|
||||||
|
<div
|
||||||
|
ref={snoozeRef}
|
||||||
|
className="absolute right-36 z-20 my-1 top-24 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-[12rem] whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{snoozeOptions.map((item) => (
|
||||||
|
<p
|
||||||
|
className="p-1.5"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
setshowSnoozeOptions(false);
|
||||||
|
snoozeOptionOnClick(item.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-2 text-xs">
|
<div className="flex justify-between gap-2 text-xs">
|
||||||
<p className="text-custom-text-300">
|
<p className="text-custom-text-300 line-clamp-1">
|
||||||
{truncateText(
|
{truncateText(
|
||||||
`${notification.data.issue.identifier}-${notification.data.issue.sequence_id} ${notification.data.issue.name}`,
|
`${notification.data.issue.identifier}-${notification.data.issue.sequence_id} ${notification.data.issue.name}`,
|
||||||
50
|
50
|
||||||
@ -152,7 +308,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="flex-shrink-0 text-custom-text-300">{calculateTimeAgo(notification.created_at)}</p>
|
<p className="flex-shrink-0 text-custom-text-300 mt-auto">{calculateTimeAgo(notification.created_at)}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -164,6 +320,11 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
icon: <MessageSquare className="h-3.5 w-3.5 text-custom-text-300" />,
|
icon: <MessageSquare className="h-3.5 w-3.5 text-custom-text-300" />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
markNotificationReadStatusToggle(notification.id).then(() => {
|
markNotificationReadStatusToggle(notification.id).then(() => {
|
||||||
|
captureEvent(NOTIFICATIONS_READ, {
|
||||||
|
issue_id: notification.data.issue.id,
|
||||||
|
tab: selectedTab,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: notification.read_at ? "Notification marked as read" : "Notification marked as unread",
|
title: notification.read_at ? "Notification marked as read" : "Notification marked as unread",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -181,6 +342,11 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
),
|
),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
markNotificationArchivedStatus(notification.id).then(() => {
|
markNotificationArchivedStatus(notification.id).then(() => {
|
||||||
|
captureEvent(NOTIFICATION_ARCHIVED, {
|
||||||
|
issue_id: notification.data.issue.id,
|
||||||
|
tab: selectedTab,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: notification.archived_at ? "Notification un-archived" : "Notification archived",
|
title: notification.archived_at ? "Notification un-archived" : "Notification archived",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -195,7 +361,6 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
item.onClick();
|
item.onClick();
|
||||||
}}
|
}}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@ -208,9 +373,6 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
<Tooltip tooltipContent="Snooze">
|
<Tooltip tooltipContent="Snooze">
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
menuButtonOnClick={(e: { stopPropagation: () => void }) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
customButton={
|
customButton={
|
||||||
<div className="flex w-full items-center gap-x-2 rounded bg-custom-background-80 p-0.5 text-sm hover:bg-custom-background-100">
|
<div className="flex w-full items-center gap-x-2 rounded bg-custom-background-80 p-0.5 text-sm hover:bg-custom-background-100">
|
||||||
<Clock className="h-3.5 w-3.5 text-custom-text-300" />
|
<Clock className="h-3.5 w-3.5 text-custom-text-300" />
|
||||||
@ -231,6 +393,11 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
markSnoozeNotification(notification.id, item.value).then(() => {
|
markSnoozeNotification(notification.id, item.value).then(() => {
|
||||||
|
captureEvent(NOTIFICATION_SNOOZED, {
|
||||||
|
issue_id: notification.data.issue.id,
|
||||||
|
tab: selectedTab,
|
||||||
|
state: "SUCCESS",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: `Notification snoozed till ${renderFormattedDate(item.value)}`,
|
title: `Notification snoozed till ${renderFormattedDate(item.value)}`,
|
||||||
type: "success",
|
type: "success",
|
||||||
|
@ -1,11 +1,22 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ArrowLeft, CheckCheck, Clock, ListFilter, MoreVertical, RefreshCw, X } from "lucide-react";
|
import { ArrowLeft, CheckCheck, Clock, ListFilter, MoreVertical, RefreshCw, X } from "lucide-react";
|
||||||
|
// components
|
||||||
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
// ui
|
// ui
|
||||||
import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
|
import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// helpers
|
// helpers
|
||||||
import { getNumberCount } from "helpers/string.helper";
|
import { getNumberCount } from "helpers/string.helper";
|
||||||
// type
|
// type
|
||||||
import type { NotificationType, NotificationCount } from "@plane/types";
|
import type { NotificationType, NotificationCount } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import {
|
||||||
|
ARCHIVED_NOTIFICATIONS,
|
||||||
|
NOTIFICATIONS_READ,
|
||||||
|
SNOOZED_NOTIFICATIONS,
|
||||||
|
UNREAD_NOTIFICATIONS,
|
||||||
|
} from "constants/event-tracker";
|
||||||
|
|
||||||
type NotificationHeaderProps = {
|
type NotificationHeaderProps = {
|
||||||
notificationCount?: NotificationCount | null;
|
notificationCount?: NotificationCount | null;
|
||||||
@ -39,6 +50,8 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
setSelectedTab,
|
setSelectedTab,
|
||||||
markAllNotificationsAsRead,
|
markAllNotificationsAsRead,
|
||||||
} = props;
|
} = props;
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
|
|
||||||
const notificationTabs: Array<{
|
const notificationTabs: Array<{
|
||||||
label: string;
|
label: string;
|
||||||
@ -65,7 +78,11 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between px-5 pt-5">
|
<div className="flex items-center justify-between px-5 pt-5">
|
||||||
<h2 className="mb-2 text-xl font-semibold">Notifications</h2>
|
<div className="flex items-center gap-x-2 ">
|
||||||
|
<SidebarHamburgerToggle />
|
||||||
|
<h2 className="md:text-xl md:font-semibold">Notifications</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-x-4 text-custom-text-200">
|
<div className="flex items-center justify-center gap-x-4 text-custom-text-200">
|
||||||
<Tooltip tooltipContent="Refresh">
|
<Tooltip tooltipContent="Refresh">
|
||||||
<button
|
<button
|
||||||
@ -84,6 +101,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
setSnoozed(false);
|
setSnoozed(false);
|
||||||
setArchived(false);
|
setArchived(false);
|
||||||
setReadNotification((prev) => !prev);
|
setReadNotification((prev) => !prev);
|
||||||
|
captureEvent(UNREAD_NOTIFICATIONS);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListFilter className="h-3.5 w-3.5" />
|
<ListFilter className="h-3.5 w-3.5" />
|
||||||
@ -97,7 +115,12 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
}
|
}
|
||||||
closeOnSelect
|
closeOnSelect
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={markAllNotificationsAsRead}>
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
markAllNotificationsAsRead();
|
||||||
|
captureEvent(NOTIFICATIONS_READ);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CheckCheck className="h-3.5 w-3.5" />
|
<CheckCheck className="h-3.5 w-3.5" />
|
||||||
Mark all as read
|
Mark all as read
|
||||||
@ -108,6 +131,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
setArchived(false);
|
setArchived(false);
|
||||||
setReadNotification(false);
|
setReadNotification(false);
|
||||||
setSnoozed((prev) => !prev);
|
setSnoozed((prev) => !prev);
|
||||||
|
captureEvent(SNOOZED_NOTIFICATIONS);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -120,6 +144,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
setSnoozed(false);
|
setSnoozed(false);
|
||||||
setReadNotification(false);
|
setReadNotification(false);
|
||||||
setArchived((prev) => !prev);
|
setArchived((prev) => !prev);
|
||||||
|
captureEvent(ARCHIVED_NOTIFICATIONS);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -128,6 +153,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
|
<div className="hidden md:block">
|
||||||
<Tooltip tooltipContent="Close">
|
<Tooltip tooltipContent="Close">
|
||||||
<button type="button" onClick={() => closePopover()}>
|
<button type="button" onClick={() => closePopover()}>
|
||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-3.5 w-3.5" />
|
||||||
@ -135,6 +161,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="mt-5 w-full border-b border-custom-border-300 px-5">
|
<div className="mt-5 w-full border-b border-custom-border-300 px-5">
|
||||||
{snoozed || archived || readNotification ? (
|
{snoozed || archived || readNotification ? (
|
||||||
<button
|
<button
|
||||||
|
@ -5,6 +5,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
// hooks
|
// hooks
|
||||||
import { useApplication } from "hooks/store";
|
import { useApplication } from "hooks/store";
|
||||||
import useUserNotification from "hooks/use-user-notifications";
|
import useUserNotification from "hooks/use-user-notifications";
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// components
|
// components
|
||||||
import { EmptyState } from "components/common";
|
import { EmptyState } from "components/common";
|
||||||
import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications";
|
import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications";
|
||||||
@ -15,8 +16,12 @@ import emptyNotification from "public/empty-state/notification.svg";
|
|||||||
import { getNumberCount } from "helpers/string.helper";
|
import { getNumberCount } from "helpers/string.helper";
|
||||||
|
|
||||||
export const NotificationPopover = observer(() => {
|
export const NotificationPopover = observer(() => {
|
||||||
|
// states
|
||||||
|
const [isActive, setIsActive] = React.useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { theme: themeStore } = useApplication();
|
const { theme: themeStore } = useApplication();
|
||||||
|
// refs
|
||||||
|
const notificationPopoverRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
notifications,
|
notifications,
|
||||||
@ -44,8 +49,11 @@ export const NotificationPopover = observer(() => {
|
|||||||
setFetchNotifications,
|
setFetchNotifications,
|
||||||
markAllNotificationsAsRead,
|
markAllNotificationsAsRead,
|
||||||
} = useUserNotification();
|
} = useUserNotification();
|
||||||
|
|
||||||
const isSidebarCollapsed = themeStore.sidebarCollapsed;
|
const isSidebarCollapsed = themeStore.sidebarCollapsed;
|
||||||
|
useOutsideClickDetector(notificationPopoverRef, () => {
|
||||||
|
// if snooze modal is open, then don't close the popover
|
||||||
|
if (selectedNotificationForSnooze === null) setIsActive(false);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -54,23 +62,22 @@ export const NotificationPopover = observer(() => {
|
|||||||
onClose={() => setSelectedNotificationForSnooze(null)}
|
onClose={() => setSelectedNotificationForSnooze(null)}
|
||||||
onSubmit={markSnoozeNotification}
|
onSubmit={markSnoozeNotification}
|
||||||
notification={notifications?.find((notification) => notification.id === selectedNotificationForSnooze) || null}
|
notification={notifications?.find((notification) => notification.id === selectedNotificationForSnooze) || null}
|
||||||
onSuccess={() => {
|
onSuccess={() => setSelectedNotificationForSnooze(null)}
|
||||||
setSelectedNotificationForSnooze(null);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Popover className="relative w-full">
|
<Popover ref={notificationPopoverRef} className="md:relative w-full">
|
||||||
{({ open: isActive, close: closePopover }) => {
|
|
||||||
if (isActive) setFetchNotifications(true);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
<>
|
||||||
<Tooltip tooltipContent="Notifications" position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
<Tooltip tooltipContent="Notifications" position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
||||||
<Popover.Button
|
<button
|
||||||
className={`group relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
className={`group relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
||||||
isActive
|
isActive
|
||||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||||
} ${isSidebarCollapsed ? "justify-center" : ""}`}
|
} ${isSidebarCollapsed ? "justify-center" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.innerWidth < 768) themeStore.toggleSidebar();
|
||||||
|
if (!isActive) setFetchNotifications(true);
|
||||||
|
setIsActive(!isActive);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Bell className="h-4 w-4" />
|
<Bell className="h-4 w-4" />
|
||||||
{isSidebarCollapsed ? null : <span>Notifications</span>}
|
{isSidebarCollapsed ? null : <span>Notifications</span>}
|
||||||
@ -83,9 +90,10 @@ export const NotificationPopover = observer(() => {
|
|||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</Popover.Button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Transition
|
<Transition
|
||||||
|
show={isActive}
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="transition ease-out duration-200"
|
enter="transition ease-out duration-200"
|
||||||
enterFrom="opacity-0 translate-y-1"
|
enterFrom="opacity-0 translate-y-1"
|
||||||
@ -94,11 +102,14 @@ export const NotificationPopover = observer(() => {
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute -top-36 left-0 z-10 ml-8 flex h-[50vh] w-[20rem] flex-col rounded-xl border border-custom-border-300 bg-custom-background-100 shadow-lg md:left-full md:w-[36rem]">
|
<Popover.Panel
|
||||||
|
className="absolute top-0 left-[280px] md:-top-36 md:ml-8 md:h-[50vh] z-10 flex h-full w-[100vw] flex-col rounded-xl md:border border-custom-border-300 bg-custom-background-100 shadow-lg md:left-full md:w-[36rem]"
|
||||||
|
static
|
||||||
|
>
|
||||||
<NotificationHeader
|
<NotificationHeader
|
||||||
notificationCount={notificationCount}
|
notificationCount={notificationCount}
|
||||||
notificationMutate={notificationMutate}
|
notificationMutate={notificationMutate}
|
||||||
closePopover={closePopover}
|
closePopover={() => setIsActive(false)}
|
||||||
isRefreshing={isRefreshing}
|
isRefreshing={isRefreshing}
|
||||||
snoozed={snoozed}
|
snoozed={snoozed}
|
||||||
archived={archived}
|
archived={archived}
|
||||||
@ -117,9 +128,10 @@ export const NotificationPopover = observer(() => {
|
|||||||
<div className="divide-y divide-custom-border-100">
|
<div className="divide-y divide-custom-border-100">
|
||||||
{notifications.map((notification) => (
|
{notifications.map((notification) => (
|
||||||
<NotificationCard
|
<NotificationCard
|
||||||
|
selectedTab={selectedTab}
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
isSnoozedTabOpen={snoozed}
|
isSnoozedTabOpen={snoozed}
|
||||||
closePopover={closePopover}
|
closePopover={() => setIsActive(false)}
|
||||||
notification={notification}
|
notification={notification}
|
||||||
markNotificationArchivedStatus={markNotificationArchivedStatus}
|
markNotificationArchivedStatus={markNotificationArchivedStatus}
|
||||||
markNotificationReadStatus={markNotificationAsRead}
|
markNotificationReadStatus={markNotificationAsRead}
|
||||||
@ -187,8 +199,6 @@ export const NotificationPopover = observer(() => {
|
|||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</>
|
</>
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Popover>
|
</Popover>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -109,7 +109,12 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
// This is a workaround to fix the issue of the Notification popover modal close on closing this modal
|
||||||
|
const closeTimeout = setTimeout(() => {
|
||||||
onClose();
|
onClose();
|
||||||
|
clearTimeout(closeTimeout);
|
||||||
|
}, 50);
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
reset({ ...defaultValues });
|
reset({ ...defaultValues });
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
@ -142,7 +147,7 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-2xl">
|
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all w-full sm:w-full sm:!max-w-2xl">
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||||
@ -156,8 +161,8 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 flex items-center gap-3">
|
<div className="mt-5 flex flex-col md:!flex-row md:items-center gap-3">
|
||||||
<div className="flex-1">
|
<div className="flex-1 pb-3 md:pb-0">
|
||||||
<h6 className="mb-2 block text-sm font-medium text-custom-text-400">Pick a date</h6>
|
<h6 className="mb-2 block text-sm font-medium text-custom-text-400">Pick a date</h6>
|
||||||
<Controller
|
<Controller
|
||||||
name="date"
|
name="date"
|
||||||
|
@ -11,11 +11,13 @@ import { WorkspaceService } from "services/workspace.service";
|
|||||||
// constants
|
// constants
|
||||||
import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
||||||
import { ROLE } from "constants/workspace";
|
import { ROLE } from "constants/workspace";
|
||||||
|
import { MEMBER_ACCEPTED } from "constants/event-tracker";
|
||||||
// types
|
// types
|
||||||
import { IWorkspaceMemberInvitation } from "@plane/types";
|
import { IWorkspaceMemberInvitation } from "@plane/types";
|
||||||
// icons
|
// icons
|
||||||
import { CheckCircle2, Search } from "lucide-react";
|
import { CheckCircle2, Search } from "lucide-react";
|
||||||
import {} from "hooks/store/use-event-tracker";
|
import {} from "hooks/store/use-event-tracker";
|
||||||
|
import { getUserRole } from "helpers/user.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleNextStep: () => void;
|
handleNextStep: () => void;
|
||||||
@ -58,11 +60,19 @@ export const Invitations: React.FC<Props> = (props) => {
|
|||||||
if (invitationsRespond.length <= 0) return;
|
if (invitationsRespond.length <= 0) return;
|
||||||
|
|
||||||
setIsJoiningWorkspaces(true);
|
setIsJoiningWorkspaces(true);
|
||||||
|
const invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]);
|
||||||
|
|
||||||
await workspaceService
|
await workspaceService
|
||||||
.joinWorkspaces({ invitations: invitationsRespond })
|
.joinWorkspaces({ invitations: invitationsRespond })
|
||||||
.then(async (res) => {
|
.then(async () => {
|
||||||
captureEvent("Member accepted", { ...res, state: "SUCCESS", accepted_from: "App" });
|
captureEvent(MEMBER_ACCEPTED, {
|
||||||
|
member_id: invitation?.id,
|
||||||
|
role: getUserRole(invitation?.role!),
|
||||||
|
project_id: undefined,
|
||||||
|
accepted_from: "App",
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Workspace invitations page",
|
||||||
|
});
|
||||||
await fetchWorkspaces();
|
await fetchWorkspaces();
|
||||||
await mutate(USER_WORKSPACES);
|
await mutate(USER_WORKSPACES);
|
||||||
await updateLastWorkspace();
|
await updateLastWorkspace();
|
||||||
@ -71,7 +81,14 @@ export const Invitations: React.FC<Props> = (props) => {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
captureEvent("Member accepted", { state: "FAILED", accepted_from: "App" });
|
captureEvent(MEMBER_ACCEPTED, {
|
||||||
|
member_id: invitation?.id,
|
||||||
|
role: getUserRole(invitation?.role!),
|
||||||
|
project_id: undefined,
|
||||||
|
accepted_from: "App",
|
||||||
|
state: "FAILED",
|
||||||
|
element: "Workspace invitations page",
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.finally(() => setIsJoiningWorkspaces(false));
|
.finally(() => setIsJoiningWorkspaces(false));
|
||||||
};
|
};
|
||||||
|
@ -18,6 +18,7 @@ import { Check, ChevronDown, Plus, XCircle } from "lucide-react";
|
|||||||
import { WorkspaceService } from "services/workspace.service";
|
import { WorkspaceService } from "services/workspace.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
@ -28,6 +29,9 @@ import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
|||||||
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
|
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
|
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
|
||||||
|
import { MEMBER_INVITED } from "constants/event-tracker";
|
||||||
|
// helpers
|
||||||
|
import { getUserRole } from "helpers/user.helper";
|
||||||
// assets
|
// assets
|
||||||
import user1 from "public/users/user-1.png";
|
import user1 from "public/users/user-1.png";
|
||||||
import user2 from "public/users/user-2.png";
|
import user2 from "public/users/user-2.png";
|
||||||
@ -267,6 +271,8 @@ export const InviteMembers: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
// store hooks
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@ -305,6 +311,17 @@ export const InviteMembers: React.FC<Props> = (props) => {
|
|||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
captureEvent(MEMBER_INVITED, {
|
||||||
|
emails: [
|
||||||
|
...payload.emails.map((email) => ({
|
||||||
|
email: email.email,
|
||||||
|
role: getUserRole(email.role),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
project_id: undefined,
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Onboarding",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -313,13 +330,18 @@ export const InviteMembers: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
await nextStep();
|
await nextStep();
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) => {
|
||||||
|
captureEvent(MEMBER_INVITED, {
|
||||||
|
project_id: undefined,
|
||||||
|
state: "FAILED",
|
||||||
|
element: "Onboarding",
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: err?.error,
|
message: err?.error,
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const appendField = () => {
|
const appendField = () => {
|
||||||
|
@ -15,6 +15,8 @@ import CyclesTour from "public/onboarding/cycles.webp";
|
|||||||
import ModulesTour from "public/onboarding/modules.webp";
|
import ModulesTour from "public/onboarding/modules.webp";
|
||||||
import ViewsTour from "public/onboarding/views.webp";
|
import ViewsTour from "public/onboarding/views.webp";
|
||||||
import PagesTour from "public/onboarding/pages.webp";
|
import PagesTour from "public/onboarding/pages.webp";
|
||||||
|
// constants
|
||||||
|
import { PRODUCT_TOUR_SKIPPED, PRODUCT_TOUR_STARTED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
@ -79,7 +81,7 @@ export const TourRoot: React.FC<Props> = observer((props) => {
|
|||||||
const [step, setStep] = useState<TTourSteps>("welcome");
|
const [step, setStep] = useState<TTourSteps>("welcome");
|
||||||
// store hooks
|
// store hooks
|
||||||
const { commandPalette: commandPaletteStore } = useApplication();
|
const { commandPalette: commandPaletteStore } = useApplication();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement, captureEvent } = useEventTracker();
|
||||||
const { currentUser } = useUser();
|
const { currentUser } = useUser();
|
||||||
|
|
||||||
const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step);
|
const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step);
|
||||||
@ -103,13 +105,22 @@ export const TourRoot: React.FC<Props> = observer((props) => {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex h-full items-end">
|
<div className="flex h-full items-end">
|
||||||
<div className="mt-8 flex items-center gap-6">
|
<div className="mt-8 flex items-center gap-6">
|
||||||
<Button variant="primary" onClick={() => setStep("issues")}>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
captureEvent(PRODUCT_TOUR_STARTED);
|
||||||
|
setStep("issues");
|
||||||
|
}}
|
||||||
|
>
|
||||||
Take a Product Tour
|
Take a Product Tour
|
||||||
</Button>
|
</Button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-transparent text-xs font-medium text-custom-primary-100 outline-custom-text-100"
|
className="bg-transparent text-xs font-medium text-custom-primary-100 outline-custom-text-100"
|
||||||
onClick={onComplete}
|
onClick={() => {
|
||||||
|
captureEvent(PRODUCT_TOUR_SKIPPED);
|
||||||
|
onComplete();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
No thanks, I will explore it myself
|
No thanks, I will explore it myself
|
||||||
</button>
|
</button>
|
||||||
@ -156,8 +167,8 @@ export const TourRoot: React.FC<Props> = observer((props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
setTrackElement("Product tour");
|
||||||
onComplete();
|
onComplete();
|
||||||
setTrackElement("Onboarding tour");
|
|
||||||
commandPaletteStore.toggleCreateProjectModal(true);
|
commandPaletteStore.toggleCreateProjectModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Camera, User2 } from "lucide-react";
|
import { Camera, User2 } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser, useWorkspace } from "hooks/store";
|
import { useEventTracker, useUser, useWorkspace } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
import { OnboardingSidebar, OnboardingStepIndicator } from "components/onboarding";
|
import { OnboardingSidebar, OnboardingStepIndicator } from "components/onboarding";
|
||||||
@ -15,6 +15,7 @@ import { IUser } from "@plane/types";
|
|||||||
import { FileService } from "services/file.service";
|
import { FileService } from "services/file.service";
|
||||||
// assets
|
// assets
|
||||||
import IssuesSvg from "public/onboarding/onboarding-issues.webp";
|
import IssuesSvg from "public/onboarding/onboarding-issues.webp";
|
||||||
|
import { USER_DETAILS } from "constants/event-tracker";
|
||||||
|
|
||||||
const defaultValues: Partial<IUser> = {
|
const defaultValues: Partial<IUser> = {
|
||||||
first_name: "",
|
first_name: "",
|
||||||
@ -48,6 +49,7 @@ export const UserDetails: React.FC<Props> = observer((props) => {
|
|||||||
// store hooks
|
// store hooks
|
||||||
const { updateCurrentUser } = useUser();
|
const { updateCurrentUser } = useUser();
|
||||||
const { workspaces } = useWorkspace();
|
const { workspaces } = useWorkspace();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// derived values
|
// derived values
|
||||||
const workspaceName = workspaces ? Object.values(workspaces)?.[0]?.name : "New Workspace";
|
const workspaceName = workspaces ? Object.values(workspaces)?.[0]?.name : "New Workspace";
|
||||||
// form info
|
// form info
|
||||||
@ -76,7 +78,21 @@ export const UserDetails: React.FC<Props> = observer((props) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await updateCurrentUser(payload);
|
await updateCurrentUser(payload)
|
||||||
|
.then(() => {
|
||||||
|
captureEvent(USER_DETAILS, {
|
||||||
|
use_case: formData.use_case,
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Onboarding",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
captureEvent(USER_DETAILS, {
|
||||||
|
use_case: formData.use_case,
|
||||||
|
state: "FAILED",
|
||||||
|
element: "Onboarding",
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const handleDelete = (url: string | null | undefined) => {
|
const handleDelete = (url: string | null | undefined) => {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
@ -5,12 +5,13 @@ import { Button, Input } from "@plane/ui";
|
|||||||
// types
|
// types
|
||||||
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
|
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser, useWorkspace } from "hooks/store";
|
import { useEventTracker, useUser, useWorkspace } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// services
|
// services
|
||||||
import { WorkspaceService } from "services/workspace.service";
|
import { WorkspaceService } from "services/workspace.service";
|
||||||
// constants
|
// constants
|
||||||
import { RESTRICTED_URLS } from "constants/workspace";
|
import { RESTRICTED_URLS } from "constants/workspace";
|
||||||
|
import { WORKSPACE_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
|
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
|
||||||
@ -33,6 +34,7 @@ export const Workspace: React.FC<Props> = (props) => {
|
|||||||
// store hooks
|
// store hooks
|
||||||
const { updateCurrentUser } = useUser();
|
const { updateCurrentUser } = useUser();
|
||||||
const { createWorkspace, fetchWorkspaces, workspaces } = useWorkspace();
|
const { createWorkspace, fetchWorkspaces, workspaces } = useWorkspace();
|
||||||
|
const { captureWorkspaceEvent } = useEventTracker();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -46,31 +48,48 @@ export const Workspace: React.FC<Props> = (props) => {
|
|||||||
setSlugError(false);
|
setSlugError(false);
|
||||||
|
|
||||||
await createWorkspace(formData)
|
await createWorkspace(formData)
|
||||||
.then(async () => {
|
.then(async (res) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "Workspace created successfully.",
|
message: "Workspace created successfully.",
|
||||||
});
|
});
|
||||||
|
captureWorkspaceEvent({
|
||||||
|
eventName: WORKSPACE_CREATED,
|
||||||
|
payload: {
|
||||||
|
...res,
|
||||||
|
state: "SUCCESS",
|
||||||
|
first_time: true,
|
||||||
|
element: "Onboarding",
|
||||||
|
},
|
||||||
|
});
|
||||||
await fetchWorkspaces();
|
await fetchWorkspaces();
|
||||||
await completeStep();
|
await completeStep();
|
||||||
})
|
})
|
||||||
|
.catch(() => {
|
||||||
|
captureWorkspaceEvent({
|
||||||
|
eventName: WORKSPACE_CREATED,
|
||||||
|
payload: {
|
||||||
|
state: "FAILED",
|
||||||
|
first_time: true,
|
||||||
|
element: "Onboarding",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Workspace could not be created. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else setSlugError(true);
|
||||||
|
})
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Workspace could not be created. Please try again.",
|
message: "Some error occurred while creating workspace. Please try again.",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else setSlugError(true);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Some error occurred while creating workspace. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const completeStep = async () => {
|
const completeStep = async () => {
|
||||||
|
@ -13,6 +13,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
|||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker";
|
||||||
|
|
||||||
export const WorkspaceDashboardView = observer(() => {
|
export const WorkspaceDashboardView = observer(() => {
|
||||||
// theme
|
// theme
|
||||||
@ -37,9 +38,8 @@ export const WorkspaceDashboardView = observer(() => {
|
|||||||
const handleTourCompleted = () => {
|
const handleTourCompleted = () => {
|
||||||
updateTourCompleted()
|
updateTourCompleted()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
captureEvent("User tour complete", {
|
captureEvent(PRODUCT_TOUR_COMPLETED, {
|
||||||
user_id: currentUser?.id,
|
user_id: currentUser?.id,
|
||||||
email: currentUser?.email,
|
|
||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -84,7 +84,7 @@ export const WorkspaceDashboardView = observer(() => {
|
|||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "Build your first project",
|
text: "Build your first project",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTrackElement("Dashboard");
|
setTrackElement("Dashboard empty state");
|
||||||
toggleCreateProjectModal(true);
|
toggleCreateProjectModal(true);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
@ -3,10 +3,14 @@ import { useRouter } from "next/router";
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// components
|
// components
|
||||||
import { PageForm } from "./page-form";
|
import { PageForm } from "./page-form";
|
||||||
|
// hooks
|
||||||
|
import { useEventTracker } from "hooks/store";
|
||||||
// types
|
// types
|
||||||
import { IPage } from "@plane/types";
|
import { IPage } from "@plane/types";
|
||||||
import { useProjectPages } from "hooks/store/use-project-page";
|
import { useProjectPages } from "hooks/store/use-project-page";
|
||||||
import { IPageStore } from "store/page.store";
|
import { IPageStore } from "store/page.store";
|
||||||
|
// constants
|
||||||
|
import { PAGE_CREATED, PAGE_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
// data?: IPage | null;
|
// data?: IPage | null;
|
||||||
@ -21,12 +25,30 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
// store hooks
|
||||||
const { createPage } = useProjectPages();
|
const { createPage } = useProjectPages();
|
||||||
|
const { capturePageEvent } = useEventTracker();
|
||||||
|
|
||||||
const createProjectPage = async (payload: IPage) => {
|
const createProjectPage = async (payload: IPage) => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
await createPage(workspaceSlug.toString(), projectId, payload);
|
await createPage(workspaceSlug.toString(), projectId, payload)
|
||||||
|
.then((res) => {
|
||||||
|
capturePageEvent({
|
||||||
|
eventName: PAGE_CREATED,
|
||||||
|
payload: {
|
||||||
|
...res,
|
||||||
|
state: "SUCCESS",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
capturePageEvent({
|
||||||
|
eventName: PAGE_CREATED,
|
||||||
|
payload: {
|
||||||
|
state: "FAILED",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: IPage) => {
|
const handleFormSubmit = async (formData: IPage) => {
|
||||||
@ -39,6 +61,14 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
|||||||
if (pageStore.access !== formData.access) {
|
if (pageStore.access !== formData.access) {
|
||||||
formData.access === 1 ? await pageStore.makePrivate() : await pageStore.makePublic();
|
formData.access === 1 ? await pageStore.makePrivate() : await pageStore.makePublic();
|
||||||
}
|
}
|
||||||
|
capturePageEvent({
|
||||||
|
eventName: PAGE_UPDATED,
|
||||||
|
payload: {
|
||||||
|
...pageStore,
|
||||||
|
state: "SUCCESS",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log("Page updated successfully", pageStore);
|
||||||
} else {
|
} else {
|
||||||
await createProjectPage(formData);
|
await createProjectPage(formData);
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,14 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePage } from "hooks/store";
|
import { useEventTracker, usePage } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { useProjectPages } from "hooks/store/use-project-page";
|
import { useProjectPages } from "hooks/store/use-project-page";
|
||||||
|
// constants
|
||||||
|
import { PAGE_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
type TConfirmPageDeletionProps = {
|
type TConfirmPageDeletionProps = {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@ -27,6 +29,7 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
|
|||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { deletePage } = useProjectPages();
|
const { deletePage } = useProjectPages();
|
||||||
|
const { capturePageEvent } = useEventTracker();
|
||||||
const pageStore = usePage(pageId);
|
const pageStore = usePage(pageId);
|
||||||
|
|
||||||
// toast alert
|
// toast alert
|
||||||
@ -49,6 +52,13 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
|
|||||||
// Delete Page will only delete the page from the archive page map, at this point only archived pages can be deleted
|
// Delete Page will only delete the page from the archive page map, at this point only archived pages can be deleted
|
||||||
await deletePage(workspaceSlug.toString(), projectId as string, pageId)
|
await deletePage(workspaceSlug.toString(), projectId as string, pageId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
capturePageEvent({
|
||||||
|
eventName: PAGE_DELETED,
|
||||||
|
payload: {
|
||||||
|
...pageStore,
|
||||||
|
state: "SUCCESS",
|
||||||
|
},
|
||||||
|
});
|
||||||
handleClose();
|
handleClose();
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -57,6 +67,13 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
capturePageEvent({
|
||||||
|
eventName: PAGE_DELETED,
|
||||||
|
payload: {
|
||||||
|
...pageStore,
|
||||||
|
state: "FAILED",
|
||||||
|
},
|
||||||
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
|
@ -22,13 +22,12 @@ export const ProfileNavbar: React.FC<Props> = (props) => {
|
|||||||
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sticky -top-0.5 z-10 flex items-center justify-between gap-4 border-b border-custom-border-300 bg-custom-background-100 px-4 sm:px-5 md:static">
|
<div className="sticky -top-0.5 z-10 hidden md:flex items-center justify-between gap-4 border-b border-custom-border-300 bg-custom-background-100 px-4 sm:px-5 md:static">
|
||||||
<div className="flex items-center overflow-x-scroll">
|
<div className="flex items-center overflow-x-scroll">
|
||||||
{tabsList.map((tab) => (
|
{tabsList.map((tab) => (
|
||||||
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
|
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
|
||||||
<span
|
<span
|
||||||
className={`flex whitespace-nowrap border-b-2 p-4 text-sm font-medium outline-none ${
|
className={`flex whitespace-nowrap border-b-2 p-4 text-sm font-medium outline-none ${router.pathname === tab.selected
|
||||||
router.pathname === tab.selected
|
|
||||||
? "border-custom-primary-100 text-custom-primary-100"
|
? "border-custom-primary-100 text-custom-primary-100"
|
||||||
: "border-transparent"
|
: "border-transparent"
|
||||||
}`}
|
}`}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button, Checkbox } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// services
|
// services
|
||||||
@ -23,6 +23,7 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
|||||||
// form data
|
// form data
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
watch,
|
||||||
control,
|
control,
|
||||||
setValue,
|
setValue,
|
||||||
formState: { isSubmitting, isDirty, dirtyFields },
|
formState: { isSubmitting, isDirty, dirtyFields },
|
||||||
@ -78,12 +79,7 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
|||||||
control={control}
|
control={control}
|
||||||
name="property_change"
|
name="property_change"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<input
|
<Checkbox checked={value} onChange={() => onChange(!value)} className="mx-2" />
|
||||||
type="checkbox"
|
|
||||||
checked={value}
|
|
||||||
onChange={() => onChange(!value)}
|
|
||||||
className="w-3.5 h-3.5 mx-2 cursor-pointer !border-custom-border-100"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -100,14 +96,14 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
|||||||
control={control}
|
control={control}
|
||||||
name="state_change"
|
name="state_change"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
checked={value}
|
checked={value}
|
||||||
|
intermediate={!value && watch("issue_completed")}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setValue("issue_completed", !value);
|
setValue("issue_completed", !value);
|
||||||
onChange(!value);
|
onChange(!value);
|
||||||
}}
|
}}
|
||||||
className="w-3.5 h-3.5 mx-2 cursor-pointer"
|
className="mx-2"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -123,12 +119,7 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
|||||||
control={control}
|
control={control}
|
||||||
name="issue_completed"
|
name="issue_completed"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<input
|
<Checkbox checked={value} onChange={() => onChange(!value)} className="mx-2" />
|
||||||
type="checkbox"
|
|
||||||
checked={value}
|
|
||||||
onChange={() => onChange(!value)}
|
|
||||||
className="w-3.5 h-3.5 mx-2 cursor-pointer"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -145,12 +136,7 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
|||||||
control={control}
|
control={control}
|
||||||
name="comment"
|
name="comment"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<input
|
<Checkbox checked={value} onChange={() => onChange(!value)} className="mx-2" />
|
||||||
type="checkbox"
|
|
||||||
checked={value}
|
|
||||||
onChange={() => onChange(!value)}
|
|
||||||
className="w-3.5 h-3.5 mx-2 cursor-pointer"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -167,12 +153,7 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
|||||||
control={control}
|
control={control}
|
||||||
name="mention"
|
name="mention"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<input
|
<Checkbox checked={value} onChange={() => onChange(!value)} className="mx-2" />
|
||||||
type="checkbox"
|
|
||||||
checked={value}
|
|
||||||
onChange={() => onChange(!value)}
|
|
||||||
className="w-3.5 h-3.5 mx-2 cursor-pointer"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@ import useSWR from "swr";
|
|||||||
import { Disclosure, Transition } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useApplication, useUser } from "hooks/store";
|
||||||
// services
|
// services
|
||||||
import { UserService } from "services/user.service";
|
import { UserService } from "services/user.service";
|
||||||
// components
|
// components
|
||||||
@ -18,6 +18,8 @@ import { renderFormattedDate } from "helpers/date-time.helper";
|
|||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
|
import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
@ -28,6 +30,8 @@ export const ProfileSidebar = observer(() => {
|
|||||||
const { workspaceSlug, userId } = router.query;
|
const { workspaceSlug, userId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { currentUser } = useUser();
|
const { currentUser } = useUser();
|
||||||
|
const { theme: themeStore } = useApplication();
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { data: userProjectsData } = useSWR(
|
const { data: userProjectsData } = useSWR(
|
||||||
workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null,
|
workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null,
|
||||||
@ -36,6 +40,14 @@ export const ProfileSidebar = observer(() => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useOutsideClickDetector(ref, () => {
|
||||||
|
if (themeStore.profileSidebarCollapsed === false) {
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
themeStore.toggleProfileSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const userDetails = [
|
const userDetails = [
|
||||||
{
|
{
|
||||||
label: "Joined on",
|
label: "Joined on",
|
||||||
@ -47,8 +59,26 @@ export const ProfileSidebar = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleToggleProfileSidebar = () => {
|
||||||
|
if (window && window.innerWidth < 768) {
|
||||||
|
themeStore.toggleProfileSidebar(true);
|
||||||
|
}
|
||||||
|
if (window && themeStore.profileSidebarCollapsed && window.innerWidth >= 768) {
|
||||||
|
themeStore.toggleProfileSidebar(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleToggleProfileSidebar);
|
||||||
|
handleToggleProfileSidebar();
|
||||||
|
return () => window.removeEventListener("resize", handleToggleProfileSidebar);
|
||||||
|
}, [themeStore]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex-shrink-0 overflow-y-auto shadow-custom-shadow-sm md:h-full md:w-80 border-l border-custom-border-100">
|
<div
|
||||||
|
className={`flex-shrink-0 overflow-hidden overflow-y-auto shadow-custom-shadow-sm border-l border-custom-border-100 bg-custom-sidebar-background-100 h-full z-[5] fixed md:relative transition-all w-full md:w-[300px]`}
|
||||||
|
style={themeStore.profileSidebarCollapsed ? { marginLeft: `${window?.innerWidth || 0}px` } : {}}
|
||||||
|
>
|
||||||
{userProjectsData ? (
|
{userProjectsData ? (
|
||||||
<>
|
<>
|
||||||
<div className="relative h-32">
|
<div className="relative h-32">
|
||||||
@ -132,8 +162,7 @@ export const ProfileSidebar = observer(() => {
|
|||||||
{project.assigned_issues > 0 && (
|
{project.assigned_issues > 0 && (
|
||||||
<Tooltip tooltipContent="Completion percentage" position="left">
|
<Tooltip tooltipContent="Completion percentage" position="left">
|
||||||
<div
|
<div
|
||||||
className={`rounded px-1 py-0.5 text-xs font-medium ${
|
className={`rounded px-1 py-0.5 text-xs font-medium ${completedIssuePercentage <= 35
|
||||||
completedIssuePercentage <= 35
|
|
||||||
? "bg-red-500/10 text-red-500"
|
? "bg-red-500/10 text-red-500"
|
||||||
: completedIssuePercentage <= 70
|
: completedIssuePercentage <= 70
|
||||||
? "bg-yellow-500/10 text-yellow-500"
|
? "bg-yellow-500/10 text-yellow-500"
|
||||||
|
@ -18,6 +18,7 @@ import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
|
|||||||
import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project";
|
import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { PROJECT_CREATED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -134,13 +135,8 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
|||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
};
|
};
|
||||||
captureProjectEvent({
|
captureProjectEvent({
|
||||||
eventName: "Project created",
|
eventName: PROJECT_CREATED,
|
||||||
payload: newPayload,
|
payload: newPayload,
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: res.workspace,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -160,16 +156,11 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
|||||||
message: err.data[key],
|
message: err.data[key],
|
||||||
});
|
});
|
||||||
captureProjectEvent({
|
captureProjectEvent({
|
||||||
eventName: "Project created",
|
eventName: PROJECT_CREATED,
|
||||||
payload: {
|
payload: {
|
||||||
...payload,
|
...payload,
|
||||||
state: "FAILED",
|
state: "FAILED",
|
||||||
},
|
}
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,8 @@ import useToast from "hooks/use-toast";
|
|||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import type { IProject } from "@plane/types";
|
import type { IProject } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { PROJECT_DELETED } from "constants/event-tracker";
|
||||||
|
|
||||||
type DeleteProjectModal = {
|
type DeleteProjectModal = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -62,13 +64,8 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
|
|||||||
|
|
||||||
handleClose();
|
handleClose();
|
||||||
captureProjectEvent({
|
captureProjectEvent({
|
||||||
eventName: "Project deleted",
|
eventName: PROJECT_DELETED,
|
||||||
payload: { ...project, state: "SUCCESS", element: "Project general settings" },
|
payload: { ...project, state: "SUCCESS", element: "Project general settings" },
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -78,13 +75,8 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
captureProjectEvent({
|
captureProjectEvent({
|
||||||
eventName: "Project deleted",
|
eventName: PROJECT_DELETED,
|
||||||
payload: { ...project, state: "FAILED", element: "Project general settings" },
|
payload: { ...project, state: "FAILED", element: "Project general settings" },
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
|
@ -18,6 +18,7 @@ import { renderFormattedDate } from "helpers/date-time.helper";
|
|||||||
import { NETWORK_CHOICES } from "constants/project";
|
import { NETWORK_CHOICES } from "constants/project";
|
||||||
// services
|
// services
|
||||||
import { ProjectService } from "services/project";
|
import { ProjectService } from "services/project";
|
||||||
|
import { PROJECT_UPDATED } from "constants/event-tracker";
|
||||||
|
|
||||||
export interface IProjectDetailsForm {
|
export interface IProjectDetailsForm {
|
||||||
project: IProject;
|
project: IProject;
|
||||||
@ -45,7 +46,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
setValue,
|
setValue,
|
||||||
setError,
|
setError,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors },
|
formState: { errors, dirtyFields },
|
||||||
} = useForm<IProject>({
|
} = useForm<IProject>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...project,
|
...project,
|
||||||
@ -77,13 +78,15 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
|
|
||||||
return updateProject(workspaceSlug.toString(), project.id, payload)
|
return updateProject(workspaceSlug.toString(), project.id, payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
const changed_properties = Object.keys(dirtyFields);
|
||||||
|
console.log(dirtyFields);
|
||||||
captureProjectEvent({
|
captureProjectEvent({
|
||||||
eventName: "Project updated",
|
eventName: PROJECT_UPDATED,
|
||||||
payload: { ...res, state: "SUCCESS", element: "Project general settings" },
|
payload: {
|
||||||
group: {
|
...res,
|
||||||
isGrouping: true,
|
changed_properties: changed_properties,
|
||||||
groupType: "Workspace_metrics",
|
state: "SUCCESS",
|
||||||
groupId: res.workspace,
|
element: "Project general settings",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -94,13 +97,8 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
captureProjectEvent({
|
captureProjectEvent({
|
||||||
eventName: "Project updated",
|
eventName: PROJECT_UPDATED,
|
||||||
payload: { ...payload, state: "FAILED", element: "Project general settings" },
|
payload: { ...payload, state: "FAILED", element: "Project general settings" },
|
||||||
group: {
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
@ -153,7 +151,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
||||||
|
|
||||||
<img src={watch("cover_image")!} alt={watch("cover_image")!} className="h-44 w-full rounded-md object-cover" />
|
<img src={watch("cover_image")!} alt={watch("cover_image")!} className="h-44 w-full rounded-md object-cover" />
|
||||||
<div className="absolute bottom-4 z-5 flex w-full items-end justify-between gap-3 px-4">
|
<div className="z-5 absolute bottom-4 flex w-full items-end justify-between gap-3 px-4">
|
||||||
<div className="flex flex-grow gap-3 truncate">
|
<div className="flex flex-grow gap-3 truncate">
|
||||||
<div className="flex h-[52px] w-[52px] flex-shrink-0 items-center justify-center rounded-lg bg-custom-background-90">
|
<div className="flex h-[52px] w-[52px] flex-shrink-0 items-center justify-center rounded-lg bg-custom-background-90">
|
||||||
<div className="grid h-7 w-7 place-items-center">
|
<div className="grid h-7 w-7 place-items-center">
|
||||||
|
@ -11,6 +11,8 @@ import useToast from "hooks/use-toast";
|
|||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IProject } from "@plane/types";
|
import { IProject } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { PROJECT_MEMBER_LEAVE } from "constants/event-tracker";
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
@ -63,8 +65,9 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
handleClose();
|
handleClose();
|
||||||
router.push(`/${workspaceSlug}/projects`);
|
router.push(`/${workspaceSlug}/projects`);
|
||||||
captureEvent("Project member leave", {
|
captureEvent(PROJECT_MEMBER_LEAVE, {
|
||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
|
element: "Project settings members page",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -73,8 +76,9 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
|
|||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Something went wrong please try again later.",
|
message: "Something went wrong please try again later.",
|
||||||
});
|
});
|
||||||
captureEvent("Project member leave", {
|
captureEvent(PROJECT_MEMBER_LEAVE, {
|
||||||
state: "FAILED",
|
state: "FAILED",
|
||||||
|
element: "Project settings members page",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMember, useProject, useUser } from "hooks/store";
|
import { useEventTracker, useMember, useProject, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { ConfirmProjectMemberRemove } from "components/project";
|
import { ConfirmProjectMemberRemove } from "components/project";
|
||||||
@ -14,6 +14,7 @@ import { ChevronDown, Dot, XCircle } from "lucide-react";
|
|||||||
// constants
|
// constants
|
||||||
import { ROLE } from "constants/workspace";
|
import { ROLE } from "constants/workspace";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { PROJECT_MEMBER_LEAVE } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -35,6 +36,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
project: { removeMemberFromProject, getProjectMemberDetails, updateMember },
|
project: { removeMemberFromProject, getProjectMemberDetails, updateMember },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
|
const { captureEvent } = useEventTracker();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -48,8 +50,11 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||||||
if (userDetails.member.id === currentUser?.id) {
|
if (userDetails.member.id === currentUser?.id) {
|
||||||
await leaveProject(workspaceSlug.toString(), projectId.toString())
|
await leaveProject(workspaceSlug.toString(), projectId.toString())
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
captureEvent(PROJECT_MEMBER_LEAVE, {
|
||||||
|
state: "SUCCESS",
|
||||||
|
element: "Project settings members page",
|
||||||
|
});
|
||||||
await fetchProjects(workspaceSlug.toString());
|
await fetchProjects(workspaceSlug.toString());
|
||||||
|
|
||||||
router.push(`/${workspaceSlug}/projects`);
|
router.push(`/${workspaceSlug}/projects`);
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
|
@ -9,9 +9,12 @@ import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar, Button, CustomSelect, CustomSearchSelect } from "@plane/ui";
|
import { Avatar, Button, CustomSelect, CustomSearchSelect } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { getUserRole } from "helpers/user.helper";
|
||||||
// constants
|
// constants
|
||||||
import { ROLE } from "constants/workspace";
|
import { ROLE } from "constants/workspace";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { PROJECT_MEMBER_ADDED } from "constants/event-tracker";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -49,7 +52,6 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const {
|
const {
|
||||||
project: { projectMemberIds, bulkAddMembersToProject },
|
project: { projectMemberIds, bulkAddMembersToProject },
|
||||||
workspace: { workspaceMemberIds, getWorkspaceMemberDetails },
|
workspace: { workspaceMemberIds, getWorkspaceMemberDetails },
|
||||||
@ -79,7 +81,7 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
const payload = { ...formData };
|
const payload = { ...formData };
|
||||||
|
|
||||||
await bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload)
|
await bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload)
|
||||||
.then((res) => {
|
.then(() => {
|
||||||
if (onSuccess) onSuccess();
|
if (onSuccess) onSuccess();
|
||||||
onClose();
|
onClose();
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -87,32 +89,23 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
message: "Members added successfully.",
|
message: "Members added successfully.",
|
||||||
});
|
});
|
||||||
captureEvent(
|
captureEvent(PROJECT_MEMBER_ADDED, {
|
||||||
"Member added",
|
members: [
|
||||||
{
|
...payload.members.map((member) => ({
|
||||||
...res,
|
member_id: member.member_id,
|
||||||
|
role: ROLE[member.role],
|
||||||
|
})),
|
||||||
|
],
|
||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
},
|
element: "Project settings members page",
|
||||||
{
|
});
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
captureEvent(
|
captureEvent(PROJECT_MEMBER_ADDED, {
|
||||||
"Member added",
|
|
||||||
{
|
|
||||||
state: "FAILED",
|
state: "FAILED",
|
||||||
},
|
element: "Project settings members page",
|
||||||
{
|
});
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
reset(defaultValues);
|
reset(defaultValues);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user