Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Enabled User @mentions and @mention-filters in core editor package #2544

Merged
merged 47 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
950bd7a
feat: created custom mention component
henit-chobisa Oct 26, 2023
bfc5744
feat: added mention suggestions and suggestion highlights
henit-chobisa Oct 26, 2023
718a20d
feat: created mention suggestion list for displaying mention suggestions
henit-chobisa Oct 26, 2023
f15022a
feat: created custom mention text component, for handling click event
henit-chobisa Oct 26, 2023
defab7c
feat: exposed mention component
henit-chobisa Oct 26, 2023
3e77b6e
feat: integrated and exposed `mentions` componenet with `editor-core`
henit-chobisa Oct 26, 2023
1d3618d
feat: integrated mentions extension with the core editor package
henit-chobisa Oct 26, 2023
20e5274
feat: exposed suggestion types from mentions
henit-chobisa Oct 26, 2023
dd07694
feat: added `mention-suggestion` parameters in `r-t-e` and `l-t-e`
henit-chobisa Oct 26, 2023
e587d65
feat: added `IssueMention` model in apiserver models
henit-chobisa Oct 26, 2023
14f21c7
chore: updated activities background job and added bs4 in requirements
henit-chobisa Oct 26, 2023
f25e6e1
feat: added mention removal logic in issue_activity
henit-chobisa Oct 26, 2023
797d027
chore: exposed mention types from `r-t-e` and `l-t-e`
henit-chobisa Oct 26, 2023
a771793
feat: integrated mentions in side peek view description form
henit-chobisa Oct 26, 2023
5c51af2
feat: added mentions in issue modal form
henit-chobisa Oct 26, 2023
ffdbc5a
feat: created custom react-hook for editor suggestions
henit-chobisa Oct 26, 2023
9b32a4c
feat: integrated mention suggestions block in RichTextEditor
henit-chobisa Oct 26, 2023
d73ee84
feat: added `mentions` integration in `lite-text-editor` instances
henit-chobisa Oct 26, 2023
04c1b2e
fix: tailwind loading nodemodules from packages
henit-chobisa Oct 26, 2023
f5fb9f2
feat: added styles for the mention suggestion list
henit-chobisa Oct 26, 2023
9b0f854
fix: update module import to resolve build failure
henit-chobisa Oct 26, 2023
684c860
feat: added mentions as an issue filter
henit-chobisa Oct 27, 2023
015c775
feat: added UI Changes to Implement `mention` filters
henit-chobisa Oct 27, 2023
603a21b
feat: added `mentions` as a filter option in the header
henit-chobisa Oct 27, 2023
7a7113b
feat: added mentions in the filter list options
henit-chobisa Oct 27, 2023
47ab302
feat: added mentions in default display filter options
henit-chobisa Oct 27, 2023
d57a7aa
feat: added filters in applied and issue params in store
henit-chobisa Oct 27, 2023
be7eae0
feat: modified types for adding mentions as a filter option
henit-chobisa Oct 27, 2023
00f7095
feat: modified `notification-card` to display message when it exists …
henit-chobisa Oct 27, 2023
0ea956f
Merge branch 'develop' into feat/editor-mentions
henit-chobisa Oct 27, 2023
988f4b0
feat: rewrote user mention management upon the changes made in develop
henit-chobisa Oct 30, 2023
f251c16
chore: merged debounce PR with the current PR for tracing changes
henit-chobisa Oct 30, 2023
4331570
Merge branch 'develop' into feat/editor-mentions
henit-chobisa Oct 30, 2023
6ae278e
fix: mentions_filters updated with the new setup
henit-chobisa Oct 30, 2023
52813cd
feat: updated requirements for bs4
henit-chobisa Oct 30, 2023
c5e20ce
feat: modified `mentions-filter` to remove many to many dependency
henit-chobisa Oct 31, 2023
595cb98
feat: implemented list manipulation instead of for loop
henit-chobisa Oct 31, 2023
b76bb91
feat: added readonly functionality in `read-only` editor core
henit-chobisa Oct 31, 2023
d233385
feat: added UI Changes for read-only mode
henit-chobisa Oct 31, 2023
03aa75f
Merge branch 'develop' into feat/editor-mentions
henit-chobisa Oct 31, 2023
deed7d9
feat: added mentions store in web Root Store
henit-chobisa Oct 31, 2023
e8c930f
chore: renamed `use-editor-suggestions` hook
henit-chobisa Oct 31, 2023
d05f870
feat: UI Improvements for conditional highlights w.r.t readonly in me…
henit-chobisa Oct 31, 2023
beb0590
fix: removed mentions from `filter_set` parameters
henit-chobisa Oct 31, 2023
da2893e
fix: merge conflicts resolved
sriramveeraghanta Nov 1, 2023
cbdcddb
fix: minor merge fixes
sriramveeraghanta Nov 1, 2023
df77b69
fix: package lock updates
sriramveeraghanta Nov 1, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apiserver/plane/bgtasks/issue_activites_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -1534,6 +1534,8 @@ def issue_activity(
IssueActivitySerializer(issue_activities_created, many=True).data,
cls=DjangoJSONEncoder,
),
requested_data=requested_data,
current_instance=current_instance
)

