[WEB-3160][WEB-3162] feat: teamspace and initiative notifications (#4420)
This commit is contained in:
@@ -18,6 +18,7 @@ from plane.db.models import (
|
||||
UserNotificationPreference,
|
||||
WorkspaceMember,
|
||||
)
|
||||
from plane.db.models.notification import EntityName
|
||||
from plane.utils.paginator import BasePaginator
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
|
||||
@@ -58,7 +59,14 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
|
||||
notifications = (
|
||||
Notification.objects.filter(workspace__slug=slug, receiver_id=request.user.id)
|
||||
.filter(entity_name__in=["issue", "epic"])
|
||||
.filter(
|
||||
entity_name__in=[
|
||||
EntityName.ISSUE.value,
|
||||
EntityName.EPIC.value,
|
||||
EntityName.INITIATIVE.value,
|
||||
EntityName.TEAMSPACE.value,
|
||||
]
|
||||
)
|
||||
.annotate(is_inbox_issue=Exists(intake_issue))
|
||||
.annotate(is_intake_issue=Exists(intake_issue))
|
||||
.annotate(
|
||||
|
||||
@@ -19,6 +19,8 @@ from plane.db.models import EmailNotificationLog, Issue, User, IssueSubscriber,
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.db.models.notification import EntityName
|
||||
from plane.ee.models import Teamspace, Initiative
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
@@ -28,19 +30,6 @@ def remove_unwanted_characters(input_text):
|
||||
return processed_text
|
||||
|
||||
|
||||
# acquire and delete redis lock
|
||||
def acquire_lock(lock_id, expire_time=300):
|
||||
redis_client = redis_instance()
|
||||
"""Attempt to acquire a lock with a specified expiration time."""
|
||||
return redis_client.set(lock_id, "true", nx=True, ex=expire_time)
|
||||
|
||||
|
||||
def release_lock(lock_id):
|
||||
"""Release a lock."""
|
||||
redis_client = redis_instance()
|
||||
redis_client.delete(lock_id)
|
||||
|
||||
|
||||
@shared_task
|
||||
def stack_email_notification():
|
||||
# get all email notifications
|
||||
@@ -70,30 +59,43 @@ def stack_email_notification():
|
||||
processed_notifications.append(receiver_notification.get("id"))
|
||||
email_notification_ids.append(receiver_notification.get("id"))
|
||||
|
||||
# Create emails for all the issues
|
||||
for issue_id, notification_data in payload.items():
|
||||
send_email_notification.delay(
|
||||
issue_id=issue_id,
|
||||
notification_data=notification_data,
|
||||
receiver_id=receiver_id,
|
||||
email_notification_ids=email_notification_ids,
|
||||
)
|
||||
entity_name = receiver_notification.get("entity_name")
|
||||
|
||||
if entity_name == EntityName.ISSUE.value or entity_name == EntityName.EPIC.value:
|
||||
# Create emails for all the issues
|
||||
for issue_id, notification_data in payload.items():
|
||||
send_email_notification.delay(
|
||||
issue_id=issue_id,
|
||||
notification_data=notification_data,
|
||||
receiver_id=receiver_id,
|
||||
email_notification_ids=email_notification_ids,
|
||||
)
|
||||
else:
|
||||
for entity_id, notification_data in payload.items():
|
||||
send_workspace_level_email_notification.delay(
|
||||
entity_id=entity_id,
|
||||
entity_name=entity_name,
|
||||
notification_data=notification_data,
|
||||
receiver_id=receiver_id,
|
||||
email_notification_ids=email_notification_ids,
|
||||
)
|
||||
|
||||
# Update the email notification log
|
||||
EmailNotificationLog.objects.filter(pk__in=processed_notifications).update(processed_at=timezone.now())
|
||||
|
||||
|
||||
def create_payload(notification_data):
|
||||
def create_payload(notification_data, entity_name):
|
||||
# return format {"actor_id": { "key": { "old_value": [], "new_value": [] } }}
|
||||
data = {}
|
||||
for actor_id, changes in notification_data.items():
|
||||
for change in changes:
|
||||
issue_activity = change.get("issue_activity", None)
|
||||
entity_activity = change.get(f"{entity_name}_activity", None)
|
||||
epic_update = change.get("epic_update", None)
|
||||
if issue_activity: # Ensure issue_activity is not None
|
||||
field = issue_activity.get("field")
|
||||
old_value = str(issue_activity.get("old_value"))
|
||||
new_value = str(issue_activity.get("new_value"))
|
||||
|
||||
if entity_activity:
|
||||
field = entity_activity.get("field")
|
||||
old_value = str(entity_activity.get("old_value"))
|
||||
new_value = str(entity_activity.get("new_value"))
|
||||
|
||||
# Append old_value if it's not empty and not already in the list
|
||||
if old_value:
|
||||
@@ -118,7 +120,7 @@ def create_payload(notification_data):
|
||||
|
||||
if not data.get("actor_id", {}).get("activity_time", False):
|
||||
data[actor_id]["activity_time"] = str(
|
||||
datetime.fromisoformat(issue_activity.get("activity_time").rstrip("Z")).strftime(
|
||||
datetime.fromisoformat(entity_activity.get("activity_time").rstrip("Z")).strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
)
|
||||
@@ -208,173 +210,226 @@ def process_html_content(content):
|
||||
|
||||
@shared_task
|
||||
def send_email_notification(issue_id, notification_data, receiver_id, email_notification_ids):
|
||||
# Convert UUIDs to a sorted, concatenated string
|
||||
sorted_ids = sorted(email_notification_ids)
|
||||
ids_str = "_".join(str(id) for id in sorted_ids)
|
||||
lock_id = f"send_email_notif_{issue_id}_{receiver_id}_{ids_str}"
|
||||
|
||||
# acquire the lock for sending emails
|
||||
try:
|
||||
if acquire_lock(lock_id=lock_id):
|
||||
base_api = settings.WEB_URL
|
||||
base_api = settings.WEB_URL
|
||||
|
||||
# Skip if base api is not present
|
||||
if not base_api:
|
||||
return
|
||||
# Skip if base api is not present
|
||||
if not base_api:
|
||||
return
|
||||
|
||||
data = create_payload(notification_data=notification_data)
|
||||
data = create_payload(
|
||||
notification_data=notification_data,
|
||||
)
|
||||
|
||||
# Get email configurations
|
||||
(
|
||||
EMAIL_HOST,
|
||||
EMAIL_HOST_USER,
|
||||
EMAIL_HOST_PASSWORD,
|
||||
EMAIL_PORT,
|
||||
EMAIL_USE_TLS,
|
||||
EMAIL_USE_SSL,
|
||||
EMAIL_FROM,
|
||||
) = get_email_configuration()
|
||||
# Get email configurations
|
||||
|
||||
try:
|
||||
receiver = User.objects.get(pk=receiver_id)
|
||||
issue = Issue.objects.get(pk=issue_id)
|
||||
except (Issue.DoesNotExist, User.DoesNotExist):
|
||||
return
|
||||
|
||||
if issue.type and issue.type.is_epic:
|
||||
entity_type = "epic"
|
||||
else:
|
||||
entity_type = "work-item"
|
||||
if issue.type and issue.type.is_epic:
|
||||
entity_type = "epic"
|
||||
else:
|
||||
entity_type = "work-item"
|
||||
|
||||
template_data = []
|
||||
total_changes = 0
|
||||
comments = []
|
||||
actors_involved = []
|
||||
template_data = []
|
||||
comments = []
|
||||
actors_involved = []
|
||||
|
||||
for actor_id, changes in data.items():
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
total_changes = total_changes + len(changes)
|
||||
comment = changes.pop("comment", False)
|
||||
mention = changes.pop("mention", False)
|
||||
actors_involved.append(actor_id)
|
||||
if comment:
|
||||
comments.append(
|
||||
{
|
||||
"actor_comments": comment,
|
||||
"actor_detail": {
|
||||
"avatar_url": f"{base_api}{actor.avatar_url}",
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
}
|
||||
)
|
||||
if mention:
|
||||
mention["new_value"] = process_html_content(mention.get("new_value"))
|
||||
mention["old_value"] = process_html_content(mention.get("old_value"))
|
||||
comments.append(
|
||||
{
|
||||
"actor_comments": mention,
|
||||
"actor_detail": {
|
||||
"avatar_url": f"{base_api}{actor.avatar_url}",
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
}
|
||||
)
|
||||
activity_time = changes.pop("activity_time")
|
||||
for actor_id, changes in data.items():
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
comment = changes.pop("comment", False)
|
||||
mention = changes.pop("mention", False)
|
||||
actors_involved.append(actor_id)
|
||||
if comment:
|
||||
comments.append(
|
||||
{
|
||||
"actor_comments": comment,
|
||||
"actor_detail": {
|
||||
"avatar_url": f"{base_api}{actor.avatar_url}",
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
}
|
||||
)
|
||||
if mention:
|
||||
mention["new_value"] = process_html_content(mention.get("new_value"))
|
||||
mention["old_value"] = process_html_content(mention.get("old_value"))
|
||||
comments.append(
|
||||
{
|
||||
"actor_comments": mention,
|
||||
"actor_detail": {
|
||||
"avatar_url": f"{base_api}{actor.avatar_url}",
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
}
|
||||
)
|
||||
activity_time = changes.pop("activity_time")
|
||||
|
||||
field = changes.pop("field")
|
||||
field = changes.pop("field")
|
||||
|
||||
if field == "epic_update":
|
||||
summary = "Updates were added to the Epic"
|
||||
template_name = "emails/notifications/epic_updates.html"
|
||||
formatted_time = activity_time
|
||||
else:
|
||||
summary = "Updates were made to the issue by"
|
||||
template_name = "emails/notifications/issue-updates.html"
|
||||
formatted_time = datetime.strptime(activity_time, "%Y-%m-%d %H:%M:%S").strftime("%H:%M %p")
|
||||
if field == "epic_update":
|
||||
summary = "Updates were added to the Epic"
|
||||
template_name = "emails/notifications/epic_updates.html"
|
||||
formatted_time = activity_time
|
||||
else:
|
||||
summary = "Updates were made to the issue by"
|
||||
template_name = "emails/notifications/issue-updates.html"
|
||||
formatted_time = datetime.strptime(activity_time, "%Y-%m-%d %H:%M:%S").strftime("%H:%M %p")
|
||||
|
||||
if changes:
|
||||
template_data.append(
|
||||
{
|
||||
"actor_detail": {
|
||||
"avatar_url": f"{base_api}{actor.avatar_url}",
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
"changes": changes,
|
||||
"issue_details": {
|
||||
"name": issue.name,
|
||||
"identifier": f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
},
|
||||
"activity_time": str(formatted_time),
|
||||
}
|
||||
)
|
||||
if changes:
|
||||
template_data.append(
|
||||
{
|
||||
"actor_detail": {
|
||||
"avatar_url": f"{base_api}{actor.avatar_url}",
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
"changes": changes,
|
||||
"issue_details": {
|
||||
"name": issue.name,
|
||||
"identifier": f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
},
|
||||
"activity_time": str(formatted_time),
|
||||
}
|
||||
)
|
||||
|
||||
issue_identifier = f"{str(issue.project.identifier)}-{str(issue.sequence_id)}"
|
||||
summary = "Updates were made to the issue by"
|
||||
issue_identifier = f"{str(issue.project.identifier)}-{str(issue.sequence_id)}"
|
||||
|
||||
# Send the mail
|
||||
subject = f"{issue.project.identifier}-{issue.sequence_id} {remove_unwanted_characters(issue.name)}"
|
||||
context = {
|
||||
"data": template_data,
|
||||
"summary": summary,
|
||||
"actors_involved": len(set(actors_involved)),
|
||||
"issue": {
|
||||
"issue_identifier": issue_identifier,
|
||||
"name": issue.name,
|
||||
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/browse/{issue_identifier}",
|
||||
},
|
||||
"receiver": {"email": receiver.email},
|
||||
# Send the mail
|
||||
subject = f"{issue.project.identifier}-{issue.sequence_id} {remove_unwanted_characters(issue.name)}"
|
||||
context = {
|
||||
"data": template_data,
|
||||
"summary": summary,
|
||||
"actors_involved": len(set(actors_involved)),
|
||||
"issue": {
|
||||
"issue_identifier": issue_identifier,
|
||||
"name": issue.name,
|
||||
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/browse/{issue_identifier}",
|
||||
"project_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/", # noqa: E501
|
||||
"workspace": str(issue.project.workspace.slug),
|
||||
"project": str(issue.project.name),
|
||||
"user_preference": f"{base_api}/{str(issue.project.workspace.slug)}/settings/account/notifications/",
|
||||
"comments": comments,
|
||||
"entity_type": entity_type,
|
||||
}
|
||||
|
||||
html_content = render_to_string(template_name, context)
|
||||
text_content = strip_tags(html_content)
|
||||
},
|
||||
"receiver": {"email": receiver.email},
|
||||
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/browse/{issue_identifier}",
|
||||
"project_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/", # noqa: E501
|
||||
"workspace": str(issue.project.workspace.slug),
|
||||
"project": str(issue.project.name),
|
||||
"user_preference": f"{base_api}/{str(issue.project.workspace.slug)}/settings/account/notifications/",
|
||||
"comments": comments,
|
||||
"entity_type": entity_type,
|
||||
}
|
||||
html_content = render_to_string("emails/notifications/issue-updates.html", context)
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
try:
|
||||
connection = get_connection(
|
||||
host=EMAIL_HOST,
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
use_ssl=EMAIL_USE_SSL == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=EMAIL_FROM,
|
||||
to=[receiver.email],
|
||||
connection=connection,
|
||||
)
|
||||
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
try:
|
||||
send_email(subject, text_content, receiver, html_content, email_notification_ids)
|
||||
|
||||
logging.getLogger("plane.worker").info("Email Sent Successfully")
|
||||
|
||||
# Update the logs
|
||||
EmailNotificationLog.objects.filter(pk__in=email_notification_ids).update(sent_at=timezone.now())
|
||||
|
||||
# release the lock
|
||||
release_lock(lock_id=lock_id)
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
# release the lock
|
||||
release_lock(lock_id=lock_id)
|
||||
return
|
||||
else:
|
||||
logging.getLogger("plane.worker").info("Duplicate email received skipping")
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return
|
||||
except (Issue.DoesNotExist, User.DoesNotExist):
|
||||
release_lock(lock_id=lock_id)
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
release_lock(lock_id=lock_id)
|
||||
return
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_workspace_level_email_notification(
|
||||
entity_id, entity_name, notification_data, receiver_id, email_notification_ids
|
||||
):
|
||||
try:
|
||||
base_api = settings.WEB_URL
|
||||
|
||||
data = create_payload(notification_data=notification_data, entity_name=entity_name)
|
||||
|
||||
ENTITY_MAPPER = {EntityName.TEAMSPACE.value: Teamspace, EntityName.INITIATIVE.value: Initiative}
|
||||
|
||||
entity = ENTITY_MAPPER.get(entity_name).objects.get(pk=entity_id)
|
||||
|
||||
actors_involved = []
|
||||
template_data = []
|
||||
|
||||
for actor_id, changes in data.items():
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
actors_involved.append(actor_id)
|
||||
activity_time = changes.pop("activity_time")
|
||||
# Parse the input string into a datetime object
|
||||
formatted_time = datetime.strptime(activity_time, "%Y-%m-%d %H:%M:%S").strftime("%H:%M %p")
|
||||
|
||||
if changes:
|
||||
template_data.append(
|
||||
{
|
||||
"actor_detail": {
|
||||
"avatar_url": f"{base_api}{actor.avatar_url}",
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
"changes": changes,
|
||||
"entity_name": entity.name,
|
||||
"activity_time": str(formatted_time),
|
||||
}
|
||||
)
|
||||
|
||||
receiver = User.objects.get(pk=receiver_id)
|
||||
context = {
|
||||
"data": template_data,
|
||||
"summary": f"Updates were to the {entity_name} by",
|
||||
"actors_involved": len(set(actors_involved)),
|
||||
entity_name: {
|
||||
f"{entity}_name": entity.name,
|
||||
# f"{entity}_url": f"{base_api}/{str(entity.workspace.slug)}/browse/{}",
|
||||
},
|
||||
"receiver": {"email": receiver.email},
|
||||
"workspace": str(entity.workspace.slug),
|
||||
}
|
||||
|
||||
html_content = render_to_string("emails/notifications/issue-updates.html", context)
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
try:
|
||||
# Get email configurations
|
||||
send_email(entity.name, text_content, receiver, html_content, email_notification_ids)
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return
|
||||
|
||||
|
||||
def send_email(subject, text_content, receiver, html_content, email_notification_ids):
|
||||
(
|
||||
EMAIL_HOST,
|
||||
EMAIL_HOST_USER,
|
||||
EMAIL_HOST_PASSWORD,
|
||||
EMAIL_PORT,
|
||||
EMAIL_USE_TLS,
|
||||
EMAIL_USE_SSL,
|
||||
EMAIL_FROM,
|
||||
) = get_email_configuration()
|
||||
|
||||
connection = get_connection(
|
||||
host=EMAIL_HOST,
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
use_ssl=EMAIL_USE_SSL == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=EMAIL_FROM,
|
||||
to=[receiver.email],
|
||||
connection=connection,
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
logging.getLogger("plane.worker").info("Email Sent Successfully")
|
||||
|
||||
# Update the logs
|
||||
EmailNotificationLog.objects.filter(pk__in=email_notification_ids).update(sent_at=timezone.now())
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Python imports
|
||||
from enum import Enum
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
@@ -6,6 +9,13 @@ from django.db import models
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
class EntityName(Enum):
|
||||
EPIC = "epic"
|
||||
ISSUE = "issue"
|
||||
INITIATIVE = "initiative"
|
||||
TEAMSPACE = "teamspace"
|
||||
|
||||
|
||||
class Notification(BaseModel):
|
||||
workspace = models.ForeignKey("db.Workspace", related_name="notifications", on_delete=models.CASCADE)
|
||||
project = models.ForeignKey("db.Project", related_name="notifications", on_delete=models.CASCADE, null=True)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Python imports
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# Third Party imports
|
||||
@@ -7,9 +8,10 @@ from celery import shared_task
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Project, Workspace
|
||||
from plane.db.models import Project, Workspace, Issue
|
||||
from plane.ee.models import (
|
||||
Initiative,
|
||||
InitiativeActivity,
|
||||
@@ -18,10 +20,12 @@ from plane.ee.models import (
|
||||
InitiativeCommentReaction,
|
||||
InitiativeLabel,
|
||||
)
|
||||
from plane.db.models import Issue
|
||||
from plane.ee.serializers import InitiativeActivitySerializer
|
||||
from plane.ee.bgtasks.notification_task import workspace_notifications
|
||||
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.db.models.notification import EntityName
|
||||
|
||||
|
||||
# Track Changes in name
|
||||
@@ -124,12 +128,20 @@ def track_end_date(
|
||||
epoch,
|
||||
):
|
||||
if current_instance.get("end_date") != requested_data.get("end_date"):
|
||||
# Only save the date similar to new_value
|
||||
old_value = isoformat = (
|
||||
datetime.fromisoformat(current_instance.get("end_date"))
|
||||
if current_instance.get("end_date") is not None
|
||||
else ""
|
||||
)
|
||||
old_value = isoformat.date()
|
||||
|
||||
initiative_activities.append(
|
||||
InitiativeActivity(
|
||||
initiative_id=initiative_id,
|
||||
actor_id=actor_id,
|
||||
verb="updated",
|
||||
old_value=(current_instance.get("end_date") if current_instance.get("end_date") is not None else ""),
|
||||
old_value=old_value,
|
||||
new_value=(requested_data.get("end_date") if requested_data.get("end_date") is not None else ""),
|
||||
field="end_date",
|
||||
workspace_id=workspace_id,
|
||||
@@ -150,14 +162,20 @@ def track_start_date(
|
||||
epoch,
|
||||
):
|
||||
if current_instance.get("start_date") != requested_data.get("start_date"):
|
||||
# Only save the date similar to new_value
|
||||
old_value = isoformat = (
|
||||
datetime.fromisoformat(current_instance.get("start_date"))
|
||||
if current_instance.get("start_date") is not None
|
||||
else ""
|
||||
)
|
||||
old_value = isoformat.date()
|
||||
|
||||
initiative_activities.append(
|
||||
InitiativeActivity(
|
||||
initiative_id=initiative_id,
|
||||
actor_id=actor_id,
|
||||
verb="updated",
|
||||
old_value=(
|
||||
current_instance.get("start_date") if current_instance.get("start_date") is not None else ""
|
||||
),
|
||||
old_value=old_value,
|
||||
new_value=(requested_data.get("start_date") if requested_data.get("start_date") is not None else ""),
|
||||
field="start_date",
|
||||
workspace_id=workspace_id,
|
||||
@@ -863,9 +881,20 @@ def initiative_activity(
|
||||
epoch=epoch,
|
||||
)
|
||||
|
||||
# Save all the values to database
|
||||
_ = InitiativeActivity.objects.bulk_create(initiative_activities)
|
||||
# Bulk create the initiative activities
|
||||
initiative_activities_created = InitiativeActivity.objects.bulk_create(initiative_activities)
|
||||
|
||||
if notification:
|
||||
workspace_notifications(
|
||||
workspace_id=workspace_id,
|
||||
entity_name=EntityName.INITIATIVE.value,
|
||||
entity_identifier=initiative_id,
|
||||
actor_id=actor_id,
|
||||
activities_created=json.dumps(
|
||||
InitiativeActivitySerializer(initiative_activities_created, many=True).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
|
||||
# Module imports
|
||||
from plane.ee.models import Initiative, Teamspace
|
||||
from plane.db.models import Notification, EmailNotificationLog, UserNotificationPreference
|
||||
from plane.db.models.notification import EntityName
|
||||
|
||||
|
||||
def workspace_notifications(workspace_id, entity_name, entity_identifier, actor_id, activities_created):
|
||||
"""
|
||||
Create notifications for workspace level features, such as initiative, teamspaces, etc.
|
||||
"""
|
||||
try:
|
||||
# Check for user notification preference
|
||||
|
||||
# Parse the activities if it's a string
|
||||
activities = json.loads(activities_created) if activities_created is not None else []
|
||||
|
||||
if entity_name == EntityName.TEAMSPACE.value:
|
||||
entity = Teamspace.objects.prefetch_related("members").get(id=entity_identifier, workspace_id=workspace_id)
|
||||
receivers = [entity_member.member for entity_member in entity.members.all()]
|
||||
|
||||
elif entity_name == EntityName.INITIATIVE.value:
|
||||
entity = Initiative.objects.get(id=entity_identifier, workspace_id=workspace_id)
|
||||
receivers = [entity.lead] if entity.lead else []
|
||||
|
||||
bulk_notifications = []
|
||||
bulk_email_notifications = []
|
||||
|
||||
for receiver in receivers:
|
||||
for activity in activities:
|
||||
# User notification preference check
|
||||
preference = UserNotificationPreference.objects.get(user_id=receiver)
|
||||
|
||||
# Do not send notification for descriiption changes
|
||||
if activity.get("field") == "description":
|
||||
continue
|
||||
|
||||
send_email = False
|
||||
|
||||
if activity.get("field") == "comment" and preference.comment:
|
||||
send_email = True
|
||||
elif activity.get("field") == "page":
|
||||
send_email = True
|
||||
elif preference.property_change:
|
||||
send_email = True
|
||||
else:
|
||||
send_email = False
|
||||
|
||||
if send_email:
|
||||
bulk_notifications.append(
|
||||
Notification(
|
||||
workspace_id=workspace_id,
|
||||
entity_identifier=entity_identifier,
|
||||
entity_name=entity_name,
|
||||
title=activity.get("comment"),
|
||||
triggered_by_id=actor_id,
|
||||
receiver_id=receiver.id,
|
||||
data=build_data(entity, entity_name, activity),
|
||||
)
|
||||
)
|
||||
|
||||
bulk_email_notifications.append(
|
||||
EmailNotificationLog(
|
||||
triggered_by_id=actor_id,
|
||||
receiver_id=receiver.id,
|
||||
entity_identifier=entity_identifier,
|
||||
entity_name=entity_name,
|
||||
data=build_data(entity, entity_name, activity),
|
||||
)
|
||||
)
|
||||
|
||||
Notification.objects.bulk_create(bulk_notifications, batch_size=100, ignore_conflicts=True)
|
||||
EmailNotificationLog.objects.bulk_create(bulk_email_notifications, batch_size=100, ignore_conflicts=True)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return
|
||||
|
||||
|
||||
def build_data(entity, entity_name, activity):
|
||||
return {
|
||||
entity_name: {"id": str(entity.id), "name": entity.name, "logo_props": entity.logo_props},
|
||||
f"{entity_name}_activity": {
|
||||
"id": str(activity.get("id", "")),
|
||||
"verb": str(activity.get("verb", "")),
|
||||
"field": str(activity.get("field", "")),
|
||||
"actor": str(activity.get("actor", "")),
|
||||
"new_value": str(activity.get("new_value", "")),
|
||||
"old_value": str(activity.get("old_value", "")),
|
||||
"old_identifier": (str(activity.get("old_identifier")) if activity.get("old_identifier") else None),
|
||||
"new_identifier": (str(activity.get("new_identifier")) if activity.get("new_identifier") else None),
|
||||
"activity_time": activity.get("created_at"),
|
||||
},
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
@@ -16,6 +17,9 @@ from plane.ee.models import (
|
||||
)
|
||||
from plane.db.models import Label, Project, User, Workspace
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.ee.bgtasks.notification_task import workspace_notifications
|
||||
from plane.ee.serializers import TeamspaceActivitySerializer
|
||||
from plane.db.models.notification import EntityName
|
||||
|
||||
|
||||
# Track Changes in name
|
||||
@@ -611,7 +615,16 @@ def delete_view_activity(
|
||||
|
||||
|
||||
@shared_task
|
||||
def team_space_activity(type, requested_data, team_space_id, actor_id, slug, current_instance, epoch):
|
||||
def team_space_activity(
|
||||
type,
|
||||
requested_data,
|
||||
team_space_id,
|
||||
actor_id,
|
||||
slug,
|
||||
current_instance,
|
||||
epoch,
|
||||
notification=False,
|
||||
):
|
||||
try:
|
||||
# Get workspace
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
@@ -654,7 +667,21 @@ def team_space_activity(type, requested_data, team_space_id, actor_id, slug, cur
|
||||
)
|
||||
|
||||
# Save all the values to database
|
||||
_ = TeamspaceActivity.objects.bulk_create(team_space_activities, batch_size=100, ignore_conflicts=True)
|
||||
teamspace_activities_created = TeamspaceActivity.objects.bulk_create(
|
||||
team_space_activities, batch_size=100, ignore_conflicts=True
|
||||
)
|
||||
|
||||
if notification:
|
||||
workspace_notifications(
|
||||
workspace_id=workspace_id,
|
||||
entity_name=EntityName.TEAMSPACE.value,
|
||||
entity_identifier=team_space.id,
|
||||
actor_id=actor_id,
|
||||
activities_created=json.dumps(
|
||||
TeamspaceActivitySerializer(teamspace_activities_created, many=True).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -44,9 +44,7 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
Teamspace.objects.annotate(
|
||||
project_ids=Coalesce(
|
||||
Subquery(
|
||||
TeamspaceProject.objects.filter(
|
||||
team_space=OuterRef("pk"), workspace__slug=slug
|
||||
)
|
||||
TeamspaceProject.objects.filter(team_space=OuterRef("pk"), workspace__slug=slug)
|
||||
.values("team_space")
|
||||
.annotate(project_ids=ArrayAgg("project_id", distinct=True))
|
||||
.values("project_ids")
|
||||
@@ -56,9 +54,7 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
)
|
||||
.annotate(
|
||||
is_member=Exists(
|
||||
TeamspaceMember.objects.filter(
|
||||
team_space=OuterRef("pk"), member_id=self.request.user.id
|
||||
)
|
||||
TeamspaceMember.objects.filter(team_space=OuterRef("pk"), member_id=self.request.user.id)
|
||||
)
|
||||
)
|
||||
.get(workspace__slug=slug, pk=team_space_id, is_member=True)
|
||||
@@ -77,9 +73,7 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
Subquery(
|
||||
TeamspaceProject.objects.filter(
|
||||
team_space=OuterRef("pk"), workspace__slug=slug
|
||||
)
|
||||
TeamspaceProject.objects.filter(team_space=OuterRef("pk"), workspace__slug=slug)
|
||||
.values("team_space")
|
||||
.annotate(project_ids=ArrayAgg("project_id", distinct=True))
|
||||
.values("project_ids")
|
||||
@@ -90,9 +84,7 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def get_add_remove_team_space_projects(
|
||||
self, slug, team_space_id, request_project_ids
|
||||
):
|
||||
def get_add_remove_team_space_projects(self, slug, team_space_id, request_project_ids):
|
||||
# Update team space projects
|
||||
existing_project_ids = [
|
||||
str(project_id)
|
||||
@@ -108,9 +100,7 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
|
||||
return project_ids_to_be_added, project_ids_to_be_removed
|
||||
|
||||
@allow_permission(
|
||||
level="WORKSPACE", allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]
|
||||
)
|
||||
@allow_permission(level="WORKSPACE", allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@check_feature_flag(FeatureFlag.TEAMSPACES)
|
||||
def get(self, request, slug, team_space_id=None):
|
||||
# Get team space by pk
|
||||
@@ -146,9 +136,7 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
)
|
||||
|
||||
# Add the lead to the team space if provided and not the creating user
|
||||
if request.data.get("lead_id") and str(
|
||||
request.data.get("lead_id")
|
||||
) != str(request.user.id):
|
||||
if request.data.get("lead_id") and str(request.data.get("lead_id")) != str(request.user.id):
|
||||
TeamspaceMember.objects.create(
|
||||
team_space=team_space,
|
||||
workspace=workspace,
|
||||
@@ -173,9 +161,7 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Workspace.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Workspace not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Workspace not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -188,14 +174,10 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
# Get workspace
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
current_instance = json.dumps(
|
||||
TeamspaceSerializer(team_space).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(TeamspaceSerializer(team_space).data, cls=DjangoJSONEncoder)
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
|
||||
serializer = TeamspaceSerializer(
|
||||
team_space, data=request.data, partial=True
|
||||
)
|
||||
serializer = TeamspaceSerializer(team_space, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
team_space = serializer.save()
|
||||
|
||||
@@ -204,14 +186,9 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
# Add the lead to the team space if provided and not the creating user
|
||||
if (
|
||||
lead_id
|
||||
and TeamspaceMember.objects.filter(
|
||||
team_space=team_space, member_id=lead_id
|
||||
).exists()
|
||||
is False
|
||||
and TeamspaceMember.objects.filter(team_space=team_space, member_id=lead_id).exists() is False
|
||||
):
|
||||
TeamspaceMember.objects.create(
|
||||
team_space=team_space, workspace=workspace, member_id=lead_id
|
||||
)
|
||||
TeamspaceMember.objects.create(team_space=team_space, workspace=workspace, member_id=lead_id)
|
||||
|
||||
# Get the list of project ids for request if it exists
|
||||
if "project_ids" in request.data:
|
||||
@@ -219,10 +196,8 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
project_ids = request.data.pop("project_ids", [])
|
||||
|
||||
# Update team space projects
|
||||
project_ids_to_be_added, project_ids_to_be_removed = (
|
||||
self.get_add_remove_team_space_projects(
|
||||
slug, team_space.pk, project_ids
|
||||
)
|
||||
project_ids_to_be_added, project_ids_to_be_removed = self.get_add_remove_team_space_projects(
|
||||
slug, team_space.pk, project_ids
|
||||
)
|
||||
|
||||
# Create team space projects
|
||||
@@ -253,6 +228,7 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
requested_data=requested_data,
|
||||
actor_id=str(request.user.id),
|
||||
team_space_id=str(team_space_id),
|
||||
notification=True,
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
@@ -263,9 +239,7 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Teamspace.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Team space not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Team space not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@allow_permission(level="WORKSPACE", allowed_roles=[ROLE.ADMIN])
|
||||
@check_feature_flag(FeatureFlag.TEAMSPACES)
|
||||
@@ -275,9 +249,7 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
"""
|
||||
try:
|
||||
# The current deleting user should be part of the team space
|
||||
if not TeamspaceMember.objects.filter(
|
||||
team_space_id=team_space_id, member_id=request.user
|
||||
).exists():
|
||||
if not TeamspaceMember.objects.filter(team_space_id=team_space_id, member_id=request.user).exists():
|
||||
return Response(
|
||||
{"error": "You are not part of the team space"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
@@ -300,6 +272,4 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint):
|
||||
team_space.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except Teamspace.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Team space not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Team space not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@@ -31,29 +31,21 @@ class TeamspaceMembersEndpoint(TeamspaceBaseEndpoint):
|
||||
def get(self, request, slug, team_space_id=None, pk=None):
|
||||
# Get team space member by pk
|
||||
if pk:
|
||||
team_space_member = TeamspaceMember.objects.get(
|
||||
workspace__slug=slug, team_space_id=team_space_id, pk=pk
|
||||
)
|
||||
team_space_member = TeamspaceMember.objects.get(workspace__slug=slug, team_space_id=team_space_id, pk=pk)
|
||||
serializer = TeamspaceMemberSerializer(team_space_member)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
# Get all team space members for a team space
|
||||
if team_space_id:
|
||||
team_space_members = TeamspaceMember.objects.filter(
|
||||
workspace__slug=slug, team_space_id=team_space_id
|
||||
)
|
||||
team_space_members = TeamspaceMember.objects.filter(workspace__slug=slug, team_space_id=team_space_id)
|
||||
serializer = TeamspaceMemberSerializer(team_space_members, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
# Get all team spaces for the use
|
||||
team_spaces = Teamspace.objects.filter(
|
||||
workspace__slug=slug, members__member_id=self.request.user.id
|
||||
)
|
||||
team_spaces = Teamspace.objects.filter(workspace__slug=slug, members__member_id=self.request.user.id)
|
||||
|
||||
# Get all team space members for users team spaces
|
||||
workspace_team_space_members = TeamspaceMember.objects.filter(
|
||||
workspace__slug=slug, team_space__in=team_spaces
|
||||
)
|
||||
workspace_team_space_members = TeamspaceMember.objects.filter(workspace__slug=slug, team_space__in=team_spaces)
|
||||
serializer = TeamspaceMemberSerializer(workspace_team_space_members, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -73,15 +65,11 @@ class TeamspaceMembersEndpoint(TeamspaceBaseEndpoint):
|
||||
).values_list("member_id", flat=True)
|
||||
)
|
||||
|
||||
current_instance = json.dumps(
|
||||
{"member_ids": current_members}, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps({"member_ids": current_members}, cls=DjangoJSONEncoder)
|
||||
|
||||
# Get the list of team space members
|
||||
team_space_members = (
|
||||
TeamspaceMember.objects.filter(
|
||||
workspace__slug=slug, member_id__in=member_ids
|
||||
)
|
||||
TeamspaceMember.objects.filter(workspace__slug=slug, member_id__in=member_ids)
|
||||
.values("member_id", "sort_order")
|
||||
.order_by("sort_order")
|
||||
)
|
||||
@@ -122,13 +110,9 @@ class TeamspaceMembersEndpoint(TeamspaceBaseEndpoint):
|
||||
)
|
||||
|
||||
# Create team space members
|
||||
TeamspaceMember.objects.bulk_create(
|
||||
bulk_team_space_members, ignore_conflicts=True, batch_size=100
|
||||
)
|
||||
TeamspaceMember.objects.bulk_create(bulk_team_space_members, ignore_conflicts=True, batch_size=100)
|
||||
|
||||
team_space_members = TeamspaceMember.objects.filter(
|
||||
workspace__slug=slug, team_space_id=team_space_id
|
||||
)
|
||||
team_space_members = TeamspaceMember.objects.filter(workspace__slug=slug, team_space_id=team_space_id)
|
||||
|
||||
# Create activity
|
||||
team_space_activity.delay(
|
||||
@@ -137,6 +121,7 @@ class TeamspaceMembersEndpoint(TeamspaceBaseEndpoint):
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
team_space_id=str(team_space.id),
|
||||
notification=True,
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
@@ -147,9 +132,7 @@ class TeamspaceMembersEndpoint(TeamspaceBaseEndpoint):
|
||||
@check_feature_flag(FeatureFlag.TEAMSPACES)
|
||||
def delete(self, request, slug, team_space_id, pk):
|
||||
# Get team space member
|
||||
team_space_member = TeamspaceMember.objects.get(
|
||||
workspace__slug=slug, team_space_id=team_space_id, pk=pk
|
||||
)
|
||||
team_space_member = TeamspaceMember.objects.get(workspace__slug=slug, team_space_id=team_space_id, pk=pk)
|
||||
|
||||
# Get team space
|
||||
team_space = Teamspace.objects.get(workspace__slug=slug, pk=team_space_id)
|
||||
@@ -162,16 +145,9 @@ class TeamspaceMembersEndpoint(TeamspaceBaseEndpoint):
|
||||
)
|
||||
|
||||
# Check if the team space has only one member
|
||||
if (
|
||||
TeamspaceMember.objects.filter(
|
||||
workspace__slug=slug, team_space_id=team_space_id
|
||||
).count()
|
||||
== 1
|
||||
):
|
||||
if TeamspaceMember.objects.filter(workspace__slug=slug, team_space_id=team_space_id).count() == 1:
|
||||
return Response(
|
||||
{
|
||||
"error": "Cannot delete the last member from team. Delete the team instead."
|
||||
},
|
||||
{"error": "Cannot delete the last member from team. Delete the team instead."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -180,20 +156,16 @@ class TeamspaceMembersEndpoint(TeamspaceBaseEndpoint):
|
||||
workspace__slug=slug, team_space_id=team_space_id
|
||||
).values_list("member_id", flat=True)
|
||||
|
||||
current_instance = json.dumps(
|
||||
{"member_ids": list(current_team_space_members)}, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps({"member_ids": list(current_team_space_members)}, cls=DjangoJSONEncoder)
|
||||
|
||||
# Get the list of team space members
|
||||
requested_team_space_members = list(
|
||||
TeamspaceMember.objects.filter(
|
||||
~Q(pk=pk), workspace__slug=slug, team_space_id=team_space_id
|
||||
).values_list("member_id", flat=True)
|
||||
TeamspaceMember.objects.filter(~Q(pk=pk), workspace__slug=slug, team_space_id=team_space_id).values_list(
|
||||
"member_id", flat=True
|
||||
)
|
||||
)
|
||||
|
||||
requested_data = json.dumps(
|
||||
{"member_ids": requested_team_space_members}, cls=DjangoJSONEncoder
|
||||
)
|
||||
requested_data = json.dumps({"member_ids": requested_team_space_members}, cls=DjangoJSONEncoder)
|
||||
|
||||
# Create activity
|
||||
team_space_activity.delay(
|
||||
@@ -202,6 +174,7 @@ class TeamspaceMembersEndpoint(TeamspaceBaseEndpoint):
|
||||
requested_data=requested_data,
|
||||
actor_id=str(request.user.id),
|
||||
team_space_id=str(team_space.id),
|
||||
notification=True,
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Updates on initiative</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||
<style type="text/css" emogrify="no">
|
||||
html {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
ol,
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
h-full {
|
||||
height: 100%;
|
||||
}
|
||||
a:hover {
|
||||
color: #3358d4 !important;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
*[class="gmail-fix"] {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css" emogrify="no">
|
||||
@media (max-width: 600px) {
|
||||
.gmx-killpill {
|
||||
content: " \03D1";
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #F4F5F5; margin: 20px;">
|
||||
<div style="width: 600px; table-layout: fixed; height: 100%; margin-left: auto; margin-right: auto;">
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<table style="width: 600px;" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="margin: 20px 0 20px 30px;">
|
||||
<img src="https://media.docs.plane.so/logo/new-logo-white.png" width="150" border="0" alt="Plane Logo" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div style="color: #1f2d5c; padding: 30px; border-radius: 4px; background-color: #fcfcfd; max-width: 100%;">
|
||||
<div>
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td>
|
||||
<p style="font-size: 1rem; color: #1f2d5c; font-weight: 600; font-family: 'Inter', sans-serif;">
|
||||
{% if initiative.initiative_name %}{{ initiative.initiative_name }}{% elif initiative.name %}{{ initiative.name }}{% else %}Initiative{% endif %} updates
|
||||
</p>
|
||||
<p style="font-size: 1rem; font-weight: 500; color: #1f2d5c; line-height: 28px; font-family: 'Inter', sans-serif;">
|
||||
{{workspace}}/<a style="color: #1f2d5c; text-decoration: none;" target="_blank" href="{{entity_url}}">{% if initiative.initiative_name %}{{ initiative.initiative_name }}{% elif initiative.name %}{{ initiative.name }}{% else %}Initiative{% endif %}</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr style="background-color: #f0f0f3; height: 1px; border: 0; margin-top: 15px; margin-bottom: 15px;" />
|
||||
{% if actors_involved == 1 %}
|
||||
<p style="font-size: 1rem; color: #1f2d5c; line-height: 28px; font-family: 'Inter', sans-serif;">
|
||||
Changes were made to the initiative by <span style="font-size: 1rem; font-weight: 700; line-height: 28px;">{% if data|length > 0 %} {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name}} {% else %} {{ comments.0.actor_detail.first_name}} {{comments.0.actor_detail.last_name}} {% endif %}</span>.
|
||||
</p>
|
||||
{% else %}
|
||||
<p style="font-size: 1rem; color: #1f2d5c; line-height: 28px; font-family: 'Inter', sans-serif;">
|
||||
Changes were made to the initiative by <span style="font-size: 1rem; font-weight: 700; line-height: 28px;">{% if data|length > 0 %} {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name}} {% else %} {{ comments.0.actor_detail.first_name}} {{comments.0.actor_detail.last_name}} {% endif %}</span>and others.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% for update in data %}
|
||||
{% if update.changes.name %}
|
||||
<!-- Initiative title updated -->
|
||||
<p style="font-size: 1rem; line-height: 28px; color: #1f2d5c; font-family: 'Inter', sans-serif;">
|
||||
The initiative title has been updated to {% if initiative.initiative_name %}{{ initiative.initiative_name }}{% elif initiative.name %}{{ initiative.name }}{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Outer update Box start -->
|
||||
{% if data %}
|
||||
{% for update in data %}
|
||||
<div style="background-color: #f7f9ff; border-radius: 8px; border-style: solid; border-width: 1px; border-color: #c1d0ff; padding: 20px; margin-top: 15px; max-width: 100%;">
|
||||
<!-- Block Heading -->
|
||||
<div style="padding-bottom: 20px">
|
||||
<p style="font-size: 0.8rem; font-weight: 600; color: #121a26; font-family: 'Inter', sans-serif;">Updates</p>
|
||||
</div>
|
||||
<!-- Property Updates -->
|
||||
<div style="background-color: white; max-width: 100%; overflow: hidden; overflow-wrap: break-word; word-wrap: break-word; padding-left: 15px; padding-bottom: 15px; border-radius: 8px;">
|
||||
<!-- action performer -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 100%">
|
||||
<tr style="border-radius: 8px; margin-top: 20px">
|
||||
<td style="width: 30px">
|
||||
{% if update.actor_detail.avatar_url %}
|
||||
<img src="{{ update.actor_detail.avatar_url }}" width="25" height="25" border="0" />
|
||||
{% else %}
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="width: 25px; height: 25px; background-color: #4f3422; border-radius: 50%; text-align: center;">
|
||||
<span style="color: white; font-weight: 500; font-size: 13px; line-height: 25px; display: inline-block;">{{ update.actor_detail.first_name.0 }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="padding-top: 20px; padding-bottom: 20px">
|
||||
<p style="font-weight: 500; font-size: 0.8rem; color: #1c2024; width: fit-content; margin-left: 5px; font-family: 'Inter', sans-serif;">{{ update.actor_detail.first_name }} {{ update.actor_detail.last_name }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-weight: 500; font-size: 0.6rem; color: #80838d; margin-left: 10px; font-family: 'Inter', sans-serif;">{{ update.activity_time }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% if update.changes.status %}
|
||||
<!-- Status changed -->
|
||||
<table role="presentation" style="padding-bottom: 15px; max-width: 100%">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/state.png" width="12" height="12" border="0" style="display: block;" />
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #525252; font-family: 'Inter', sans-serif;">Status:</p>
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646C; font-family: 'Inter', sans-serif;">{{ update.changes.status.old_value.0|default:"—" }}</p>
|
||||
</td>
|
||||
<td style="padding-left: 10px; padding-right: 10px;">
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.png" width="16" height="16" border="0" style="display: block;" />
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646c; font-family: 'Inter', sans-serif;">{{update.changes.status.new_value|last|default:"—" }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if update.changes.start_date %}
|
||||
<!-- Start date changed -->
|
||||
<table role="presentation" style="max-width: 100%; padding-bottom: 15px">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/due-date.png" width="12" height="12" border="0" style="display: block" />
|
||||
</td>
|
||||
<td style="padding: 0px; text-align: center">
|
||||
<p style="padding: 0px; margin: 0px; font-size: 0.8rem; font-weight: 500; color: #525252; font-family: 'Inter', sans-serif;">Start Date:</p>
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646C; margin-left: 5px; padding: 0px; font-family: 'Inter', sans-serif;">{{ update.changes.start_date.old_value.0|default:"—" }}</p>
|
||||
</td>
|
||||
<td style="padding-left: 10px; padding-right: 10px;">
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.png" width="16" height="16" border="0" style="display: block;" />
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646c; padding: 0px; font-family: 'Inter', sans-serif;">{{update.changes.start_date.new_value|last|default:"—" }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if update.changes.end_date %}
|
||||
<!-- End date changed -->
|
||||
<table role="presentation" style="max-width: 100%; padding-bottom: 15px">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/due-date.png" width="12" height="12" border="0" style="display: block" />
|
||||
</td>
|
||||
<td style="padding: 0px; text-align: center">
|
||||
<p style="padding: 0px; margin: 0px; font-size: 0.8rem; font-weight: 500; color: #525252; font-family: 'Inter', sans-serif;">End Date:</p>
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646C; margin-left: 5px; padding: 0px; font-family: 'Inter', sans-serif;">{{ update.changes.end_date.old_value.0|default:"—" }}</p>
|
||||
</td>
|
||||
<td style="padding-left: 10px; padding-right: 10px;">
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.png" width="16" height="16" border="0" style="display: block;" />
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646c; padding: 0px; font-family: 'Inter', sans-serif;">{{update.changes.end_date.new_value|last|default:"—" }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if update.changes.lead %}
|
||||
<!-- Lead changed -->
|
||||
<table role="presentation" style="padding-bottom: 15px; max-width: 100%">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/assignee.png" width="12" height="12" border="0" style="display: block" />
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #525252; font-family: 'Inter', sans-serif;">Lead:</p>
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646C; font-family: 'Inter', sans-serif;">{{ update.changes.lead.old_value.0|default:"—" }}</p>
|
||||
</td>
|
||||
<td style="padding-left: 10px; padding-right: 10px;">
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.png" width="16" height="16" border="0" style="display: block;" />
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646c; font-family: 'Inter', sans-serif;">{{update.changes.lead.new_value|last|default:"—" }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if update.changes.labels %}
|
||||
<!-- Labels -->
|
||||
<table role="presentation" style="padding-bottom: 15px; max-width: 100%; padding-right: 10px;">
|
||||
<tr>
|
||||
<td valign="top" style="white-space: nowrap; padding: 0px;">
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/labels.png" width="12" height="12" border="0" style="display: inline-block;" />
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #525252; padding-right: 5px; font-family: 'Inter', sans-serif;">Labels:</span>
|
||||
</td>
|
||||
<td style="overflow-wrap: anywhere;word-break: break-all; padding: 0px;">
|
||||
{% if update.changes.labels.new_value.0 %}
|
||||
<span style="font-size: 0.8rem; line-height: 1px; color: #0d74ce; background-color: #e6f4fe; margin-right: 5px; padding-left: 4px; padding-right: 4px; padding-bottom: 2px; padding-top: 2px; font-weight: 500; border-radius: 2px; max-lines: 1; font-family: 'Inter', sans-serif;">{{update.changes.labels.new_value.0}}</span>
|
||||
{% endif %}
|
||||
{% if update.changes.labels.new_value.1 %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #0d74ce; margin-left: 2px; margin-right: 5px; white-space: nowrap; font-family: 'Inter', sans-serif;">+{{ update.changes.labels.new_value|length|add:"-1"}} more</span>
|
||||
{% endif %}
|
||||
{% if update.changes.labels.old_value.0 %}
|
||||
<span style="font-size: 0.8rem; padding-left: 4px; padding-right: 4px; padding-bottom: 2px; padding-top: 2px; font-weight: 500; text-decoration: line-through; color: #641723; background-color: #feebec; margin-right: 5px; border-radius: 2px; font-family: 'Inter', sans-serif;">{{update.changes.labels.old_value.0}}</span>
|
||||
{% endif %}
|
||||
{% if update.changes.labels.old_value.1 %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-left: 2px; white-space: nowrap; font-family: 'Inter', sans-serif;">+{{ update.changes.labels.old_value|length|add:"-1"}} more</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if update.changes.projects %}
|
||||
<!-- Projects changed -->
|
||||
<table role="presentation" style="max-width: 100%; padding-bottom: 15px">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/projects.png" width="12" height="12" border="0" style="display: inline-block" />
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #525252; font-family: 'Inter', sans-serif;">Projects:</span>
|
||||
</td>
|
||||
{% if update.changes.projects.new_value.0 %}
|
||||
<td style="padding-left: 5px;overflow-wrap: break-word;">
|
||||
{% for project in update.changes.projects.new_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">{{ project }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.projects.new_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ update.changes.projects.new_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.projects.old_value.0 %}
|
||||
<td style="padding-left: 8px;">
|
||||
{% for project in update.changes.projects.old_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; text-decoration: line-through; font-family: 'Inter', sans-serif;">{{ project }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.projects.old_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ update.changes.projects.old_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if update.changes.epics %}
|
||||
<!-- Epics changed -->
|
||||
<table role="presentation" style="max-width: 100%; padding-bottom: 15px">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/epic.png" width="12" height="12" border="0" style="display: inline-block;" />
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #525252; margin-right: 5px; font-family: 'Inter', sans-serif;">Epics:</span>
|
||||
</td>
|
||||
{% if update.changes.epics.new_value.0 %}
|
||||
<td style="padding-left: 5px;overflow-wrap: break-word;">
|
||||
{% for epic in update.changes.epics.new_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">{{ epic }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.epics.new_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ update.changes.epics.new_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.epics.old_value.0 %}
|
||||
<td style="padding-left: 8px;">
|
||||
{% for epic in update.changes.epics.old_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; text-decoration: line-through; font-family: 'Inter', sans-serif;">{{ epic }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.epics.old_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ update.changes.epics.old_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if update.changes.link %}
|
||||
<!-- Link Added -->
|
||||
<table role="presentation" style="padding-bottom: 15px; max-width: 100%">
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/link.png" width="12" height="12" border="0" style="display: block; margin-top: 3px;" />
|
||||
</td>
|
||||
<td valign="top">
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #525252; margin-right: 5px; font-family: 'Inter', sans-serif;">Links:</p>
|
||||
</td>
|
||||
<td>
|
||||
{% for link in update.changes.link.new_value %}
|
||||
<a href="{{link}}" style="font-size: 0.8rem; font-weight: 600; color: #3a5bc7; display: block; padding-bottom: 5px; font-family: 'Inter', sans-serif;">{{ link }}</a>
|
||||
{% endfor %}
|
||||
{% if update.changes.link.old_value|length > 0 %}
|
||||
{% if update.changes.link.old_value.0 != "None" %}
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646c; font-family: 'Inter', sans-serif;">2 Links were removed</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<!-- Outer update Box end -->
|
||||
|
||||
{% if comments.0 %}
|
||||
<!-- Comments outer update Box -->
|
||||
<div style="max-width: 100%; background-color: #f7f9ff; border-radius: 8px; border-style: solid; border-width: 1px; border-color: #c1d0ff; padding: 20px; margin-top: 15px;">
|
||||
<!-- Block Heading -->
|
||||
<p style="font-size: 0.8rem; font-weight: 600; color: #121a26; padding-bottom: 20px; font-family: 'Inter', sans-serif;">Comment</p>
|
||||
<!-- Comments -->
|
||||
{% for comment in comments %}
|
||||
<table cellspacing="0" style="width: 100%; margin-bottom: 15px;">
|
||||
<tr>
|
||||
<td valign="top" style="width: 30px; padding-bottom: 10px;">
|
||||
{% if comment.actor_detail.avatar_url %}
|
||||
<img src="{{ comment.actor_detail.avatar_url }}" width="25" height="25" border="0" style="border-radius: 50%; display: block;" />
|
||||
{% else %}
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="width: 25px; height: 25px; background-color: #4f3422; border-radius: 50%; text-align: center;">
|
||||
<span style="color: white; font-weight: 500; font-size: 13px; line-height: 25px; display: inline-block;">{{ comment.actor_detail.first_name.0 }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td valign="top" style="padding-left: 8px; padding-bottom: 10px;">
|
||||
<p style="font-weight: 500; font-size: 0.8rem; color: #1c2024; margin: 0; padding: 0; font-family: 'Inter', sans-serif;">{{ comment.actor_detail.first_name }} {{ comment.actor_detail.last_name }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% for actor_comment in comment.actor_comments.new_value %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td style="padding-left: 33px; padding-top: 5px;">
|
||||
<div style="padding: 8px 12px; background-color: white; font-size: 0.8rem; color: #1c2024; border-radius: 6px; border: 1px solid #e5e7eb; max-width: 430px; word-wrap: break-word; font-family: 'Inter', sans-serif;">
|
||||
<p style="margin: 0; padding: 0;">{{ actor_comment|safe }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{{ entity_url }}" style="text-decoration: none;">
|
||||
<div style="max-width: min-content; white-space: nowrap; background-color: #3e63dd; padding: 10px 15px; border: 1px solid #2f4ba8; border-radius: 4px; margin-top: 15px; cursor: pointer; font-size: 0.8rem; color: white; font-family: 'Inter', sans-serif;">
|
||||
View initiative
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<table style="width: 100%; padding: 20px; justify-content: center;" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="font-size: 0.8rem; color: #1c2024; font-family: 'Inter', sans-serif;">
|
||||
This email was sent to
|
||||
<a href="mailto:{{receiver.email}}" style="color: #3a5bc7; font-weight: 500; text-decoration: none;">{{ receiver.email }}</a>.
|
||||
If you'd rather not receive this kind of email,
|
||||
<a href="{{ entity_url }}" style="color: #3a5bc7; text-decoration: none;">you can unsubscribe to the initiative</a>
|
||||
or
|
||||
<a href="{{ user_preference }}" style="color: #3a5bc7; text-decoration: none;">manage your email preferences</a>.
|
||||
<!-- Social links -->
|
||||
<div style="margin-top: 60px; float: right;">
|
||||
<a href="https://github.com/makeplane" target="_blank" style="margin-left: 10px; text-decoration: none;">
|
||||
<img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="25" height="25" border="0" alt="GitHub" style="display: inline-block;" />
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/planepowers/" target="_blank" style="margin-left: 10px; text-decoration: none;">
|
||||
<img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="25" height="25" border="0" alt="LinkedIn" style="display: inline-block;" />
|
||||
</a>
|
||||
<a href="https://twitter.com/planepowers" target="_blank" style="margin-left: 10px; text-decoration: none;">
|
||||
<img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="25" height="25" border="0" alt="Twitter" style="display: inline-block;" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,327 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Updates on teamspace</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||
<style type="text/css" emogrify="no">
|
||||
html {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
ol,
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
h-full {
|
||||
height: 100%;
|
||||
}
|
||||
a:hover {
|
||||
color: #3358d4 !important;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
*[class="gmail-fix"] {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css" emogrify="no">
|
||||
@media (max-width: 600px) {
|
||||
.gmx-killpill {
|
||||
content: " \03D1";
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #F4F5F5; margin: 20px;">
|
||||
<div style="width: 600px; table-layout: fixed; height: 100%; margin-left: auto; margin-right: auto;">
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<table style="width: 600px;" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="margin: 20px 0 20px 30px;">
|
||||
<img src="https://media.docs.plane.so/logo/new-logo-white.png" width="150" border="0" alt="Plane Logo" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div style="color: #1f2d5c; padding: 30px; border-radius: 4px; background-color: #fcfcfd; max-width: 100%;">
|
||||
<div>
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td>
|
||||
<p style="font-size: 1rem; color: #1f2d5c; font-weight: 600; font-family: 'Inter', sans-serif;">
|
||||
{% if teamspace.teamspace_name %}{{ teamspace.teamspace_name }}{% elif teamspace.name %}{{ teamspace.name }}{% else %}Teamspace{% endif %} updates
|
||||
</p>
|
||||
<p style="font-size: 1rem; font-weight: 500; color: #1f2d5c; line-height: 28px; font-family: 'Inter', sans-serif;">
|
||||
{{workspace}}/<a style="color: #1f2d5c; text-decoration: none;" target="_blank" href="{{entity_url}}">{% if teamspace.teamspace_name %}{{ teamspace.teamspace_name }}{% elif teamspace.name %}{{ teamspace.name }}{% else %}Teamspace{% endif %}</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr style="background-color: #f0f0f3; height: 1px; border: 0; margin-top: 15px; margin-bottom: 15px;" />
|
||||
{% if actors_involved == 1 %}
|
||||
<p style="font-size: 1rem; color: #1f2d5c; line-height: 28px; font-family: 'Inter', sans-serif;">
|
||||
Changes were made to the teamspace by <span style="font-size: 1rem; font-weight: 700; line-height: 28px;">{% if data|length > 0 %} {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name}} {% else %} {{ comments.0.actor_detail.first_name}} {{comments.0.actor_detail.last_name}} {% endif %}</span>.
|
||||
</p>
|
||||
{% else %}
|
||||
<p style="font-size: 1rem; color: #1f2d5c; line-height: 28px; font-family: 'Inter', sans-serif;">
|
||||
Changes were made to the teamspace by <span style="font-size: 1rem; font-weight: 700; line-height: 28px;">{% if data|length > 0 %} {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name}} {% else %} {{ comments.0.actor_detail.first_name}} {{comments.0.actor_detail.last_name}} {% endif %}</span>and others.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% for update in data %}
|
||||
{% if update.changes.name %}
|
||||
<!-- Teamspace title updated -->
|
||||
<p style="font-size: 1rem; line-height: 28px; color: #1f2d5c; font-family: 'Inter', sans-serif;">
|
||||
The teamspace title has been updated to {% if teamspace.teamspace_name %}{{ teamspace.teamspace_name }}{% elif teamspace.name %}{{ teamspace.name }}{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Outer update Box start -->
|
||||
{% if data %}
|
||||
{% for update in data %}
|
||||
<div style="background-color: #f7f9ff; border-radius: 8px; border-style: solid; border-width: 1px; border-color: #c1d0ff; padding: 20px; margin-top: 15px; max-width: 100%;">
|
||||
<!-- Block Heading -->
|
||||
<div style="padding-bottom: 20px">
|
||||
<p style="font-size: 0.8rem; font-weight: 600; color: #121a26; font-family: 'Inter', sans-serif;">Updates</p>
|
||||
</div>
|
||||
<!-- Property Updates -->
|
||||
<div style="background-color: white; max-width: 100%; overflow: hidden; overflow-wrap: break-word; word-wrap: break-word; padding-left: 15px; padding-bottom: 15px; border-radius: 8px;">
|
||||
<!-- action performer -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 100%">
|
||||
<tr style="border-radius: 8px; margin-top: 20px">
|
||||
<td style="width: 30px">
|
||||
{% if update.actor_detail.avatar_url %}
|
||||
<img src="{{ update.actor_detail.avatar_url }}" width="25" height="25" border="0" />
|
||||
{% else %}
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="width: 25px; height: 25px; background-color: #4f3422; border-radius: 50%; text-align: center;">
|
||||
<span style="color: white; font-weight: 500; font-size: 13px; line-height: 25px; display: inline-block;">{{ update.actor_detail.first_name.0 }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="padding-top: 20px; padding-bottom: 20px">
|
||||
<p style="font-weight: 500; font-size: 0.8rem; color: #1c2024; width: fit-content; margin-left: 5px; font-family: 'Inter', sans-serif;">{{ update.actor_detail.first_name }} {{ update.actor_detail.last_name }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-weight: 500; font-size: 0.6rem; color: #80838d; margin-left: 10px; font-family: 'Inter', sans-serif;">{{ update.activity_time }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% if update.changes.lead %}
|
||||
<!-- Lead changed -->
|
||||
<table role="presentation" style="padding-bottom: 15px; max-width: 100%">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/assignee.png" width="12" height="12" border="0" style="display: block" />
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #525252; font-family: 'Inter', sans-serif;">Lead:</p>
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646C; font-family: 'Inter', sans-serif;">{{ update.changes.lead.old_value.0|default:"—" }}</p>
|
||||
</td>
|
||||
<td style="padding-left: 10px; padding-right: 10px;">
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.png" width="16" height="16" border="0" style="display: block;" />
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646c; font-family: 'Inter', sans-serif;">{{update.changes.lead.new_value|last|default:"—" }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if update.changes.labels %}
|
||||
<!-- Labels -->
|
||||
<table role="presentation" style="padding-bottom: 15px; max-width: 100%; padding-right: 10px;">
|
||||
<tr>
|
||||
<td valign="top" style="white-space: nowrap; padding: 0px;">
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/labels.png" width="12" height="12" border="0" style="display: inline-block;" />
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #525252; padding-right: 5px; font-family: 'Inter', sans-serif;">Labels:</span>
|
||||
</td>
|
||||
<td style="overflow-wrap: anywhere;word-break: break-all; padding: 0px;">
|
||||
{% if update.changes.labels.new_value.0 %}
|
||||
<span style="font-size: 0.8rem; line-height: 1px; color: #0d74ce; background-color: #e6f4fe; margin-right: 5px; padding-left: 4px; padding-right: 4px; padding-bottom: 2px; padding-top: 2px; font-weight: 500; border-radius: 2px; max-lines: 1; font-family: 'Inter', sans-serif;">{{update.changes.labels.new_value.0}}</span>
|
||||
{% endif %}
|
||||
{% if update.changes.labels.new_value.1 %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #0d74ce; margin-left: 2px; margin-right: 5px; white-space: nowrap; font-family: 'Inter', sans-serif;">+{{ update.changes.labels.new_value|length|add:"-1"}} more</span>
|
||||
{% endif %}
|
||||
{% if update.changes.labels.old_value.0 %}
|
||||
<span style="font-size: 0.8rem; padding-left: 4px; padding-right: 4px; padding-bottom: 2px; padding-top: 2px; font-weight: 500; text-decoration: line-through; color: #641723; background-color: #feebec; margin-right: 5px; border-radius: 2px; font-family: 'Inter', sans-serif;">{{update.changes.labels.old_value.0}}</span>
|
||||
{% endif %}
|
||||
{% if update.changes.labels.old_value.1 %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-left: 2px; white-space: nowrap; font-family: 'Inter', sans-serif;">+{{ update.changes.labels.old_value|length|add:"-1"}} more</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if update.changes.projects %}
|
||||
<!-- Projects changed -->
|
||||
<table role="presentation" style="max-width: 100%; padding-bottom: 15px">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/projects.png" width="12" height="12" border="0" style="display: inline-block" />
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #525252; font-family: 'Inter', sans-serif;">Projects:</span>
|
||||
</td>
|
||||
{% if update.changes.projects.new_value.0 %}
|
||||
<td style="padding-left: 5px;overflow-wrap: break-word;">
|
||||
{% for project in update.changes.projects.new_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">{{ project }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.projects.new_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ update.changes.projects.new_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.projects.old_value.0 %}
|
||||
<td style="padding-left: 8px;">
|
||||
{% for project in update.changes.projects.old_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; text-decoration: line-through; font-family: 'Inter', sans-serif;">{{ project }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.projects.old_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ update.changes.projects.old_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if update.changes.members %}
|
||||
<!-- Members changed -->
|
||||
<table role="presentation" style="max-width: 100%; padding-bottom: 15px">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/assignee.png" width="12" height="12" border="0" style="display: inline-block" />
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #525252; font-family: 'Inter', sans-serif;">Members:</span>
|
||||
</td>
|
||||
{% if update.changes.members.new_value.0 %}
|
||||
<td style="padding-left: 5px;overflow-wrap: break-word;">
|
||||
{% for member in update.changes.members.new_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">{{ member }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.members.new_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ update.changes.members.new_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.members.old_value.0 %}
|
||||
<td style="padding-left: 8px;">
|
||||
{% for member in update.changes.members.old_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; text-decoration: line-through; font-family: 'Inter', sans-serif;">{{ member }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.members.old_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ update.changes.members.old_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<!-- Outer update Box end -->
|
||||
|
||||
{% if comments.0 %}
|
||||
<!-- Comments outer update Box -->
|
||||
<div style="max-width: 100%; background-color: #f7f9ff; border-radius: 8px; border-style: solid; border-width: 1px; border-color: #c1d0ff; padding: 20px; margin-top: 15px;">
|
||||
<!-- Block Heading -->
|
||||
<p style="font-size: 0.8rem; font-weight: 600; color: #121a26; padding-bottom: 20px; font-family: 'Inter', sans-serif;">Comment</p>
|
||||
<!-- Comments -->
|
||||
{% for comment in comments %}
|
||||
<table cellspacing="0" style="width: 100%; margin-bottom: 15px;">
|
||||
<tr>
|
||||
<td valign="top" style="width: 30px; padding-bottom: 10px;">
|
||||
{% if comment.actor_detail.avatar_url %}
|
||||
<img src="{{ comment.actor_detail.avatar_url }}" width="25" height="25" border="0" style="border-radius: 50%; display: block;" />
|
||||
{% else %}
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="width: 25px; height: 25px; background-color: #4f3422; border-radius: 50%; text-align: center;">
|
||||
<span style="color: white; font-weight: 500; font-size: 13px; line-height: 25px; display: inline-block;">{{ comment.actor_detail.first_name.0 }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td valign="top" style="padding-left: 8px; padding-bottom: 10px;">
|
||||
<p style="font-weight: 500; font-size: 0.8rem; color: #1c2024; margin: 0; padding: 0; font-family: 'Inter', sans-serif;">{{ comment.actor_detail.first_name }} {{ comment.actor_detail.last_name }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% for actor_comment in comment.actor_comments.new_value %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td style="padding-left: 33px; padding-top: 5px;">
|
||||
<div style="padding: 8px 12px; background-color: white; font-size: 0.8rem; color: #1c2024; border-radius: 6px; border: 1px solid #e5e7eb; max-width: 430px; word-wrap: break-word; font-family: 'Inter', sans-serif;">
|
||||
<p style="margin: 0; padding: 0;">{{ actor_comment|safe }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{{ entity_url }}" style="text-decoration: none;">
|
||||
<div style="max-width: min-content; white-space: nowrap; background-color: #3e63dd; padding: 10px 15px; border: 1px solid #2f4ba8; border-radius: 4px; margin-top: 15px; cursor: pointer; font-size: 0.8rem; color: white; font-family: 'Inter', sans-serif;">
|
||||
View teamspace
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<table style="width: 100%; padding: 20px; justify-content: center;" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="font-size: 0.8rem; color: #1c2024; font-family: 'Inter', sans-serif;">
|
||||
This email was sent to
|
||||
<a href="mailto:{{receiver.email}}" style="color: #3a5bc7; font-weight: 500; text-decoration: none;">{{ receiver.email }}</a>.
|
||||
If you'd rather not receive this kind of email,
|
||||
<a href="{{ entity_url }}" style="color: #3a5bc7; text-decoration: none;">you can unsubscribe to the teamspace</a>
|
||||
or
|
||||
<a href="{{ user_preference }}" style="color: #3a5bc7; text-decoration: none;">manage your email preferences</a>.
|
||||
<!-- Social links -->
|
||||
<div style="margin-top: 60px; float: right;">
|
||||
<a href="https://github.com/makeplane" target="_blank" style="margin-left: 10px; text-decoration: none;">
|
||||
<img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="25" height="25" border="0" alt="GitHub" style="display: inline-block;" />
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/planepowers/" target="_blank" style="margin-left: 10px; text-decoration: none;">
|
||||
<img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="25" height="25" border="0" alt="LinkedIn" style="display: inline-block;" />
|
||||
</a>
|
||||
<a href="https://twitter.com/planepowers" target="_blank" style="margin-left: 10px; text-decoration: none;">
|
||||
<img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="25" height="25" border="0" alt="Twitter" style="display: inline-block;" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
<!-- action performer -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding-bottom: 2px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td style="padding-right: 8px;">
|
||||
{% if actor_detail.avatar_url %}
|
||||
<img src="{{ actor_detail.avatar_url }}" width="25" height="25" border="0" style="border-radius: 50%; display: block;" />
|
||||
{% else %}
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="background-color: #4f3422; border-radius: 50%; overflow: hidden; max-height: 28px; height: 28px; max-width: 28px; width: 28px;">
|
||||
<tr>
|
||||
<td style="text-align: center; padding: 0; height: 28px; width: 28px; vertical-align: middle;">
|
||||
<span style="color: white; font-weight: 500; font-size: 13px; line-height: 28px; display: inline-block;">{{ actor_detail.first_name.0 }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td style="color: #1c2024; font-weight: 500; font-size: 12px; line-height: 16px; font-family: 'Inter', sans-serif; padding-bottom: 2px;">
|
||||
{{ actor_detail.first_name }} {{ actor_detail.last_name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color: #6c7275; font-weight: 400; font-size: 10px; line-height: 14px; font-family: 'Inter', sans-serif;">
|
||||
{{ activity_time }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1,58 @@
|
||||
{% if comments.0 %} <!-- Comments outer update Box -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 15px;">
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #FAFAFA; border-radius: 8px; border: 1px solid #FAFAFA;">
|
||||
<tr>
|
||||
<td style="padding: 20px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td style="color: #1D1F20; font-size: 16px; font-family: 'Inter', sans-serif; font-weight: 600; line-height: 24px; padding-bottom: 12px;">
|
||||
Comment
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Comments -->
|
||||
{% for comment in comments %}
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-bottom: 15px;">
|
||||
<tr>
|
||||
<td valign="top" style="width: 30px; padding-bottom: 10px;">
|
||||
{% if comment.actor_detail.avatar_url %}
|
||||
<img src="{{ comment.actor_detail.avatar_url }}" width="25" height="25" border="0" style="border-radius: 50%; display: block;" />
|
||||
{% else %}
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="background-color: #4f3422; border-radius: 50%; overflow: hidden; max-height: 28px; height: 28px; max-width: 28px; width: 28px;">
|
||||
<tr>
|
||||
<td style="text-align: center; padding: 0; height: 28px; width: 28px; vertical-align: middle;">
|
||||
<span style="color: white; font-weight: 500; font-size: 13px; line-height: 28px; display: inline-block;">{{ comment.actor_detail.first_name.0 }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td valign="top" style="padding-left: 8px; padding-bottom: 10px;">
|
||||
<p style="font-weight: 500; font-size: 0.8rem; color: #1c2024; margin: 0; padding: 0; font-family: 'Inter', sans-serif;">{{ comment.actor_detail.first_name }} {{ comment.actor_detail.last_name }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% for actor_comment in comment.actor_comments.new_value %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td style="padding-left: 33px; padding-top: 5px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding: 8px 12px; background-color: white; border-radius: 6px; border: 1px solid #e5e7eb; max-width: 430px; word-wrap: break-word;">
|
||||
<tr>
|
||||
<td style="font-size: 0.8rem; color: #1c2024; font-family: 'Inter', sans-serif;">
|
||||
<p style="margin: 0; padding: 0;">{{ actor_comment|safe }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" >
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ title|default:"Email Update" }}</title>
|
||||
<style type="text/css" emogrify="no"> html { font-family: system-ui; } p, h1, h2, h3, h4, ol, ul { margin: 0; } h-full { height: 100%; } a:hover { color: #3358d4 !important; } </style>
|
||||
<style> *[class="gmail-fix"] { display: none !important; } </style>
|
||||
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style>
|
||||
</head>
|
||||
<body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #f7f9ff; margin: 20px" >
|
||||
<div style=" width: 600px; table-layout: fixed; height: 100%; margin-left: auto; margin-right: auto; " >
|
||||
{% include "emails/partials/email_header.html" %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
{% include "emails/partials/email_footer.html" %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
<!-- Footer -->
|
||||
<table style="width: 100%; padding: 20px; justify-content: center;" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="font-size: 0.8rem; color: #1c2024; font-family: 'Inter', sans-serif;">
|
||||
This email was sent to
|
||||
<a href="mailto:{{receiver.email}}" style="color: #3a5bc7; font-weight: 500; text-decoration: none;">{{ receiver.email }}</a>.
|
||||
If you'd rather not receive this kind of email,
|
||||
<a href="{{ entity_url }}" style="color: #3a5bc7; text-decoration: none;">you can unsubscribe to the {{entity_type}}</a>
|
||||
or
|
||||
<a href="{{ user_preference }}" style="color: #3a5bc7; text-decoration: none;">manage your email preferences</a>.
|
||||
|
||||
<!-- Social links -->
|
||||
<div style="margin-top: 60px; float: right;">
|
||||
<a href="https://github.com/makeplane" target="_blank" style="margin-left: 10px; text-decoration: none;">
|
||||
<img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="25" height="25" border="0" alt="GitHub" style="display: inline-block;" />
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/planepowers/" target="_blank" style="margin-left: 10px; text-decoration: none;">
|
||||
<img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="25" height="25" border="0" alt="LinkedIn" style="display: inline-block;" />
|
||||
</a>
|
||||
<a href="https://twitter.com/planepowers" target="_blank" style="margin-left: 10px; text-decoration: none;">
|
||||
<img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="25" height="25" border="0" alt="Twitter" style="display: inline-block;" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1,12 @@
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<table style="width: 600px;" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="margin: 20px 0 20px 30px;">
|
||||
<img src="https://media.docs.plane.so/logo/new-logo-white.png" width="150" border="0" alt="Plane Logo" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
{% if change %} <!-- date changed -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-bottom: 15px;">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/due-date.png" width="12" height="12" border="0" style="display: block;" />
|
||||
</td>
|
||||
<td style="padding: 0px; text-align: center;">
|
||||
<p style="padding: 0px; margin: 0px; font-size: 0.8rem; font-weight: 500; color: #525252; font-family: 'Inter', sans-serif;">{{ label|default:"Date" }}:</p>
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646C; margin-left: 5px; padding: 0px; font-family: 'Inter', sans-serif;">{{ change.old_value.0|default:"—" }}</p>
|
||||
</td>
|
||||
<td style="padding-left: 10px; padding-right: 10px;">
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.png" width="16" height="16" border="0" style="display: block;" />
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646c; padding: 0px; font-family: 'Inter', sans-serif;">{{change.new_value|last|default:"—" }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% if change %} <!-- Epics changed -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-bottom: 15px;">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/blocking.png" width="12" height="12" border="0" style="display: inline-block;" />
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #525252; margin-right: 5px; font-family: 'Inter', sans-serif;">Epics:</span>
|
||||
</td>
|
||||
{% if change.new_value.0 %}
|
||||
<td style="padding-left: 5px;overflow-wrap: break-word;">
|
||||
{% for epic in change.new_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">{{ epic }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if change.new_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ change.new_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if change.old_value.0 %}
|
||||
<td style="padding-left: 8px;">
|
||||
{% for epic in change.old_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; text-decoration: line-through; font-family: 'Inter', sans-serif;">{{ epic }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if change.old_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ change.old_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% if change %} <!-- Labels -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-bottom: 15px; padding-right: 10px;">
|
||||
<tr>
|
||||
<td valign="top" style="white-space: nowrap; padding: 0px;">
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/labels.png" width="12" height="12" border="0" style="display: inline-block;" />
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #525252; padding-right: 5px; font-family: 'Inter', sans-serif;">Labels:</span>
|
||||
</td>
|
||||
<td style="overflow-wrap: anywhere;word-break: break-all; padding: 0px;">
|
||||
{% if change.new_value.0 %}
|
||||
<span style="font-size: 0.8rem; line-height: 1px; color: #0d74ce; background-color: #e6f4fe; margin-right: 5px; padding-left: 4px; padding-right: 4px; padding-bottom: 2px; padding-top: 2px; font-weight: 500; border-radius: 2px; max-lines: 1; font-family: 'Inter', sans-serif;">{{change.new_value.0}}</span>
|
||||
{% endif %}
|
||||
{% if change.new_value.1 %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #0d74ce; margin-left: 2px; margin-right: 5px; white-space: nowrap; font-family: 'Inter', sans-serif;">+{{ change.new_value|length|add:"-1"}} more</span>
|
||||
{% endif %}
|
||||
{% if change.old_value.0 %}
|
||||
<span style="font-size: 0.8rem; padding-left: 4px; padding-right: 4px; padding-bottom: 2px; padding-top: 2px; font-weight: 500; text-decoration: line-through; color: #641723; background-color: #feebec; margin-right: 5px; border-radius: 2px; font-family: 'Inter', sans-serif;">{{change.old_value.0}}</span>
|
||||
{% endif %}
|
||||
{% if change.old_value.1 %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-left: 2px; white-space: nowrap; font-family: 'Inter', sans-serif;">+{{ change.old_value|length|add:"-1"}} more</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% if change %} <!-- Lead changed -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-bottom: 15px;">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/assignee.png" width="12" height="12" border="0" style="display: block;" />
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #525252; font-family: 'Inter', sans-serif;">Lead:</p>
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646C; font-family: 'Inter', sans-serif;">{{ change.old_value.0|default:"—" }}</p>
|
||||
</td>
|
||||
<td style="padding-left: 10px; padding-right: 10px;">
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.png" width="16" height="16" border="0" style="display: block;" />
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646c; font-family: 'Inter', sans-serif;">{{change.new_value|last|default:"—" }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,22 @@
|
||||
{% if change %} <!-- Link Added -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-bottom: 15px;">
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/link.png" width="12" height="12" border="0" style="display: block; margin-top: 3px;" />
|
||||
</td>
|
||||
<td valign="top">
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #525252; margin-right: 5px; font-family: 'Inter', sans-serif;">Links:</p>
|
||||
</td>
|
||||
<td>
|
||||
{% for link in change.new_value %}
|
||||
<a href="{{link}}" style="font-size: 0.8rem; font-weight: 600; color: #3a5bc7; display: block; padding-bottom: 5px; font-family: 'Inter', sans-serif;">{{ link }}</a>
|
||||
{% endfor %}
|
||||
{% if change.old_value|length > 0 %}
|
||||
{% if change.old_value.0 != "None" %}
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646c; font-family: 'Inter', sans-serif;">2 Links were removed</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% if change %} <!-- Members changed -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-bottom: 15px;">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/assignee.png" width="12" height="12" border="0" style="display: inline-block;" />
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #525252; font-family: 'Inter', sans-serif;">Members:</span>
|
||||
</td>
|
||||
{% if change.new_value.0 %}
|
||||
<td style="padding-left: 5px;overflow-wrap: break-word;">
|
||||
{% for member in change.new_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">{{ member }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if change.new_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ change.new_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if change.old_value.0 %}
|
||||
<td style="padding-left: 8px;">
|
||||
{% for member in change.old_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; text-decoration: line-through; font-family: 'Inter', sans-serif;">{{ member }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if change.old_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ change.old_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% if change %} <!-- Projects changed -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-bottom: 15px;">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/labels.png" width="12" height="12" border="0" style="display: inline-block;" />
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #525252; font-family: 'Inter', sans-serif;">Projects:</span>
|
||||
</td>
|
||||
{% if change.new_value.0 %}
|
||||
<td style="padding-left: 5px;overflow-wrap: break-word;">
|
||||
{% for project in change.new_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">{{ project }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if change.new_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #3a5bc7; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ change.new_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if change.old_value.0 %}
|
||||
<td style="padding-left: 8px;">
|
||||
{% for project in change.old_value|slice:":2" %}
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; text-decoration: line-through; font-family: 'Inter', sans-serif;">{{ project }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if change.old_value.2 %}
|
||||
<td>
|
||||
<span style="font-size: 0.8rem; font-weight: 500; color: #641723; margin-right: 3px; padding-top: 0px; font-family: 'Inter', sans-serif;">+{{ change.old_value|length|add:"-2" }} more</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% if change %} <!-- Status changed -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-bottom: 15px;">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/state.png" width="12" height="12" border="0" style="display: block;" />
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #525252; font-family: 'Inter', sans-serif;">Status:</p>
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646C; font-family: 'Inter', sans-serif;">{{ change.old_value.0|default:"—" }}</p>
|
||||
</td>
|
||||
<td style="padding-left: 10px; padding-right: 10px;">
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.png" width="16" height="16" border="0" style="display: block;" />
|
||||
</td>
|
||||
<td>
|
||||
<p style="font-size: 0.8rem; font-weight: 500; color: #60646c; font-family: 'Inter', sans-serif;">{{change.new_value|last|default:"—" }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,18 @@
|
||||
<!-- Outer update Box start -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 15px;">
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #FAFAFA; border-radius: 8px; border: 1px solid #FAFAFA;">
|
||||
<tr>
|
||||
<td style="padding: 20px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td style="color: #1D1F20; font-size: 16px; font-family: 'Inter', sans-serif; font-weight: 600; line-height: 24px; padding-bottom: 12px;">
|
||||
{{ heading|default:"Updates" }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Card -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: white; border-radius: 8px; border: 1px solid #EDEFF3;">
|
||||
<tr>
|
||||
<td style="padding: 12px;">
|
||||
@@ -0,0 +1,11 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Outer update Box end -->
|
||||
|
||||
Reference in New Issue
Block a user