[WEB-460] refactor: editors, chore: pages list improvement (#4090)

* fix: stroing the transactions in page

* fix: page details changes

* chore: page response change

* chore: removed duplicated endpoints

* chore: optimised the urls

* chore: removed archived and favorite pages

* chore: revamping pages store and components

* mentions loading state part done

* fixed mentions not showing in modals

* removed comments and cleaned up types

* removed unused types

* reset: head

* chore: pages store and component updates

* style: pages list item UI

* fix: improved colors and drag handle width

* fix: slash commands are no more shown in the code blocks

* fix: cleanup/hide drag handles post drop

* fix: hide/cleanup drag handles post drag start

* fix: aligning the drag handles better with the node post css changes of the length

* fix: juggling back and forth of drag handles in ordered and unordered lists

* chore: fix imports, ts errors and other things

* fix: clearing nodes to default node i.e paragraph before converting it to other types of nodes

For more reference on what this does, please refer https://tiptap.dev/docs/editor/api/commands/clear-nodes

* chore: clearNodes after delete in case of selections being present

* fix: hiding link selector in the bubble menu if inline code block is selected

* chore: filtering, ordering and searching implemented

* chore: updated pages store and updated UI

* chore: new core editor just for document editor created

* chore: removed setIsSubmitting prop in doc editor

* fix: fixed submitting state for image uploads

* refactor: setShouldShowAlert removed

* refactor: rerenderOnPropsChange prop removed

* chore: type inference magic in ref to expose an api for controlling editor menu items from outside

* fix: naming imports

* chore: change names of the exposed functions and removing old types

* refactor: remove debouncedUpdatesEnabled prop;

* refactor: editor heading markings now parsed using html

* chore: removed unrelated components from the document editor

* refactor: page details granular components

* fix: remove onActionCompleteHandler

* refactor: removed rerenderOnProps change prop

* feat: added getMarkDown function

* chore: update dropdown option actions

* fix: sidebar markings update logic

* chore: add image and to-do list actions to the toolbar

* fix: handling refs and populating them via callbacks

* feat: scroll to node api exposed

* cleaning up editor refs when the editor is destroyed

* feat: scrolling added to read only instance of the editor

* fix: markings logic

* fix: build errors with types

* fix: build erros

* fix: subscribing to transactions of editor via ref

* chore: remove debug statements

* fix: type errors

* fix: temporary different slash commands for document editor

* chore: inline code extension style

* chore: remove border from readOnly editor

* fix: editor bottom padding

* chore: pages improvements

* chore: handle Enter key on the page title

* feat: added loading indicator logic in mentions

* fix: mentions and slash commands now work well with multiple editors in one place

* refactor: page store structure, filtering logic

* feat: added better seperation in inline code blocks

* feat: list autojoining added

* fix: pages folder structure

* fix: image refocus from external parts

* working lists somewhat

* chore: implement page reactions

* fix: build errors

* fix: build errors

* fixed drag handles stuff

* task list item fixed

* working

* fix: working on multiple nested lists

* chore: remove debug statements

* fix: Tab key on first list item handled to not go out of editor focus

* feat: threshold auto scroll support added and multi nested list selection fixed

* fix: caret color bug with improved inline code blocks

* fix: node range error when bulk deleting with list

* fix: removed slash commands from working in code blocks

* chore: update typography margins

* chore: new field added in page model

* fix: better type inference in slash commands

* chore: code block UI

* feat: image insertion at correct position using ref added

* feat: added improved mentions support for space

* fix: type errors in mentions for comments in web app

* sync: core with document-core

* fix: build errors

* fix: fallback for appendTo not being able to find active container instantly

* fix: page store

* fix: page description

* fix: css quality issues

* chore: code cleanup

* chore: removed placeholder text in codeblocks

* chore: archived pages response change

* chore: archived pages response change

* fix: initial pages list fetch

* fix: pages list filters and ordering

* chore: add access change option in the quick actions dropdown

* fix: inline code block caret fixed

* regression: removing extra text

* chore: caret color removed

* feat: copy code button added in code blocks

* fix: initial load of page details

* fix: initial load of page details

* fix: image resizing weird behavior on click/expanding it too much fixed now

* chore: copy page response

* fix: todo list spacing

* chore: description html in the copy page

* chore: handle latest description on refetch

* fix: saner scroll behaviours

* fix: block menu positioning

* fix: updated empty string description

* feat: tab change sync support added

* fix: infinite rerendering with markings

* fix: block menu finally

* fix: intial load on reload bug fixed

* fix: nested lists alignment

* fix: editor padding

* fix: first level list items copyable

* chore: list spacing

* fix: title change

* fix: pages list block items interaction

* fix: saving chip position

* fix: delete action from block menu to focus properly

* fix: margin-bottom as 0 to avoid weird spacing when a paragraph node follows a list node

* style: table, chore: lite text editor toolbar

* fix: page description tab sync

* fix: lists spacing and alignment

* refactor: document editor props

* feat: rich text editor wrapper created and migrated core

* feat: created wrapper around lite text editor and merged core

* chore: add lite text editor toolbar

* fix: build errors

* fix: type errors and addead live updation of toolbar

* chore: pages migration

* fix: inbox issue

* refactor: remove redundant package

* refactor: unused files

* fix: add dompurify to space app

* fix: inline code margin

* fix: editor className props

* fix: build errors

* fix: traversing up the tree before assuming the parent is not a list item

* fix: drag handle positions for list items fixed

* fix: removed focus at end logic after deleting block

* fix: image wrapper overflow scroll fix with block menu's position

* fix: selection and deletion logic for nested lists fixed!!

* fix: hiding the block menu while scrolling in the document/app

* fix: merge conflicts resolved from develop

* fix: inbox issue description

* chore: move page title to the web app

* fix: handling edge cases for table selection

* chore: lint issues

* refactor: list item functions moved to same file

* refactor: use mention hook

* fix: added try catch blocks for mention suggestions

* chore: remove unused code

* fix: remove console logs

* fix: remove console logs

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com>
This commit is contained in:
Aaryan Khandelwal 2024-04-11 21:28:59 +05:30 committed by GitHub
parent 8b6035d315
commit 3e2355e223
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
248 changed files with 7602 additions and 5619 deletions

View file

@ -93,6 +93,7 @@ from .page import (
PageSerializer,
PageLogSerializer,
SubPageSerializer,
PageDetailSerializer,
PageFavoriteSerializer,
)

View file

@ -3,9 +3,6 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import LabelLiteSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import (
Page,
PageLog,
@ -17,22 +14,33 @@ from plane.db.models import (
class PageSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
label_details = LabelLiteSerializer(
read_only=True, source="labels", many=True
)
labels = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
class Meta:
model = Page
fields = "__all__"
fields = [
"id",
"name",
"owned_by",
"access",
"color",
"labels",
"parent",
"is_favorite",
"is_locked",
"archived_at",
"workspace",
"project",
"created_at",
"updated_at",
"created_by",
"updated_by",
"view_props",
]
read_only_fields = [
"workspace",
"project",
@ -48,8 +56,12 @@ class PageSerializer(BaseSerializer):
labels = validated_data.pop("labels", None)
project_id = self.context["project_id"]
owned_by_id = self.context["owned_by_id"]
description_html = self.context["description_html"]
page = Page.objects.create(
**validated_data, project_id=project_id, owned_by_id=owned_by_id
**validated_data,
description_html=description_html,
project_id=project_id,
owned_by_id=owned_by_id,
)
if labels is not None:
@ -91,6 +103,13 @@ class PageSerializer(BaseSerializer):
return super().update(instance, validated_data)
class PageDetailSerializer(PageSerializer):
description_html = serializers.CharField()
class Meta(PageSerializer.Meta):
fields = PageSerializer.Meta.fields + ["description_html"]
class SubPageSerializer(BaseSerializer):
entity_details = serializers.SerializerMethodField()

View file

@ -31,102 +31,51 @@ urlpatterns = [
),
name="project-pages",
),
# favorite pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
"workspaces/<str:slug>/projects/<uuid:project_id>/favorite-pages/<uuid:pk>/",
PageFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/<uuid:page_id>/",
PageFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-pages",
),
# archived pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
PageViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
PageViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/archive/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/archive/",
PageViewSet.as_view(
{
"post": "archive",
"delete": "unarchive",
}
),
name="project-page-archive",
name="project-page-archive-unarchive",
),
# lock and unlock
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unarchive/",
PageViewSet.as_view(
{
"post": "unarchive",
}
),
name="project-page-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
PageViewSet.as_view(
{
"get": "archive_list",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/lock/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/lock/",
PageViewSet.as_view(
{
"post": "lock",
"delete": "unlock",
}
),
name="project-pages",
name="project-pages-lock-unlock",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unlock/",
PageViewSet.as_view(
{
"post": "unlock",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/transactions/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/transactions/<uuid:transaction>/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/sub-pages/",
SubPagesEndpoint.as_view(),
name="sub-page",
),

View file

@ -1,4 +1,5 @@
# Python imports
import json
from datetime import datetime
# Django imports
@ -17,6 +18,7 @@ from plane.app.serializers import (
PageLogSerializer,
PageSerializer,
SubPageSerializer,
PageDetailSerializer,
)
from plane.db.models import (
Page,
@ -28,6 +30,8 @@ from plane.db.models import (
# Module imports
from ..base import BaseAPIView, BaseViewSet
from plane.bgtasks.page_transaction_task import page_transaction
def unarchive_archive_page_and_descendants(page_id, archived_at):
# Your SQL query
@ -87,11 +91,21 @@ class PageViewSet(BaseViewSet):
def create(self, request, slug, project_id):
serializer = PageSerializer(
data=request.data,
context={"project_id": project_id, "owned_by_id": request.user.id},
context={
"project_id": project_id,
"owned_by_id": request.user.id,
"description_html": request.data.get(
"description_html", "<p></p>"
),
},
)
if serializer.is_valid():
serializer.save()
# capture the page transaction
page_transaction.delay(request.data, None, serializer.data["id"])
page = Page.objects.get(pk=serializer.data["id"])
serializer = PageDetailSerializer(page)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -125,9 +139,22 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
serializer = PageSerializer(page, data=request.data, partial=True)
serializer = PageDetailSerializer(
page, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
# capture the page transaction
if request.data.get("description_html"):
page_transaction.delay(
new_value=request.data,
old_value=json.dumps(
{
"description_html": page.description_html,
}
),
page_id=pk,
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
@ -140,18 +167,24 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
def lock(self, request, slug, project_id, page_id):
def retrieve(self, request, slug, project_id, pk=None):
page = self.get_queryset().filter(pk=pk).first()
return Response(
PageDetailSerializer(page).data, status=status.HTTP_200_OK
)
def lock(self, request, slug, project_id, pk):
page = Page.objects.filter(
pk=page_id, workspace__slug=slug, project_id=project_id
pk=pk, workspace__slug=slug, project_id=project_id
).first()
page.is_locked = True
page.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def unlock(self, request, slug, project_id, page_id):
def unlock(self, request, slug, project_id, pk):
page = Page.objects.filter(
pk=page_id, workspace__slug=slug, project_id=project_id
pk=pk, workspace__slug=slug, project_id=project_id
).first()
page.is_locked = False
@ -160,13 +193,13 @@ class PageViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
def list(self, request, slug, project_id):
queryset = self.get_queryset().filter(archived_at__isnull=True)
queryset = self.get_queryset()
pages = PageSerializer(queryset, many=True).data
return Response(pages, status=status.HTTP_200_OK)
def archive(self, request, slug, project_id, page_id):
def archive(self, request, slug, project_id, pk):
page = Page.objects.get(
pk=page_id, workspace__slug=slug, project_id=project_id
pk=pk, workspace__slug=slug, project_id=project_id
)
# only the owner or admin can archive the page
@ -184,13 +217,16 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
unarchive_archive_page_and_descendants(page_id, datetime.now())
unarchive_archive_page_and_descendants(pk, datetime.now())
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(
{"archived_at": str(datetime.now())},
status=status.HTTP_200_OK,
)
def unarchive(self, request, slug, project_id, page_id):
def unarchive(self, request, slug, project_id, pk):
page = Page.objects.get(
pk=page_id, workspace__slug=slug, project_id=project_id
pk=pk, workspace__slug=slug, project_id=project_id
)
# only the owner or admin can un archive the page
@ -213,19 +249,10 @@ class PageViewSet(BaseViewSet):
page.parent = None
page.save(update_fields=["parent"])
unarchive_archive_page_and_descendants(page_id, None)
unarchive_archive_page_and_descendants(pk, None)
return Response(status=status.HTTP_204_NO_CONTENT)
def archive_list(self, request, slug, project_id):
pages = Page.objects.filter(
project_id=project_id,
workspace__slug=slug,
).filter(archived_at__isnull=False)
pages = PageSerializer(pages, many=True).data
return Response(pages, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, pk):
page = Page.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
@ -269,29 +296,20 @@ class PageFavoriteViewSet(BaseViewSet):
serializer_class = PageFavoriteSerializer
model = PageFavorite
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(archived_at__isnull=True)
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(user=self.request.user)
.select_related("page", "page__owned_by")
def create(self, request, slug, project_id, pk):
_ = PageFavorite.objects.create(
project_id=project_id,
page_id=pk,
user=request.user,
)
return Response(status=status.HTTP_204_NO_CONTENT)
def create(self, request, slug, project_id):
serializer = PageFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, page_id):
def destroy(self, request, slug, project_id, pk):
page_favorite = PageFavorite.objects.get(
project=project_id,
user=request.user,
workspace__slug=slug,
page_id=page_id,
page_id=pk,
)
page_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

View file

@ -14,6 +14,7 @@ from plane.app.permissions import (
from plane.db.models import State, Issue
from plane.utils.cache import invalidate_cache
class StateViewSet(BaseViewSet):
serializer_class = StateSerializer
model = State
@ -38,7 +39,9 @@ class StateViewSet(BaseViewSet):
.distinct()
)
@invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False)
@invalidate_cache(
path="workspaces/:slug/states/", url_params=True, user=False
)
def create(self, request, slug, project_id):
serializer = StateSerializer(data=request.data)
if serializer.is_valid():
@ -59,7 +62,9 @@ class StateViewSet(BaseViewSet):
return Response(state_dict, status=status.HTTP_200_OK)
return Response(states, status=status.HTTP_200_OK)
@invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False)
@invalidate_cache(
path="workspaces/:slug/states/", url_params=True, user=False
)
def mark_as_default(self, request, slug, project_id, pk):
# Select all the states which are marked as default
_ = State.objects.filter(
@ -70,7 +75,9 @@ class StateViewSet(BaseViewSet):
).update(default=True)
return Response(status=status.HTTP_204_NO_CONTENT)
@invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False)
@invalidate_cache(
path="workspaces/:slug/states/", url_params=True, user=False
)
def destroy(self, request, slug, project_id, pk):
state = State.objects.get(
is_triage=False,

View file

@ -326,11 +326,11 @@ class IssueViewFavoriteViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, view_id):
view_favourite = IssueViewFavorite.objects.get(
view_favorite = IssueViewFavorite.objects.get(
project=project_id,
user=request.user,
workspace__slug=slug,
view_id=view_id,
)
view_favourite.delete()
view_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

View file

@ -0,0 +1,76 @@
# Python imports
import json
# Django imports
from django.utils import timezone
# Third-party imports
from bs4 import BeautifulSoup
# Module imports
from plane.db.models import Page, PageLog
from celery import shared_task
def extract_components(value, tag):
try:
mentions = []
html = value.get("description_html")
soup = BeautifulSoup(html, "html.parser")
mention_tags = soup.find_all(tag)
for mention_tag in mention_tags:
mention = {
"id": mention_tag.get("id"),
"entity_identifier": mention_tag.get("entity_identifier"),
"entity_name": mention_tag.get("entity_name"),
}
mentions.append(mention)
return mentions
except Exception:
return []
@shared_task
def page_transaction(new_value, old_value, page_id):
page = Page.objects.get(pk=page_id)
new_page_mention = PageLog.objects.filter(page_id=page_id).exists()
old_value = json.loads(old_value)
new_transactions = []
deleted_transaction_ids = set()
# TODO - Add "issue-embed-component", "img", "todo" components
components = ["mention-component"]
for component in components:
old_mentions = extract_components(old_value, component)
new_mentions = extract_components(new_value, component)
new_mentions_ids = {mention["id"] for mention in new_mentions}
old_mention_ids = {mention["id"] for mention in old_mentions}
deleted_transaction_ids.update(old_mention_ids - new_mentions_ids)
new_transactions.extend(
PageLog(
transaction=mention["id"],
page_id=page_id,
entity_identifier=mention["entity_identifier"],
entity_name=mention["entity_name"],
workspace_id=page.workspace_id,
project_id=page.project_id,
created_at=timezone.now(),
updated_at=timezone.now(),
)
for mention in new_mentions
if mention["id"] not in old_mention_ids or not new_page_mention
)
# Create new PageLog objects for new transactions
PageLog.objects.bulk_create(new_transactions, batch_size=10, ignore_conflicts=True)
# Delete the removed transactions
PageLog.objects.filter(
transaction__in=deleted_transaction_ids
).delete()

View file

@ -0,0 +1,20 @@
# Generated by Django 4.2.10 on 2024-04-09 11:34
from django.db import migrations, models
import plane.db.models.page
class Migration(migrations.Migration):
dependencies = [
('db', '0063_state_is_triage_alter_state_group'),
]
operations = [
migrations.AddField(
model_name="page",
name="view_props",
field=models.JSONField(
default=plane.db.models.page.get_view_props
),
),
]

View file

@ -9,6 +9,10 @@ from . import ProjectBaseModel
from plane.utils.html_processor import strip_tags
def get_view_props():
return {"full_width": False}
class Page(ProjectBaseModel):
name = models.CharField(max_length=255)
description = models.JSONField(default=dict, blank=True)
@ -35,6 +39,7 @@ class Page(ProjectBaseModel):
)
archived_at = models.DateField(null=True)
is_locked = models.BooleanField(default=False)
view_props = models.JSONField(default=get_view_props)
class Meta:
verbose_name = "Page"
@ -81,7 +86,7 @@ class PageLog(ProjectBaseModel):
ordering = ("-created_at",)
def __str__(self):
return f"{self.page.name} {self.type}"
return f"{self.page.name} {self.entity_name}"
class PageBlock(ProjectBaseModel):