return
Expand Down
175 changes: 168 additions & 7 deletions apiserver/plane/bgtasks/notification_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,98 @@
from django.utils import timezone

# Module imports
from plane.db.models import IssueSubscriber, Project, IssueAssignee, Issue, Notification
from plane.db.models import IssueMention, IssueSubscriber, Project, User, IssueAssignee, Issue, Notification

# Third Party imports
from celery import shared_task
from bs4 import BeautifulSoup


def get_new_mentions(requested_instance, current_instance):
# requested_data is the newer instance of the current issue
# current_instance is the older instance of the current issue, saved in the database

# extract mentions from both the instance of data
mentions_older = extract_mentions(current_instance)
mentions_newer = extract_mentions(requested_instance)

# Getting Set Difference from mentions_newer
henit-chobisa marked this conversation as resolved.
Show resolved Hide resolved
new_mentions = [
mention for mention in mentions_newer if mention not in mentions_older]

return new_mentions

# Get Removed Mention


def get_removed_mentions(requested_instance, current_instance):
# requested_data is the newer instance of the current issue
# current_instance is the older instance of the current issue, saved in the database

# extract mentions from both the instance of data
mentions_older = extract_mentions(current_instance)
mentions_newer = extract_mentions(requested_instance)

# Getting Set Difference from mentions_newer
removed_mentions = [
henit-chobisa marked this conversation as resolved.
Show resolved Hide resolved
mention for mention in mentions_older if mention not in mentions_newer]

return removed_mentions

# Adds mentions as subscribers


def extract_mentions_as_subscribers(project_id, issue_id, mentions):
# mentions is an array of User IDs representing the FILTERED set of mentioned users

bulk_mention_subscribers = []

for mention_id in mentions:
# If the particular mention has not already been subscribed to the issue, he must be sent the mentioned notification
if not IssueSubscriber.objects.filter(
issue_id=issue_id,
subscriber=mention_id,
project=project_id,
).exists():
mentioned_user = User.objects.get(pk=mention_id)

project = Project.objects.get(pk=project_id)
issue = Issue.objects.get(pk=issue_id)

bulk_mention_subscribers.append(IssueSubscriber(
workspace=project.workspace,
project=project,
issue=issue,
subscriber=mentioned_user,
))
return bulk_mention_subscribers

# Parse Issue Description & extracts mentions


def extract_mentions(issue_instance):
try:
# issue_instance has to be a dictionary passed, containing the description_html and other set of activity data.
mentions = []
# Convert string to dictionary
data = json.loads(issue_instance)
html = data.get("description_html")
soup = BeautifulSoup(html, 'html.parser')
mention_tags = soup.find_all(
'mention-component', attrs={'target': 'users'})

mentions = [mention_tag['id'] for mention_tag in mention_tags]

return list(set(mentions))
except Exception as e:
return []


@shared_task
def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created):
def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created, requested_data, current_instance):
issue_activities_created = (
json.loads(issue_activities_created) if issue_activities_created is not None else None
json.loads(
issue_activities_created) if issue_activities_created is not None else None
)
if type not in [
"cycle.activity.created",
Expand All @@ -33,14 +115,35 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
]:
# Create Notifications
bulk_notifications = []

"""
Mention Tasks
1. Perform Diffing and Extract the mentions, that mention notification needs to be sent
2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers
"""

# Get new mentions from the newer instance
new_mentions = get_new_mentions(
requested_instance=requested_data, current_instance=current_instance)
removed_mention = get_removed_mentions(
requested_instance=requested_data, current_instance=current_instance)

# Get New Subscribers from the mentions of the newer instance
requested_mentions = extract_mentions(
issue_instance=requested_data)
mention_subscribers = extract_mentions_as_subscribers(
project_id=project_id, issue_id=issue_id, mentions=requested_mentions)

issue_subscribers = list(
IssueSubscriber.objects.filter(project_id=project_id, issue_id=issue_id)
.exclude(subscriber_id=actor_id)
IssueSubscriber.objects.filter(
project_id=project_id, issue_id=issue_id)
.exclude(subscriber_id__in=list(new_mentions + [actor_id]))
.values_list("subscriber", flat=True)
)

issue_assignees = list(
IssueAssignee.objects.filter(project_id=project_id, issue_id=issue_id)
IssueAssignee.objects.filter(
project_id=project_id, issue_id=issue_id)
.exclude(assignee_id=actor_id)
.values_list("assignee", flat=True)
)
Expand Down Expand Up @@ -89,7 +192,8 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
"new_value": str(issue_activity.get("new_value")),
"old_value": str(issue_activity.get("old_value")),
"issue_comment": str(
issue_activity.get("issue_comment").comment_stripped
issue_activity.get(
"issue_comment").comment_stripped
if issue_activity.get("issue_comment") is not None
else ""
),
Expand All @@ -98,5 +202,62 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
)
)

# Add Mentioned as Issue Subscribers
IssueSubscriber.objects.bulk_create(
mention_subscribers, batch_size=100)

for mention_id in new_mentions:
if (mention_id != actor_id):
for issue_activity in issue_activities_created:
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender="in_app:issue_activities:mention",
triggered_by_id=actor_id,
receiver_id=mention_id,
entity_identifier=issue_id,
entity_name="issue",
project=project,
message=f"You have been mentioned in the issue {issue.name}",
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(issue.project.identifier),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(issue_activity.get("id")),
"verb": str(issue_activity.get("verb")),
"field": str(issue_activity.get("field")),
"actor": str(issue_activity.get("actor_id")),
"new_value": str(issue_activity.get("new_value")),
"old_value": str(issue_activity.get("old_value")),
},
},
)
)

# Create New Mentions Here
aggregated_issue_mentions = []

for mention_id in new_mentions:
mentioned_user = User.objects.get(pk=mention_id)
aggregated_issue_mentions.append(
IssueMention(
mention=mentioned_user,
issue=issue,
project=project,
workspace=project.workspace
)
)

IssueMention.objects.bulk_create(
aggregated_issue_mentions, batch_size=100)
IssueMention.objects.filter(
issue=issue.id, mention__in=removed_mention).delete()

# Bulk create notifications
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import django.db.models.deletion
import plane.db.models.issue


class Migration(migrations.Migration):

dependencies = [
Expand All @@ -18,4 +17,4 @@ class Migration(migrations.Migration):
name='properties',
field=models.JSONField(default=plane.db.models.issue.get_default_properties),
),
]
]
45 changes: 45 additions & 0 deletions apiserver/plane/db/migrations/0047_issue_mention_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 4.2.5 on 2023-10-25 05:01

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):

dependencies = [
('db', '0046_alter_analyticview_created_by_and_more'),
]

operations = [
migrations.CreateModel(
name="issue_mentions",
fields=[
('created_at', models.DateTimeField(
auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(
auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4,
editable=False, primary_key=True, serialize=False, unique=True)),
('mention', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='issue_mention', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='issuemention_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='issue_mention', to='db.issue')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='project_issuemention', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='issuemention_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='workspace_issuemention', to='db.workspace')),
],
options={
'verbose_name': 'IssueMention',
'verbose_name_plural': 'IssueMentions',
'db_table': 'issue_mentions',
'ordering': ('-created_at',),
},
)
]
1 change: 1 addition & 0 deletions apiserver/plane/db/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
Label,
IssueBlocker,
IssueRelation,
IssueMention,
IssueLink,
IssueSequence,
IssueAttachment,
Expand Down
21 changes: 20 additions & 1 deletion apiserver/plane/db/models/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,26 @@ class Meta:
ordering = ("-created_at",)

def __str__(self):
return f"{self.issue.name} {self.related_issue.name}"
return f"{self.issue.name} {self.related_issue.name}"

class IssueMention(ProjectBaseModel):
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_mention"
)
mention = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="issue_mention",
)
class Meta:
unique_together = ["issue", "mention"]
verbose_name = "Issue Mention"
verbose_name_plural = "Issue Mentions"
db_table = "issue_mentions"
ordering = ("-created_at",)

def __str__(self):
return f"{self.issue.name} {self.mention.email}"


class IssueAssignee(ProjectBaseModel):
Expand Down
12 changes: 12 additions & 0 deletions apiserver/plane/utils/issue_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ def filter_assignees(params, filter, method):
filter["assignees__in"] = params.get("assignees")
return filter

def filter_mentions(params, filter, method):
if method == "GET":
mentions = [item for item in params.get("mentions").split(",") if item != 'null']
mentions = filter_valid_uuids(mentions)
if len(mentions) and "" not in mentions:
filter["issue_mention__mention__id__in"] = mentions
else:
if params.get("mentions", None) and len(params.get("mentions")) and params.get("mentions") != 'null':
filter["issue_mention__mention__id__in"] = params.get("mentions")
return filter


def filter_created_by(params, filter, method):
if method == "GET":
Expand Down Expand Up @@ -326,6 +337,7 @@ def issue_filters(query_params, method):
"parent": filter_parent,
"labels": filter_labels,
"assignees": filter_assignees,
"mentions": filter_mentions,
"created_by": filter_created_by,
"name": filter_name,
"created_at": filter_created_at,
Expand Down
3 changes: 2 additions & 1 deletion apiserver/requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ django_celery_beat==2.5.0
psycopg-binary==3.1.10
psycopg-c==3.1.10
scout-apm==2.26.1
openpyxl==3.1.2
openpyxl==3.1.2
beautifulsoup4==4.12.2
12 changes: 7 additions & 5 deletions packages/editor/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@
"check-types": "tsc --noEmit"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "18.2.0",
"next": "12.3.2",
"next-themes": "^0.2.1"
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "18.2.0"
},
"dependencies": {
"react-moveable" : "^0.54.2",
"@blueprintjs/popover2": "^2.0.10",
"@tiptap/core": "^2.1.7",
"@tiptap/extension-color": "^2.1.11",
"@tiptap/extension-image": "^2.1.7",
"@tiptap/extension-link": "^2.1.7",
"@tiptap/extension-mention": "^2.1.12",
"@tiptap/extension-table": "^2.1.6",
"@tiptap/extension-table-cell": "^2.1.6",
"@tiptap/extension-table-header": "^2.1.6",
Expand All @@ -44,16 +44,18 @@
"@tiptap/pm": "^2.1.7",
"@tiptap/react": "^2.1.7",
"@tiptap/starter-kit": "^2.1.10",
"@tiptap/suggestion": "^2.0.4",
"@types/node": "18.15.3",
"@types/react": "^18.2.5",
"@types/react-dom": "18.0.11",
"@types/node": "18.15.3",
"class-variance-authority": "^0.7.0",
"clsx": "^1.2.1",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"eventsource-parser": "^0.1.0",
"lucide-react": "^0.244.0",
"react-markdown": "^8.0.7",
"react-moveable": "^0.54.2",
"tailwind-merge": "^1.14.0",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.2",
Expand Down
Loading
Loading