Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa817a6fb1 | ||
|
|
47b1055350 | ||
|
|
57ca6d1a97 | ||
|
|
d4f2059585 | ||
|
|
41b03ee642 | ||
|
|
78ca0b2474 | ||
|
|
add67c2b13 | ||
|
|
335e13be52 |
@@ -38,8 +38,6 @@ export const WorkspaceCreateForm = () => {
|
||||
getValues,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
} = useForm<IWorkspace>({ defaultValues, mode: "onChange" });
|
||||
// derived values
|
||||
const workspaceBaseURL = encodeURI(WEB_BASE_URL || window.location.origin + "/");
|
||||
|
||||
const handleCreateWorkspace = async (formData: IWorkspace) => {
|
||||
await workspaceService
|
||||
@@ -126,7 +124,7 @@ export const WorkspaceCreateForm = () => {
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">Set your workspace's URL</h4>
|
||||
<div className="flex gap-0.5 w-full items-center rounded-md border-[0.5px] border-custom-border-200 px-3">
|
||||
<span className="whitespace-nowrap text-sm text-custom-text-200">{workspaceBaseURL}</span>
|
||||
<span className="whitespace-nowrap text-sm text-custom-text-200">{WEB_BASE_URL}/</span>
|
||||
<Controller
|
||||
control={control}
|
||||
name="slug"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
// helpers
|
||||
import { Tooltip } from "@plane/ui";
|
||||
@@ -19,9 +20,9 @@ export const WorkspaceListItem = observer(({ workspaceId }: TWorkspaceListItemPr
|
||||
|
||||
if (!workspace) return null;
|
||||
return (
|
||||
<a
|
||||
<Link
|
||||
key={workspaceId}
|
||||
href={`${WEB_BASE_URL}/${encodeURIComponent(workspace.slug)}`}
|
||||
href={encodeURI(WEB_BASE_URL + "/" + workspace.slug)}
|
||||
target="_blank"
|
||||
className="group flex items-center justify-between p-4 gap-2.5 truncate border border-custom-border-200/70 hover:border-custom-border-200 hover:bg-custom-background-90 rounded-md"
|
||||
>
|
||||
@@ -76,6 +77,6 @@ export const WorkspaceListItem = observer(({ workspaceId }: TWorkspaceListItemPr
|
||||
<div className="flex-shrink-0">
|
||||
<ExternalLink size={14} className="text-custom-text-400 group-hover:text-custom-text-200" />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -30,8 +30,7 @@ export class WorkspaceService extends APIService {
|
||||
* @returns Promise<any>
|
||||
*/
|
||||
async workspaceSlugCheck(slug: string): Promise<any> {
|
||||
const params = new URLSearchParams({ slug });
|
||||
return this.get(`/api/instances/workspace-slug-check/?${params.toString()}`)
|
||||
return this.get(`/api/instances/workspace-slug-check/?slug=${slug}`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface IWorkspaceStore {
|
||||
// computed
|
||||
workspaceIds: string[];
|
||||
// helper actions
|
||||
hydrate: (data: Record<string, IWorkspace>) => void;
|
||||
hydrate: (data: any) => void;
|
||||
getWorkspaceById: (workspaceId: string) => IWorkspace | undefined;
|
||||
// fetch actions
|
||||
fetchWorkspaces: () => Promise<IWorkspace[]>;
|
||||
@@ -59,9 +59,9 @@ export class WorkspaceStore implements IWorkspaceStore {
|
||||
// helper actions
|
||||
/**
|
||||
* @description Hydrates the workspaces
|
||||
* @param data - Record<string, IWorkspace>
|
||||
* @param data - any
|
||||
*/
|
||||
hydrate = (data: Record<string, IWorkspace>) => {
|
||||
hydrate = (data: any) => {
|
||||
if (data) this.workspaces = data;
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"version": "0.24.0",
|
||||
"version": "0.23.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.24.0"
|
||||
"version": "0.23.1"
|
||||
}
|
||||
|
||||
@@ -258,7 +258,9 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
ProjectSerializer(project).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
intake_view = request.data.get("inbox_view", project.intake_view)
|
||||
intake_view = request.data.get(
|
||||
"inbox_view", request.data.get("intake_view", False)
|
||||
)
|
||||
|
||||
if project.archived_at:
|
||||
return Response(
|
||||
|
||||
@@ -114,7 +114,7 @@ class PageViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def create(self, request, slug, project_id):
|
||||
serializer = PageSerializer(
|
||||
data=request.data,
|
||||
@@ -134,7 +134,7 @@ class PageViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
try:
|
||||
page = Page.objects.get(
|
||||
@@ -234,7 +234,7 @@ class PageViewSet(BaseViewSet):
|
||||
)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def lock(self, request, slug, project_id, pk):
|
||||
page = Page.objects.filter(
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
@@ -244,7 +244,7 @@ class PageViewSet(BaseViewSet):
|
||||
page.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def unlock(self, request, slug, project_id, pk):
|
||||
page = Page.objects.filter(
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
@@ -255,7 +255,7 @@ class PageViewSet(BaseViewSet):
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def access(self, request, slug, project_id, pk):
|
||||
access = request.data.get("access", 0)
|
||||
page = Page.objects.filter(
|
||||
@@ -296,7 +296,7 @@ class PageViewSet(BaseViewSet):
|
||||
pages = PageSerializer(queryset, many=True).data
|
||||
return Response(pages, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def archive(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(pk=pk, workspace__slug=slug, projects__id=project_id)
|
||||
|
||||
@@ -323,7 +323,7 @@ class PageViewSet(BaseViewSet):
|
||||
|
||||
return Response({"archived_at": str(datetime.now())}, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def unarchive(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(pk=pk, workspace__slug=slug, projects__id=project_id)
|
||||
|
||||
@@ -348,7 +348,7 @@ class PageViewSet(BaseViewSet):
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
|
||||
@allow_permission([ROLE.ADMIN], creator=True, model=Page)
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(pk=pk, workspace__slug=slug, projects__id=project_id)
|
||||
|
||||
|
||||
@@ -384,9 +384,11 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
intake_view = request.data.get(
|
||||
"inbox_view", request.data.get("intake_view", False)
|
||||
)
|
||||
|
||||
project = Project.objects.get(pk=pk)
|
||||
intake_view = request.data.get("inbox_view", project.intake_view)
|
||||
current_instance = json.dumps(
|
||||
ProjectSerializer(project).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
@@ -353,7 +353,6 @@ class ExportWorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
workspace__slug=slug,
|
||||
created_at__date=request.data.get("date"),
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
actor_id=user_id,
|
||||
).select_related("actor", "workspace", "issue", "project")[:10000]
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# Python imports
|
||||
import json
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
@@ -18,9 +16,8 @@ from plane.db.models import (
|
||||
IssueComment,
|
||||
IssueActivity,
|
||||
UserNotificationPreference,
|
||||
ProjectMember,
|
||||
ProjectMember
|
||||
)
|
||||
from django.db.models import Subquery
|
||||
|
||||
# Third Party imports
|
||||
from celery import shared_task
|
||||
@@ -98,8 +95,7 @@ def extract_mentions_as_subscribers(project_id, issue_id, mentions):
|
||||
).exists()
|
||||
and not Issue.objects.filter(
|
||||
project_id=project_id, pk=issue_id, created_by_id=mention_id
|
||||
).exists()
|
||||
and ProjectMember.objects.filter(
|
||||
).exists() and ProjectMember.objects.filter(
|
||||
project_id=project_id, member_id=mention_id, is_active=True
|
||||
).exists()
|
||||
):
|
||||
@@ -246,19 +242,14 @@ def notifications(
|
||||
2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers
|
||||
"""
|
||||
|
||||
# get the list of active project members
|
||||
project_members = ProjectMember.objects.filter(
|
||||
project_id=project_id, is_active=True
|
||||
).values_list("member_id", flat=True)
|
||||
|
||||
# Get new mentions from the newer instance
|
||||
new_mentions = get_new_mentions(
|
||||
requested_instance=requested_data, current_instance=current_instance
|
||||
)
|
||||
|
||||
new_mentions = [
|
||||
str(mention) for mention in new_mentions if mention in set(project_members)
|
||||
]
|
||||
new_mentions = list(ProjectMember.objects.filter(
|
||||
project_id=project_id, member_id__in=new_mentions, is_active=True
|
||||
).values_list("member_id", flat=True))
|
||||
new_mentions = [str(member_id) for member_id in new_mentions]
|
||||
removed_mention = get_removed_mentions(
|
||||
requested_instance=requested_data, current_instance=current_instance
|
||||
)
|
||||
@@ -289,11 +280,6 @@ def notifications(
|
||||
new_value=issue_comment_new_value,
|
||||
)
|
||||
comment_mentions = comment_mentions + new_comment_mentions
|
||||
comment_mentions = [
|
||||
mention
|
||||
for mention in comment_mentions
|
||||
if UUID(mention) in set(project_members)
|
||||
]
|
||||
|
||||
comment_mention_subscribers = extract_mentions_as_subscribers(
|
||||
project_id=project_id, issue_id=issue_id, mentions=all_comment_mentions
|
||||
@@ -307,11 +293,7 @@ def notifications(
|
||||
|
||||
# ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- #
|
||||
issue_subscribers = list(
|
||||
IssueSubscriber.objects.filter(
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
subscriber__in=Subquery(project_members),
|
||||
)
|
||||
IssueSubscriber.objects.filter(project_id=project_id, issue_id=issue_id, project__project_projectmember__is_active=True,)
|
||||
.exclude(
|
||||
subscriber_id__in=list(new_mentions + comment_mentions + [actor_id])
|
||||
)
|
||||
@@ -332,9 +314,7 @@ def notifications(
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
issue_assignees = IssueAssignee.objects.filter(
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
assignee__in=Subquery(project_members),
|
||||
issue_id=issue_id, project_id=project_id
|
||||
).values_list("assignee", flat=True)
|
||||
|
||||
issue_subscribers = list(set(issue_subscribers) - {uuid.UUID(actor_id)})
|
||||
|
||||
@@ -18,9 +18,6 @@ class WorkspaceSerializer(BaseSerializer):
|
||||
# Check if the slug is restricted
|
||||
if value in RESTRICTED_WORKSPACE_SLUGS:
|
||||
raise serializers.ValidationError("Slug is not valid")
|
||||
# Check uniqueness case-insensitively
|
||||
if Workspace.objects.filter(slug__iexact=value).exists():
|
||||
raise serializers.ValidationError("Slug is already in use")
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -25,7 +25,7 @@ class InstanceWorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
workspace = (
|
||||
Workspace.objects.filter(slug__iexact=slug).exists()
|
||||
Workspace.objects.filter(slug=slug).exists()
|
||||
or slug in RESTRICTED_WORKSPACE_SLUGS
|
||||
)
|
||||
return Response({"status": not workspace}, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
# Python imports
|
||||
import os
|
||||
import atexit
|
||||
|
||||
# Third party imports
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
||||
|
||||
# Global variable to track initialization
|
||||
_TRACER_PROVIDER = None
|
||||
import os
|
||||
|
||||
|
||||
def init_tracer():
|
||||
"""Initialize OpenTelemetry with proper shutdown handling"""
|
||||
global _TRACER_PROVIDER
|
||||
|
||||
# If already initialized, return existing provider
|
||||
if _TRACER_PROVIDER is not None:
|
||||
return _TRACER_PROVIDER
|
||||
# Check if already initialized to prevent double initialization
|
||||
if trace.get_tracer_provider().__class__.__name__ == "TracerProvider":
|
||||
return
|
||||
|
||||
# Configure the tracer provider
|
||||
service_name = os.environ.get("SERVICE_NAME", "plane-ce-api")
|
||||
resource = Resource.create({"service.name": service_name})
|
||||
tracer_provider = TracerProvider(resource=resource)
|
||||
|
||||
# Set as global tracer provider
|
||||
trace.set_tracer_provider(tracer_provider)
|
||||
|
||||
# Configure the OTLP exporter
|
||||
@@ -39,20 +29,12 @@ def init_tracer():
|
||||
# Initialize Django instrumentation
|
||||
DjangoInstrumentor().instrument()
|
||||
|
||||
# Store provider globally
|
||||
_TRACER_PROVIDER = tracer_provider
|
||||
|
||||
# Register shutdown handler
|
||||
atexit.register(shutdown_tracer)
|
||||
|
||||
return tracer_provider
|
||||
|
||||
|
||||
def shutdown_tracer():
|
||||
"""Shutdown OpenTelemetry tracers and processors"""
|
||||
global _TRACER_PROVIDER
|
||||
provider = trace.get_tracer_provider()
|
||||
|
||||
if _TRACER_PROVIDER is not None:
|
||||
if hasattr(_TRACER_PROVIDER, "shutdown"):
|
||||
_TRACER_PROVIDER.shutdown()
|
||||
_TRACER_PROVIDER = None
|
||||
if hasattr(provider, "shutdown"):
|
||||
provider.shutdown()
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "live",
|
||||
"version": "0.24.0",
|
||||
"version": "0.23.1",
|
||||
"description": "",
|
||||
"main": "./src/server.ts",
|
||||
"private": true,
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.24.0",
|
||||
"version": "0.23.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
@@ -22,7 +22,7 @@
|
||||
"devDependencies": {
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"turbo": "^2.3.3"
|
||||
"turbo": "^2.3.2"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"name": "plane"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./auth";
|
||||
export * from "./endpoints";
|
||||
export * from "./issue";
|
||||
export * from "./workspace";
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "0.24.0",
|
||||
"version": "0.23.1",
|
||||
"private": true,
|
||||
"main": "./src/index.ts"
|
||||
"main": "./index.ts"
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
|
||||
// PI Base Url
|
||||
export const PI_BASE_URL = process.env.NEXT_PUBLIC_PI_BASE_URL || "";
|
||||
// God Mode Admin App Base Url
|
||||
export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || "";
|
||||
export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
|
||||
export const GOD_MODE_URL = encodeURI(`${ADMIN_BASE_URL}${ADMIN_BASE_PATH}/`);
|
||||
// Publish App Base Url
|
||||
export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || "";
|
||||
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
|
||||
export const SITES_URL = encodeURI(`${SPACE_BASE_URL}${SPACE_BASE_PATH}/`);
|
||||
// Live App Base Url
|
||||
export const LIVE_BASE_URL = process.env.NEXT_PUBLIC_LIVE_BASE_URL || "";
|
||||
export const LIVE_BASE_PATH = process.env.NEXT_PUBLIC_LIVE_BASE_PATH || "";
|
||||
export const LIVE_URL = encodeURI(`${LIVE_BASE_URL}${LIVE_BASE_PATH}/`);
|
||||
@@ -1,76 +0,0 @@
|
||||
export const ORGANIZATION_SIZE = [
|
||||
"Just myself",
|
||||
"2-10",
|
||||
"11-50",
|
||||
"51-200",
|
||||
"201-500",
|
||||
"500+",
|
||||
];
|
||||
|
||||
export const RESTRICTED_URLS = [
|
||||
"404",
|
||||
"accounts",
|
||||
"api",
|
||||
"create-workspace",
|
||||
"god-mode",
|
||||
"installations",
|
||||
"invitations",
|
||||
"onboarding",
|
||||
"profile",
|
||||
"spaces",
|
||||
"workspace-invitations",
|
||||
"password",
|
||||
"flags",
|
||||
"monitor",
|
||||
"monitoring",
|
||||
"ingest",
|
||||
"plane-pro",
|
||||
"plane-ultimate",
|
||||
"enterprise",
|
||||
"plane-enterprise",
|
||||
"disco",
|
||||
"silo",
|
||||
"chat",
|
||||
"calendar",
|
||||
"drive",
|
||||
"channels",
|
||||
"upgrade",
|
||||
"billing",
|
||||
"sign-in",
|
||||
"sign-up",
|
||||
"signin",
|
||||
"signup",
|
||||
"config",
|
||||
"live",
|
||||
"admin",
|
||||
"m",
|
||||
"import",
|
||||
"importers",
|
||||
"integrations",
|
||||
"integration",
|
||||
"configuration",
|
||||
"initiatives",
|
||||
"initiative",
|
||||
"config",
|
||||
"workflow",
|
||||
"workflows",
|
||||
"epics",
|
||||
"epic",
|
||||
"story",
|
||||
"mobile",
|
||||
"dashboard",
|
||||
"desktop",
|
||||
"onload",
|
||||
"real-time",
|
||||
"one",
|
||||
"pages",
|
||||
"mobile",
|
||||
"business",
|
||||
"pro",
|
||||
"settings",
|
||||
"monitor",
|
||||
"license",
|
||||
"licenses",
|
||||
"instances",
|
||||
"instance",
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
export const ORGANIZATION_SIZE = [
|
||||
"Just myself",
|
||||
"2-10",
|
||||
"11-50",
|
||||
"51-200",
|
||||
"201-500",
|
||||
"500+",
|
||||
];
|
||||
|
||||
export const RESTRICTED_URLS = [
|
||||
"404",
|
||||
"accounts",
|
||||
"api",
|
||||
"create-workspace",
|
||||
"error",
|
||||
"god-mode",
|
||||
"installations",
|
||||
"invitations",
|
||||
"onboarding",
|
||||
"profile",
|
||||
"spaces",
|
||||
"workspace-invitations",
|
||||
];
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor",
|
||||
"version": "0.24.0",
|
||||
"version": "0.23.1",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
import { Extensions } from "@tiptap/core";
|
||||
// types
|
||||
import { TExtensions } from "@/types";
|
||||
|
||||
type Props = {
|
||||
disabledExtensions: TExtensions[];
|
||||
};
|
||||
|
||||
export const CoreEditorAdditionalExtensions = (props: Props): Extensions => {
|
||||
const {} = props;
|
||||
return [];
|
||||
};
|
||||
export const CoreEditorAdditionalExtensions = (): Extensions => [];
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
import { Extensions } from "@tiptap/core";
|
||||
// types
|
||||
import { TExtensions } from "@/types";
|
||||
|
||||
type Props = {
|
||||
disabledExtensions: TExtensions[];
|
||||
};
|
||||
|
||||
export const CoreReadOnlyEditorAdditionalExtensions = (props: Props): Extensions => {
|
||||
const {} = props;
|
||||
return [];
|
||||
};
|
||||
export const CoreReadOnlyEditorAdditionalExtensions = (): Extensions => [];
|
||||
|
||||
@@ -15,13 +15,7 @@ type Props = {
|
||||
|
||||
export const DocumentEditorAdditionalExtensions = (_props: Props) => {
|
||||
const { disabledExtensions } = _props;
|
||||
const extensions: Extensions = disabledExtensions?.includes("slash-commands")
|
||||
? []
|
||||
: [
|
||||
SlashCommands({
|
||||
disabledExtensions,
|
||||
}),
|
||||
];
|
||||
const extensions: Extensions = disabledExtensions?.includes("slash-commands") ? [] : [SlashCommands()];
|
||||
|
||||
return extensions;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./core";
|
||||
export * from "./document-extensions";
|
||||
export * from "./slash-commands";
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// extensions
|
||||
import { TSlashCommandAdditionalOption } from "@/extensions";
|
||||
// types
|
||||
import { TExtensions } from "@/types";
|
||||
|
||||
type Props = {
|
||||
disabledExtensions: TExtensions[];
|
||||
};
|
||||
|
||||
export const coreEditorAdditionalSlashCommandOptions = (props: Props): TSlashCommandAdditionalOption[] => {
|
||||
const {} = props;
|
||||
const options: TSlashCommandAdditionalOption[] = [];
|
||||
return options;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type TEditorAdditionalCommands = never;
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./editor";
|
||||
export * from "./issue-embed";
|
||||
|
||||
-2
@@ -15,7 +15,6 @@ import { EditorReadOnlyRefApi, ICollaborativeDocumentReadOnlyEditor } from "@/ty
|
||||
const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOnlyEditor) => {
|
||||
const {
|
||||
containerClassName,
|
||||
disabledExtensions,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editorClassName = "",
|
||||
embedHandler,
|
||||
@@ -38,7 +37,6 @@ const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOn
|
||||
}
|
||||
|
||||
const { editor, hasServerConnectionFailed, hasServerSynced } = useReadOnlyCollaborativeEditor({
|
||||
disabledExtensions,
|
||||
editorClassName,
|
||||
extensions,
|
||||
fileHandler,
|
||||
|
||||
@@ -10,10 +10,9 @@ import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
||||
// types
|
||||
import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig, TExtensions, TFileHandler } from "@/types";
|
||||
import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig, TFileHandler } from "@/types";
|
||||
|
||||
interface IDocumentReadOnlyEditor {
|
||||
disabledExtensions: TExtensions[];
|
||||
id: string;
|
||||
initialValue: string;
|
||||
containerClassName: string;
|
||||
@@ -32,7 +31,6 @@ interface IDocumentReadOnlyEditor {
|
||||
const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
||||
const {
|
||||
containerClassName,
|
||||
disabledExtensions,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editorClassName = "",
|
||||
embedHandler,
|
||||
@@ -53,7 +51,6 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
||||
}
|
||||
|
||||
const editor = useReadOnlyEditor({
|
||||
disabledExtensions,
|
||||
editorClassName,
|
||||
extensions,
|
||||
fileHandler,
|
||||
|
||||
@@ -19,7 +19,6 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
||||
const {
|
||||
children,
|
||||
containerClassName,
|
||||
disabledExtensions,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editorClassName = "",
|
||||
extensions,
|
||||
@@ -38,7 +37,6 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
||||
} = props;
|
||||
|
||||
const editor = useEditor({
|
||||
disabledExtensions,
|
||||
editorClassName,
|
||||
enableHistory: true,
|
||||
extensions,
|
||||
|
||||
@@ -12,7 +12,6 @@ import { IReadOnlyEditorProps } from "@/types";
|
||||
export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||
const {
|
||||
containerClassName,
|
||||
disabledExtensions,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editorClassName = "",
|
||||
fileHandler,
|
||||
@@ -23,7 +22,6 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||
} = props;
|
||||
|
||||
const editor = useReadOnlyEditor({
|
||||
disabledExtensions,
|
||||
editorClassName,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
|
||||
@@ -8,7 +8,12 @@ import { SideMenuExtension, SlashCommands } from "@/extensions";
|
||||
import { EditorRefApi, IRichTextEditor } from "@/types";
|
||||
|
||||
const RichTextEditor = (props: IRichTextEditor) => {
|
||||
const { disabledExtensions, dragDropEnabled, bubbleMenuEnabled = true, extensions: externalExtensions = [] } = props;
|
||||
const {
|
||||
disabledExtensions,
|
||||
dragDropEnabled,
|
||||
bubbleMenuEnabled = true,
|
||||
extensions: externalExtensions = [],
|
||||
} = props;
|
||||
|
||||
const getExtensions = useCallback(() => {
|
||||
const extensions = [
|
||||
@@ -19,11 +24,7 @@ const RichTextEditor = (props: IRichTextEditor) => {
|
||||
}),
|
||||
];
|
||||
if (!disabledExtensions?.includes("slash-commands")) {
|
||||
extensions.push(
|
||||
SlashCommands({
|
||||
disabledExtensions,
|
||||
})
|
||||
);
|
||||
extensions.push(SlashCommands());
|
||||
}
|
||||
|
||||
return extensions;
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
// constants
|
||||
import { COLORS_LIST } from "@/constants/common";
|
||||
// local components
|
||||
import { CalloutBlockColorSelector } from "./color-selector";
|
||||
import { CalloutBlockLogoSelector } from "./logo-selector";
|
||||
// types
|
||||
import { EAttributeNames, TCalloutBlockAttributes } from "./types";
|
||||
// utils
|
||||
import { updateStoredBackgroundColor } from "./utils";
|
||||
|
||||
type Props = NodeViewProps & {
|
||||
node: NodeViewProps["node"] & {
|
||||
attrs: TCalloutBlockAttributes;
|
||||
};
|
||||
updateAttributes: (attrs: Partial<TCalloutBlockAttributes>) => void;
|
||||
};
|
||||
|
||||
export const CustomCalloutBlock: React.FC<Props> = (props) => {
|
||||
const { editor, node, updateAttributes } = props;
|
||||
// states
|
||||
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
|
||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||
// derived values
|
||||
const activeBackgroundColor = COLORS_LIST.find((c) => node.attrs["data-background"] === c.key)?.backgroundColor;
|
||||
|
||||
return (
|
||||
<NodeViewWrapper
|
||||
className="editor-callout-component group/callout-node relative bg-custom-background-90 rounded-lg text-custom-text-100 p-4 my-2 flex items-start gap-4 transition-colors duration-500 break-words"
|
||||
style={{
|
||||
backgroundColor: activeBackgroundColor,
|
||||
}}
|
||||
>
|
||||
<CalloutBlockLogoSelector
|
||||
blockAttributes={node.attrs}
|
||||
disabled={!editor.isEditable}
|
||||
isOpen={isEmojiPickerOpen}
|
||||
handleOpen={(val) => setIsEmojiPickerOpen(val)}
|
||||
updateAttributes={updateAttributes}
|
||||
/>
|
||||
<CalloutBlockColorSelector
|
||||
disabled={!editor.isEditable}
|
||||
isOpen={isColorPickerOpen}
|
||||
toggleDropdown={() => setIsColorPickerOpen((prev) => !prev)}
|
||||
onSelect={(val) => {
|
||||
updateAttributes({
|
||||
[EAttributeNames.BACKGROUND]: val,
|
||||
});
|
||||
updateStoredBackgroundColor(val);
|
||||
}}
|
||||
/>
|
||||
<NodeViewContent as="div" className="w-full break-words" />
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,75 +0,0 @@
|
||||
import { Ban, ChevronDown } from "lucide-react";
|
||||
// constants
|
||||
import { COLORS_LIST } from "@/constants/common";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
|
||||
type Props = {
|
||||
disabled: boolean;
|
||||
isOpen: boolean;
|
||||
onSelect: (color: string | null) => void;
|
||||
toggleDropdown: () => void;
|
||||
};
|
||||
|
||||
export const CalloutBlockColorSelector: React.FC<Props> = (props) => {
|
||||
const { disabled, isOpen, onSelect, toggleDropdown } = props;
|
||||
|
||||
const handleColorSelect = (val: string | null) => {
|
||||
onSelect(val);
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("opacity-0 pointer-events-none absolute top-2 right-2 z-10 transition-opacity", {
|
||||
"group-hover/callout-node:opacity-100 group-hover/callout-node:pointer-events-auto": !disabled,
|
||||
"opacity-100 pointer-events-auto": isOpen,
|
||||
})}
|
||||
contentEditable={false}
|
||||
>
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
toggleDropdown();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-1 h-full whitespace-nowrap py-1 px-2.5 text-sm font-medium text-custom-text-300 hover:bg-white/10 active:bg-custom-background-80 rounded transition-colors",
|
||||
{
|
||||
"bg-white/10": isOpen,
|
||||
}
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<span>Color</span>
|
||||
<ChevronDown className="flex-shrink-0 size-3" />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<section className="absolute top-full right-0 z-10 mt-1 rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 p-2 shadow-custom-shadow-rg animate-in fade-in slide-in-from-top-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{COLORS_LIST.map((color) => (
|
||||
<button
|
||||
key={color.key}
|
||||
type="button"
|
||||
className="flex-shrink-0 size-6 rounded border-[0.5px] border-custom-border-400 hover:opacity-60 transition-opacity"
|
||||
style={{
|
||||
backgroundColor: color.backgroundColor,
|
||||
}}
|
||||
onClick={() => handleColorSelect(color.key)}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-300 border-[0.5px] border-custom-border-400 hover:bg-custom-background-80 transition-colors"
|
||||
onClick={() => handleColorSelect(null)}
|
||||
>
|
||||
<Ban className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { Node as NodeType } from "@tiptap/pm/model";
|
||||
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||
// types
|
||||
import { EAttributeNames, TCalloutBlockAttributes } from "./types";
|
||||
// utils
|
||||
import { DEFAULT_CALLOUT_BLOCK_ATTRIBUTES } from "./utils";
|
||||
|
||||
// Extend Tiptap's Commands interface
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
calloutComponent: {
|
||||
insertCallout: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const CustomCalloutExtensionConfig = Node.create({
|
||||
name: "calloutComponent",
|
||||
group: "block",
|
||||
content: "block+",
|
||||
|
||||
addAttributes() {
|
||||
const attributes = {
|
||||
// Reduce instead of map to accumulate the attributes directly into an object
|
||||
...Object.values(EAttributeNames).reduce((acc, value) => {
|
||||
acc[value] = {
|
||||
default: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[value],
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
return attributes;
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
markdown: {
|
||||
serialize(state: MarkdownSerializerState, node: NodeType) {
|
||||
const attrs = node.attrs as TCalloutBlockAttributes;
|
||||
const logoInUse = attrs["data-logo-in-use"];
|
||||
// add callout logo
|
||||
if (logoInUse === "emoji") {
|
||||
state.write(
|
||||
`> <img src="${attrs["data-emoji-url"]}" alt="${attrs["data-emoji-unicode"]}" width="30px" />\n`
|
||||
);
|
||||
} else {
|
||||
state.write(`> <icon>${attrs["data-icon-name"]} icon</icon>\n`);
|
||||
}
|
||||
// add an empty line after the logo
|
||||
state.write("> \n");
|
||||
// add '> ' before each line of the callout content
|
||||
state.wrapBlock("> ", null, node, () => state.renderContent(node));
|
||||
state.closeBlock(node);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: `div[${EAttributeNames.BLOCK_TYPE}="${DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[EAttributeNames.BLOCK_TYPE]}"]`,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
// Render HTML for the callout node
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["div", mergeAttributes(HTMLAttributes), 0];
|
||||
},
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
import { findParentNodeClosestToPos, Predicate, ReactNodeViewRenderer } from "@tiptap/react";
|
||||
// extensions
|
||||
import { CustomCalloutBlock } from "@/extensions";
|
||||
// helpers
|
||||
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
|
||||
// config
|
||||
import { CustomCalloutExtensionConfig } from "./extension-config";
|
||||
// utils
|
||||
import { getStoredBackgroundColor, getStoredLogo } from "./utils";
|
||||
|
||||
export const CustomCalloutExtension = CustomCalloutExtensionConfig.extend({
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
insertCallout:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
// get stored logo values and background color from the local storage
|
||||
const storedLogoValues = getStoredLogo();
|
||||
const storedBackgroundValue = getStoredBackgroundColor();
|
||||
|
||||
return commands.insertContent({
|
||||
type: this.name,
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
...storedLogoValues,
|
||||
"data-background": storedBackgroundValue,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
Backspace: ({ editor }) => {
|
||||
const { $from, empty } = editor.state.selection;
|
||||
try {
|
||||
const isParentNodeCallout: Predicate = (node) => node.type === this.type;
|
||||
const parentNodeDetails = findParentNodeClosestToPos($from, isParentNodeCallout);
|
||||
// Check if selection is empty and at the beginning of the callout
|
||||
if (empty && parentNodeDetails) {
|
||||
const isCursorAtCalloutBeginning = $from.pos === parentNodeDetails.start + 1;
|
||||
if (parentNodeDetails.node.content.size > 2 && isCursorAtCalloutBeginning) {
|
||||
editor.commands.setTextSelection(parentNodeDetails.pos - 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in performing backspace action on callout", error);
|
||||
}
|
||||
return false; // Allow the default behavior if conditions are not met
|
||||
},
|
||||
ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name),
|
||||
ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name),
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CustomCalloutBlock);
|
||||
},
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./block";
|
||||
export * from "./extension";
|
||||
export * from "./read-only-extension";
|
||||
@@ -1,97 +0,0 @@
|
||||
// plane helpers
|
||||
import { convertHexEmojiToDecimal } from "@plane/helpers";
|
||||
// plane ui
|
||||
import { EmojiIconPicker, EmojiIconPickerTypes, Logo, TEmojiLogoProps } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// types
|
||||
import { TCalloutBlockAttributes } from "./types";
|
||||
// utils
|
||||
import { DEFAULT_CALLOUT_BLOCK_ATTRIBUTES, updateStoredLogo } from "./utils";
|
||||
|
||||
type Props = {
|
||||
blockAttributes: TCalloutBlockAttributes;
|
||||
disabled: boolean;
|
||||
handleOpen: (val: boolean) => void;
|
||||
isOpen: boolean;
|
||||
updateAttributes: (attrs: Partial<TCalloutBlockAttributes>) => void;
|
||||
};
|
||||
|
||||
export const CalloutBlockLogoSelector: React.FC<Props> = (props) => {
|
||||
const { blockAttributes, disabled, handleOpen, isOpen, updateAttributes } = props;
|
||||
|
||||
const logoValue: TEmojiLogoProps = {
|
||||
in_use: blockAttributes["data-logo-in-use"],
|
||||
icon: {
|
||||
color: blockAttributes["data-icon-color"],
|
||||
name: blockAttributes["data-icon-name"],
|
||||
},
|
||||
emoji: {
|
||||
value: blockAttributes["data-emoji-unicode"]?.toString(),
|
||||
url: blockAttributes["data-emoji-url"],
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div contentEditable={false}>
|
||||
<EmojiIconPicker
|
||||
closeOnSelect={false}
|
||||
isOpen={isOpen}
|
||||
handleToggle={handleOpen}
|
||||
className="flex-shrink-0 grid place-items-center"
|
||||
buttonClassName={cn("flex-shrink-0 size-8 grid place-items-center rounded-lg", {
|
||||
"hover:bg-white/10": !disabled,
|
||||
})}
|
||||
label={<Logo logo={logoValue} size={18} type="lucide" />}
|
||||
onChange={(val) => {
|
||||
// construct the new logo value based on the type of value
|
||||
let newLogoValue: Partial<TCalloutBlockAttributes> = {};
|
||||
let newLogoValueToStoreInLocalStorage: TEmojiLogoProps = {
|
||||
in_use: "emoji",
|
||||
emoji: {
|
||||
value: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-unicode"],
|
||||
url: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-url"],
|
||||
},
|
||||
};
|
||||
if (val.type === "emoji") {
|
||||
newLogoValue = {
|
||||
"data-emoji-unicode": convertHexEmojiToDecimal(val.value.unified),
|
||||
"data-emoji-url": val.value.imageUrl,
|
||||
};
|
||||
newLogoValueToStoreInLocalStorage = {
|
||||
in_use: "emoji",
|
||||
emoji: {
|
||||
value: convertHexEmojiToDecimal(val.value.unified),
|
||||
url: val.value.imageUrl,
|
||||
},
|
||||
};
|
||||
} else if (val.type === "icon") {
|
||||
newLogoValue = {
|
||||
"data-icon-name": val.value.name,
|
||||
"data-icon-color": val.value.color,
|
||||
};
|
||||
newLogoValueToStoreInLocalStorage = {
|
||||
in_use: "icon",
|
||||
icon: {
|
||||
name: val.value.name,
|
||||
color: val.value.color,
|
||||
},
|
||||
};
|
||||
}
|
||||
// update node attributes
|
||||
updateAttributes({
|
||||
"data-logo-in-use": val.type,
|
||||
...newLogoValue,
|
||||
});
|
||||
// update stored logo in local storage
|
||||
updateStoredLogo(newLogoValueToStoreInLocalStorage);
|
||||
handleOpen(false);
|
||||
}}
|
||||
defaultIconColor={logoValue?.in_use && logoValue.in_use === "icon" ? logoValue?.icon?.color : undefined}
|
||||
defaultOpen={logoValue.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON}
|
||||
disabled={disabled}
|
||||
searchDisabled
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
// extensions
|
||||
import { CustomCalloutBlock } from "@/extensions";
|
||||
// config
|
||||
import { CustomCalloutExtensionConfig } from "./extension-config";
|
||||
|
||||
export const CustomCalloutReadOnlyExtension = CustomCalloutExtensionConfig.extend({
|
||||
selectable: false,
|
||||
draggable: false,
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CustomCalloutBlock);
|
||||
},
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
export enum EAttributeNames {
|
||||
ICON_COLOR = "data-icon-color",
|
||||
ICON_NAME = "data-icon-name",
|
||||
EMOJI_UNICODE = "data-emoji-unicode",
|
||||
EMOJI_URL = "data-emoji-url",
|
||||
LOGO_IN_USE = "data-logo-in-use",
|
||||
BACKGROUND = "data-background",
|
||||
BLOCK_TYPE = "data-block-type",
|
||||
}
|
||||
|
||||
export type TCalloutBlockIconAttributes = {
|
||||
[EAttributeNames.ICON_COLOR]: string | undefined;
|
||||
[EAttributeNames.ICON_NAME]: string | undefined;
|
||||
};
|
||||
|
||||
export type TCalloutBlockEmojiAttributes = {
|
||||
[EAttributeNames.EMOJI_UNICODE]: string | undefined;
|
||||
[EAttributeNames.EMOJI_URL]: string | undefined;
|
||||
};
|
||||
|
||||
export type TCalloutBlockAttributes = {
|
||||
[EAttributeNames.LOGO_IN_USE]: "emoji" | "icon";
|
||||
[EAttributeNames.BACKGROUND]: string;
|
||||
[EAttributeNames.BLOCK_TYPE]: "callout-component";
|
||||
} & TCalloutBlockIconAttributes &
|
||||
TCalloutBlockEmojiAttributes;
|
||||
@@ -1,85 +0,0 @@
|
||||
// plane helpers
|
||||
import { sanitizeHTML } from "@plane/helpers";
|
||||
// plane ui
|
||||
import { TEmojiLogoProps } from "@plane/ui";
|
||||
// types
|
||||
import {
|
||||
EAttributeNames,
|
||||
TCalloutBlockAttributes,
|
||||
TCalloutBlockEmojiAttributes,
|
||||
TCalloutBlockIconAttributes,
|
||||
} from "./types";
|
||||
|
||||
export const DEFAULT_CALLOUT_BLOCK_ATTRIBUTES: TCalloutBlockAttributes = {
|
||||
"data-logo-in-use": "emoji",
|
||||
"data-icon-color": null,
|
||||
"data-icon-name": null,
|
||||
"data-emoji-unicode": "128161",
|
||||
"data-emoji-url": "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png",
|
||||
"data-background": null,
|
||||
"data-block-type": "callout-component",
|
||||
};
|
||||
|
||||
type TStoredLogoValue = Pick<TCalloutBlockAttributes, EAttributeNames.LOGO_IN_USE> &
|
||||
(TCalloutBlockEmojiAttributes | TCalloutBlockIconAttributes);
|
||||
|
||||
// function to get the stored logo from local storage
|
||||
export const getStoredLogo = (): TStoredLogoValue => {
|
||||
const fallBackValues: TStoredLogoValue = {
|
||||
"data-logo-in-use": "emoji",
|
||||
"data-emoji-unicode": DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-unicode"],
|
||||
"data-emoji-url": DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-url"],
|
||||
};
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const storedData = sanitizeHTML(localStorage.getItem("editor-calloutComponent-logo"));
|
||||
if (storedData) {
|
||||
let parsedData: TEmojiLogoProps;
|
||||
try {
|
||||
parsedData = JSON.parse(storedData);
|
||||
} catch (error) {
|
||||
console.error(`Error parsing stored callout logo, stored value- ${storedData}`, error);
|
||||
localStorage.removeItem("editor-calloutComponent-logo");
|
||||
return fallBackValues;
|
||||
}
|
||||
if (parsedData.in_use === "emoji" && parsedData.emoji?.value) {
|
||||
return {
|
||||
"data-logo-in-use": "emoji",
|
||||
"data-emoji-unicode": parsedData.emoji.value || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-unicode"],
|
||||
"data-emoji-url": parsedData.emoji.url || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-url"],
|
||||
};
|
||||
}
|
||||
if (parsedData.in_use === "icon" && parsedData.icon?.name) {
|
||||
return {
|
||||
"data-logo-in-use": "icon",
|
||||
"data-icon-name": parsedData.icon.name || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-icon-name"],
|
||||
"data-icon-color": parsedData.icon.color || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-icon-color"],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallback values
|
||||
return fallBackValues;
|
||||
};
|
||||
// function to update the stored logo on local storage
|
||||
export const updateStoredLogo = (value: TEmojiLogoProps): void => {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem("editor-calloutComponent-logo", JSON.stringify(value));
|
||||
};
|
||||
// function to get the stored background color from local storage
|
||||
export const getStoredBackgroundColor = (): string | null => {
|
||||
if (typeof window !== "undefined") {
|
||||
return sanitizeHTML(localStorage.getItem("editor-calloutComponent-background"));
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// function to update the stored background color on local storage
|
||||
export const updateStoredBackgroundColor = (value: string | null): void => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (value === null) {
|
||||
localStorage.removeItem("editor-calloutComponent-background");
|
||||
return;
|
||||
} else {
|
||||
localStorage.setItem("editor-calloutComponent-background", value);
|
||||
}
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Extensions } from "@tiptap/core";
|
||||
import TaskItem from "@tiptap/extension-task-item";
|
||||
import TaskList from "@tiptap/extension-task-list";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
@@ -17,12 +18,10 @@ import { CustomMentionWithoutProps } from "./mentions/mentions-without-props";
|
||||
import { CustomQuoteExtension } from "./quote";
|
||||
import { TableHeader, TableCell, TableRow, Table } from "./table";
|
||||
import { CustomTextAlignExtension } from "./text-align";
|
||||
import { CustomCalloutExtensionConfig } from "./callout/extension-config";
|
||||
import { CustomColorExtension } from "./custom-color";
|
||||
// plane editor extensions
|
||||
import { CoreEditorAdditionalExtensionsWithoutProps } from "@/plane-editor/extensions/core/without-props";
|
||||
|
||||
export const CoreEditorExtensionsWithoutProps = [
|
||||
export const CoreEditorExtensionsWithoutProps: Extensions = [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
@@ -89,7 +88,6 @@ export const CoreEditorExtensionsWithoutProps = [
|
||||
TableRow,
|
||||
CustomMentionWithoutProps(),
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutExtensionConfig,
|
||||
CustomColorExtension,
|
||||
...CoreEditorAdditionalExtensionsWithoutProps,
|
||||
];
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Editor, mergeAttributes } from "@tiptap/core";
|
||||
import { Image } from "@tiptap/extension-image";
|
||||
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// extensions
|
||||
import { CustomImageNode } from "@/extensions/custom-image";
|
||||
import { CustomImageNode, ImageAttributes } from "@/extensions/custom-image";
|
||||
// plugins
|
||||
import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image";
|
||||
// types
|
||||
@@ -124,9 +126,14 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
deletedImageSet: new Map<string, boolean>(),
|
||||
uploadInProgress: false,
|
||||
maxFileSize,
|
||||
// escape markdown for images
|
||||
markdown: {
|
||||
serialize() {},
|
||||
serialize(state: MarkdownSerializerState, node: Node) {
|
||||
const attrs = node.attrs as ImageAttributes;
|
||||
const imageSource = state.esc(this?.editor?.commands?.getImageSource?.(attrs.src) || attrs.src);
|
||||
const imageWidth = state.esc(attrs.width?.toString());
|
||||
state.write(`<img src="${state.esc(imageSource)}" width="${imageWidth}" />`);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { mergeAttributes } from "@tiptap/core";
|
||||
import { Image } from "@tiptap/extension-image";
|
||||
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
// components
|
||||
import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image";
|
||||
import { CustomImageNode, ImageAttributes, UploadImageExtensionStorage } from "@/extensions/custom-image";
|
||||
// types
|
||||
import { TFileHandler } from "@/types";
|
||||
|
||||
@@ -52,9 +54,14 @@ export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAsset
|
||||
addStorage() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
// escape markdown for images
|
||||
markdown: {
|
||||
serialize() {},
|
||||
serialize(state: MarkdownSerializerState, node: Node) {
|
||||
const attrs = node.attrs as ImageAttributes;
|
||||
const imageSource = state.esc(this?.editor?.commands?.getImageSource?.(attrs.src) || attrs.src);
|
||||
const imageWidth = state.esc(attrs.width?.toString());
|
||||
state.write(`<img src="${state.esc(imageSource)}" width="${imageWidth}" />`);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -9,7 +9,6 @@ import StarterKit from "@tiptap/starter-kit";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
// extensions
|
||||
import {
|
||||
CustomCalloutExtension,
|
||||
CustomCodeBlockExtension,
|
||||
CustomCodeInlineExtension,
|
||||
CustomCodeMarkPlugin,
|
||||
@@ -33,12 +32,11 @@ import {
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
// types
|
||||
import { IMentionHighlight, IMentionSuggestion, TExtensions, TFileHandler } from "@/types";
|
||||
import { IMentionHighlight, IMentionSuggestion, TFileHandler } from "@/types";
|
||||
// plane editor extensions
|
||||
import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
||||
|
||||
type TArguments = {
|
||||
disabledExtensions: TExtensions[];
|
||||
enableHistory: boolean;
|
||||
fileHandler: TFileHandler;
|
||||
mentionConfig: {
|
||||
@@ -50,7 +48,7 @@ type TArguments = {
|
||||
};
|
||||
|
||||
export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
const { disabledExtensions, enableHistory, fileHandler, mentionConfig, placeholder, tabIndex } = args;
|
||||
const { enableHistory, fileHandler, mentionConfig, placeholder, tabIndex } = args;
|
||||
|
||||
return [
|
||||
StarterKit.configure({
|
||||
@@ -164,10 +162,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
}),
|
||||
CharacterCount,
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutExtension,
|
||||
CustomColorExtension,
|
||||
...CoreEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
}),
|
||||
...CoreEditorAdditionalExtensions(),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./callout";
|
||||
export * from "./code";
|
||||
export * from "./code-inline";
|
||||
export * from "./custom-image";
|
||||
|
||||
@@ -23,18 +23,16 @@ import {
|
||||
HeadingListExtension,
|
||||
CustomReadOnlyImageExtension,
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutReadOnlyExtension,
|
||||
CustomColorExtension,
|
||||
} from "@/extensions";
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
// types
|
||||
import { IMentionHighlight, TExtensions, TFileHandler } from "@/types";
|
||||
import { IMentionHighlight, TFileHandler } from "@/types";
|
||||
// plane editor extensions
|
||||
import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
||||
|
||||
type Props = {
|
||||
disabledExtensions: TExtensions[];
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
mentionConfig: {
|
||||
mentionHighlights?: () => Promise<IMentionHighlight[]>;
|
||||
@@ -42,7 +40,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||
const { disabledExtensions, fileHandler, mentionConfig } = props;
|
||||
const { fileHandler, mentionConfig } = props;
|
||||
|
||||
return [
|
||||
StarterKit.configure({
|
||||
@@ -131,9 +129,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||
CustomColorExtension,
|
||||
HeadingListExtension,
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutReadOnlyExtension,
|
||||
...CoreReadOnlyEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
}),
|
||||
...CoreReadOnlyEditorAdditionalExtensions(),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
List,
|
||||
ListOrdered,
|
||||
ListTodo,
|
||||
MessageSquareText,
|
||||
MinusSquare,
|
||||
Table,
|
||||
TextQuote,
|
||||
@@ -35,14 +34,10 @@ import {
|
||||
toggleTextColor,
|
||||
toggleBackgroundColor,
|
||||
insertImage,
|
||||
insertCallout,
|
||||
setText,
|
||||
} from "@/helpers/editor-commands";
|
||||
// types
|
||||
import { CommandProps, ISlashCommandItem, TExtensions, TSlashCommandSectionKeys } from "@/types";
|
||||
// plane editor extensions
|
||||
import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions";
|
||||
// local types
|
||||
import { CommandProps, ISlashCommandItem, TSlashCommandSectionKeys } from "@/types";
|
||||
import { TSlashCommandAdditionalOption } from "./root";
|
||||
|
||||
export type TSlashCommandSection = {
|
||||
@@ -51,15 +46,9 @@ export type TSlashCommandSection = {
|
||||
items: ISlashCommandItem[];
|
||||
};
|
||||
|
||||
type TArgs = {
|
||||
additionalOptions?: TSlashCommandAdditionalOption[];
|
||||
disabledExtensions: TExtensions[];
|
||||
};
|
||||
|
||||
export const getSlashCommandFilteredSections =
|
||||
(args: TArgs) =>
|
||||
(additionalOptions?: TSlashCommandAdditionalOption[]) =>
|
||||
({ query }: { query: string }): TSlashCommandSection[] => {
|
||||
const { additionalOptions, disabledExtensions } = args;
|
||||
const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [
|
||||
{
|
||||
key: "general",
|
||||
@@ -190,15 +179,6 @@ export const getSlashCommandFilteredSections =
|
||||
searchTerms: ["img", "photo", "picture", "media", "upload"],
|
||||
command: ({ editor, range }: CommandProps) => insertImage({ editor, event: "insert", range }),
|
||||
},
|
||||
{
|
||||
commandKey: "callout",
|
||||
key: "callout",
|
||||
title: "Callout",
|
||||
icon: <MessageSquareText className="size-3.5" />,
|
||||
description: "Insert callout",
|
||||
searchTerms: ["callout", "comment", "message", "info", "alert"],
|
||||
command: ({ editor, range }: CommandProps) => insertCallout(editor, range),
|
||||
},
|
||||
{
|
||||
commandKey: "divider",
|
||||
key: "divider",
|
||||
@@ -289,16 +269,15 @@ export const getSlashCommandFilteredSections =
|
||||
},
|
||||
];
|
||||
|
||||
[
|
||||
...(additionalOptions ?? []),
|
||||
...coreEditorAdditionalSlashCommandOptions({
|
||||
disabledExtensions,
|
||||
}),
|
||||
]?.forEach((item) => {
|
||||
additionalOptions?.forEach((item) => {
|
||||
const sectionToPushTo = SLASH_COMMAND_SECTIONS.find((s) => s.key === item.section) ?? SLASH_COMMAND_SECTIONS[0];
|
||||
const itemIndexToPushAfter = sectionToPushTo.items.findIndex((i) => i.commandKey === item.pushAfter);
|
||||
if (itemIndexToPushAfter !== -1) {
|
||||
sectionToPushTo.items.splice(itemIndexToPushAfter + 1, 0, item);
|
||||
if (itemIndexToPushAfter !== undefined) {
|
||||
const resolvedIndex =
|
||||
itemIndexToPushAfter + 1 < sectionToPushTo.items.length
|
||||
? itemIndexToPushAfter + 1
|
||||
: sectionToPushTo.items.length - 1;
|
||||
sectionToPushTo.items.splice(resolvedIndex, 0, item);
|
||||
} else {
|
||||
sectionToPushTo.items.push(item);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ReactRenderer } from "@tiptap/react";
|
||||
import Suggestion, { SuggestionOptions } from "@tiptap/suggestion";
|
||||
import tippy from "tippy.js";
|
||||
// types
|
||||
import { ISlashCommandItem, TEditorCommands, TExtensions, TSlashCommandSectionKeys } from "@/types";
|
||||
import { ISlashCommandItem, TEditorCommands, TSlashCommandSectionKeys } from "@/types";
|
||||
// components
|
||||
import { getSlashCommandFilteredSections } from "./command-items-list";
|
||||
import { SlashCommandsMenu, SlashCommandsMenuProps } from "./command-menu";
|
||||
@@ -107,15 +107,10 @@ const renderItems = () => {
|
||||
};
|
||||
};
|
||||
|
||||
type TExtensionProps = {
|
||||
additionalOptions?: TSlashCommandAdditionalOption[];
|
||||
disabledExtensions: TExtensions[];
|
||||
};
|
||||
|
||||
export const SlashCommands = (props: TExtensionProps) =>
|
||||
export const SlashCommands = (additionalOptions?: TSlashCommandAdditionalOption[]) =>
|
||||
Command.configure({
|
||||
suggestion: {
|
||||
items: getSlashCommandFilteredSections(props),
|
||||
items: getSlashCommandFilteredSections(additionalOptions),
|
||||
render: renderItems,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -189,7 +189,3 @@ export const insertHorizontalRule = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
||||
else editor.chain().focus().setHorizontalRule().run();
|
||||
};
|
||||
export const insertCallout = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).insertCallout().run();
|
||||
else editor.chain().focus().insertCallout().run();
|
||||
};
|
||||
|
||||
@@ -75,7 +75,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
|
||||
}, [provider, id]);
|
||||
|
||||
const editor = useEditor({
|
||||
disabledExtensions,
|
||||
id,
|
||||
onTransaction,
|
||||
editorProps,
|
||||
|
||||
@@ -16,20 +16,12 @@ import { IMarking, scrollSummary, scrollToNodeViaDOMCoordinates } from "@/helper
|
||||
// props
|
||||
import { CoreEditorProps } from "@/props";
|
||||
// types
|
||||
import {
|
||||
EditorRefApi,
|
||||
IMentionHighlight,
|
||||
IMentionSuggestion,
|
||||
TEditorCommands,
|
||||
TExtensions,
|
||||
TFileHandler,
|
||||
} from "@/types";
|
||||
import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types";
|
||||
|
||||
export interface CustomEditorProps {
|
||||
editorClassName: string;
|
||||
editorProps?: EditorProps;
|
||||
enableHistory: boolean;
|
||||
disabledExtensions: TExtensions[];
|
||||
extensions?: any;
|
||||
fileHandler: TFileHandler;
|
||||
forwardedRef?: MutableRefObject<EditorRefApi | null>;
|
||||
@@ -53,7 +45,6 @@ export interface CustomEditorProps {
|
||||
|
||||
export const useEditor = (props: CustomEditorProps) => {
|
||||
const {
|
||||
disabledExtensions,
|
||||
editorClassName,
|
||||
editorProps = {},
|
||||
enableHistory,
|
||||
@@ -88,7 +79,6 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
},
|
||||
extensions: [
|
||||
...CoreEditorExtensions({
|
||||
disabledExtensions,
|
||||
enableHistory,
|
||||
fileHandler,
|
||||
mentionConfig: {
|
||||
|
||||
@@ -11,7 +11,6 @@ import { TReadOnlyCollaborativeEditorProps } from "@/types";
|
||||
|
||||
export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEditorProps) => {
|
||||
const {
|
||||
disabledExtensions,
|
||||
editorClassName,
|
||||
editorProps = {},
|
||||
extensions,
|
||||
@@ -67,7 +66,6 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit
|
||||
}, [provider, id]);
|
||||
|
||||
const editor = useReadOnlyEditor({
|
||||
disabledExtensions,
|
||||
editorProps,
|
||||
editorClassName,
|
||||
extensions: [
|
||||
|
||||
@@ -11,15 +11,14 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
|
||||
// props
|
||||
import { CoreReadOnlyEditorProps } from "@/props";
|
||||
// types
|
||||
import { EditorReadOnlyRefApi, IMentionHighlight, TExtensions, TFileHandler } from "@/types";
|
||||
import { EditorReadOnlyRefApi, IMentionHighlight, TFileHandler } from "@/types";
|
||||
|
||||
interface CustomReadOnlyEditorProps {
|
||||
disabledExtensions: TExtensions[];
|
||||
editorClassName: string;
|
||||
editorProps?: EditorProps;
|
||||
extensions?: any;
|
||||
forwardedRef?: MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
initialValue?: string;
|
||||
editorClassName: string;
|
||||
forwardedRef?: MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
extensions?: any;
|
||||
editorProps?: EditorProps;
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
mentionHandler: {
|
||||
@@ -30,7 +29,6 @@ interface CustomReadOnlyEditorProps {
|
||||
|
||||
export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||
const {
|
||||
disabledExtensions,
|
||||
initialValue,
|
||||
editorClassName,
|
||||
forwardedRef,
|
||||
@@ -56,7 +54,6 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
||||
},
|
||||
extensions: [
|
||||
...CoreReadOnlyEditorExtensions({
|
||||
disabledExtensions,
|
||||
mentionConfig: {
|
||||
mentionHighlights: mentionHandler.highlights,
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@ export type TServerHandler = {
|
||||
};
|
||||
|
||||
type TCollaborativeEditorHookProps = {
|
||||
disabledExtensions: TExtensions[];
|
||||
disabledExtensions?: TExtensions[];
|
||||
editorClassName: string;
|
||||
editorProps?: EditorProps;
|
||||
extensions?: Extensions;
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
TServerHandler,
|
||||
} from "@/types";
|
||||
import { TTextAlign } from "@/extensions";
|
||||
// plane editor types
|
||||
import { TEditorAdditionalCommands } from "@/plane-editor/types";
|
||||
|
||||
export type TEditorCommands =
|
||||
| "text"
|
||||
@@ -39,7 +41,7 @@ export type TEditorCommands =
|
||||
| "text-color"
|
||||
| "background-color"
|
||||
| "text-align"
|
||||
| "callout";
|
||||
| TEditorAdditionalCommands;
|
||||
|
||||
export type TCommandExtraProps = {
|
||||
image: {
|
||||
@@ -104,7 +106,7 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
|
||||
export interface IEditorProps {
|
||||
containerClassName?: string;
|
||||
displayConfig?: TDisplayConfig;
|
||||
disabledExtensions: TExtensions[];
|
||||
disabledExtensions?: TExtensions[];
|
||||
editorClassName?: string;
|
||||
fileHandler: TFileHandler;
|
||||
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||
@@ -121,7 +123,7 @@ export interface IEditorProps {
|
||||
onEnterKeyPress?: (e?: any) => void;
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
tabIndex?: number;
|
||||
value?: string | null;
|
||||
value?: string | null;
|
||||
}
|
||||
export interface ILiteTextEditor extends IEditorProps {
|
||||
extensions?: any[];
|
||||
@@ -146,7 +148,6 @@ export interface ICollaborativeDocumentEditor
|
||||
// read only editor props
|
||||
export interface IReadOnlyEditorProps {
|
||||
containerClassName?: string;
|
||||
disabledExtensions: TExtensions[];
|
||||
displayConfig?: TDisplayConfig;
|
||||
editorClassName?: string;
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed" | "slash-commands" | "enter-key";
|
||||
export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed" | "slash-commands"| "enter-key";
|
||||
|
||||
@@ -179,26 +179,13 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
}
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li > div {
|
||||
& > p {
|
||||
margin-top: 10px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
[data-text-color] {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
ul[data-type="taskList"] li > div > p {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li[data-checked="true"] {
|
||||
& > div > p {
|
||||
color: rgb(var(--color-text-400));
|
||||
}
|
||||
|
||||
[data-text-color] {
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
color: rgb(var(--color-text-400));
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
/* end to-do list */
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@plane/eslint-config",
|
||||
"private": true,
|
||||
"version": "0.24.0",
|
||||
"version": "0.23.1",
|
||||
"files": [
|
||||
"library.js",
|
||||
"next.js",
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from "./use-local-storage";
|
||||
export * from "./use-outside-click-detector";
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
export const getValueFromLocalStorage = (key: string, defaultValue: any) => {
|
||||
if (typeof window === undefined || typeof window === "undefined")
|
||||
return defaultValue;
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : defaultValue;
|
||||
} catch (error) {
|
||||
window.localStorage.removeItem(key);
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
export const setValueIntoLocalStorage = (key: string, value: any) => {
|
||||
if (typeof window === undefined || typeof window === "undefined")
|
||||
return false;
|
||||
try {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const useLocalStorage = <T,>(key: string, initialValue: T) => {
|
||||
const [storedValue, setStoredValue] = useState<T | null>(() =>
|
||||
getValueFromLocalStorage(key, initialValue)
|
||||
);
|
||||
|
||||
const setValue = useCallback(
|
||||
(value: T) => {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
setStoredValue(value);
|
||||
window.dispatchEvent(new Event(`local-storage:${key}`));
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
||||
const clearValue = useCallback(() => {
|
||||
window.localStorage.removeItem(key);
|
||||
setStoredValue(null);
|
||||
window.dispatchEvent(new Event(`local-storage:${key}`));
|
||||
}, [key]);
|
||||
|
||||
const reHydrate = useCallback(() => {
|
||||
const data = getValueFromLocalStorage(key, initialValue);
|
||||
setStoredValue(data);
|
||||
}, [key, initialValue]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(`local-storage:${key}`, reHydrate);
|
||||
return () => {
|
||||
window.removeEventListener(`local-storage:${key}`, reHydrate);
|
||||
};
|
||||
}, [key, reHydrate]);
|
||||
|
||||
return { storedValue, setValue, clearValue } as const;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/helpers",
|
||||
"version": "0.24.0",
|
||||
"version": "0.23.1",
|
||||
"description": "Helper functions shared across multiple apps internally",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind-config-custom",
|
||||
"version": "0.24.0",
|
||||
"version": "0.23.1",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.24.0",
|
||||
"version": "0.23.1",
|
||||
"private": true,
|
||||
"types": "./src/index.d.ts",
|
||||
"main": "./src/index.d.ts"
|
||||
|
||||
Vendored
-1
@@ -95,7 +95,6 @@ export type TIssuesResponse = {
|
||||
total_pages: number;
|
||||
extra_stats: null;
|
||||
results: TIssueResponseResults;
|
||||
total_results: number;
|
||||
};
|
||||
|
||||
export type TBulkIssueProperties = Pick<
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/typescript-config",
|
||||
"version": "0.24.0",
|
||||
"version": "0.23.1",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "0.24.0",
|
||||
"version": "0.23.1",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -8,27 +8,12 @@ type Props = {
|
||||
hideChevron?: boolean;
|
||||
indicatorElement?: React.ReactNode;
|
||||
actionItemElement?: React.ReactNode;
|
||||
className?: string;
|
||||
titleClassName?: string;
|
||||
};
|
||||
|
||||
export const CollapsibleButton: FC<Props> = (props) => {
|
||||
const {
|
||||
isOpen,
|
||||
title,
|
||||
hideChevron = false,
|
||||
indicatorElement,
|
||||
actionItemElement,
|
||||
className = "",
|
||||
titleClassName = "",
|
||||
} = props;
|
||||
const { isOpen, title, hideChevron = false, indicatorElement, actionItemElement } = props;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-3 h-12 px-2.5 py-3 border-b border-custom-border-200",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 h-12 px-2.5 py-3 border-b border-custom-border-200">
|
||||
<div className="flex items-center gap-3.5">
|
||||
<div className="flex items-center gap-3">
|
||||
{!hideChevron && (
|
||||
@@ -38,7 +23,7 @@ export const CollapsibleButton: FC<Props> = (props) => {
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<span className={cn("text-base text-custom-text-100 font-medium", titleClassName)}>{title}</span>
|
||||
<span className="text-base text-custom-text-100 font-medium">{title}</span>
|
||||
</div>
|
||||
{indicatorElement && indicatorElement}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import range from "lodash/range";
|
||||
import React from "react";
|
||||
|
||||
export const DropdownOptionsLoader = () => (
|
||||
<div className="flex flex-col gap-1 animate-pulse">
|
||||
{range(6).map((index) => (
|
||||
<div key={index} className="flex h-[1.925rem] w-full rounded px-1 py-1.5 bg-custom-background-90" />
|
||||
{Array.from({ length: 6 }, (_, i) => (
|
||||
<div key={i} className="flex h-[1.925rem] w-full rounded px-1 py-1.5 bg-custom-background-90" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const CommentFillIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
|
||||
<svg viewBox="0 0 24 24" className={`${className}`} xmlns="http://www.w3.org/2000/svg" {...rest}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.848 2.771C7.21613 2.4234 9.60649 2.24927 12 2.25C14.43 2.25 16.817 2.428 19.152 2.77C21.13 3.062 22.5 4.794 22.5 6.74V12.76C22.5 14.706 21.13 16.438 19.152 16.73C17.212 17.014 15.236 17.185 13.23 17.235C13.1303 17.2369 13.0351 17.277 12.964 17.347L8.78 21.53C8.67511 21.6348 8.54153 21.7061 8.39614 21.735C8.25074 21.7638 8.10004 21.749 7.96308 21.6923C7.82611 21.6356 7.70903 21.5395 7.62661 21.4163C7.54419 21.2931 7.50013 21.1482 7.5 21V17.045C6.61329 16.9639 5.72895 16.8585 4.848 16.729C2.87 16.439 1.5 14.705 1.5 12.759V6.741C1.5 4.795 2.87 3.061 4.848 2.771Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
export type Props = {
|
||||
className?: string;
|
||||
width?: string | number;
|
||||
height?: string | number;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export const EpicIcon: React.FC<Props> = ({ width = "16", height = "16", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0.900146 9.33203V12.0142C0.900146 12.3736 1.17392 12.6654 1.51126 12.6654H14.9557C15.1178 12.6654 15.2732 12.5968 15.3878 12.4746C15.5024 12.3525 15.5668 12.1869 15.5668 12.0142V10.3299L13.375 7.99523C13.1458 7.75134 12.8351 7.61436 12.5113 7.61436C12.1874 7.61436 11.8767 7.75134 11.6476 7.99523L10.1257 9.35919L10.2534 9.56204L11.7209 9.60056C11.7809 9.66017 11.8291 9.73206 11.8625 9.81194C11.8959 9.89181 11.9138 9.97804 11.9153 10.0655C11.9167 10.1529 11.9017 10.2397 11.8709 10.3208C11.8402 10.4019 11.7944 10.4756 11.7364 10.5374C11.6784 10.5992 11.6092 10.648 11.5332 10.6807C11.4571 10.7135 11.3756 10.7296 11.2935 10.728C11.2114 10.7265 11.1305 10.7073 11.0556 10.6717C10.9806 10.6362 10.9131 10.5848 10.8572 10.5209L10.2534 9.56204L6.60385 3.76614C6.37468 3.52226 6.11293 3.33203 5.78904 3.33203C5.46515 3.33203 5.20339 3.52226 4.97422 3.76614L0.900146 9.33203Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M11.7209 9.60056L10.2534 9.56204L10.8572 10.5209C10.9131 10.5848 10.9806 10.6362 11.0556 10.6717C11.1305 10.7073 11.2114 10.7265 11.2935 10.728C11.3756 10.7296 11.4571 10.7135 11.5332 10.6807C11.6092 10.648 11.6784 10.5992 11.7364 10.5374C11.7944 10.4756 11.8402 10.4019 11.8709 10.3208C11.9017 10.2397 11.9167 10.1529 11.9153 10.0655C11.9138 9.97804 11.8959 9.89181 11.8625 9.81194C11.8291 9.73206 11.7809 9.66017 11.7209 9.60056Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -7,15 +7,12 @@ export * from "./blocker-icon";
|
||||
export * from "./calendar-after-icon";
|
||||
export * from "./calendar-before-icon";
|
||||
export * from "./center-panel-icon";
|
||||
export * from "./comment-fill-icon";
|
||||
export * from "./create-icon";
|
||||
export * from "./dice-icon";
|
||||
export * from "./discord-icon";
|
||||
export * from "./epic-icon";
|
||||
export * from "./full-screen-panel-icon";
|
||||
export * from "./github-icon";
|
||||
export * from "./gitlab-icon";
|
||||
export * from "./info-icon";
|
||||
export * from "./layer-stack";
|
||||
export * from "./layers-icon";
|
||||
export * from "./monospace-icon";
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const InfoFillIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
|
||||
<svg viewBox="0 0 24 24" className={`${className}`} xmlns="http://www.w3.org/2000/svg" {...rest}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.25 12C2.25 6.615 6.615 2.25 12 2.25C17.385 2.25 21.75 6.615 21.75 12C21.75 17.385 17.385 21.75 12 21.75C6.615 21.75 2.25 17.385 2.25 12ZM10.956 10.558C12.102 9.985 13.393 11.021 13.082 12.264L12.373 15.1L12.415 15.08C12.5912 15.0025 12.7905 14.9958 12.9715 15.0612C13.1526 15.1265 13.3016 15.259 13.3877 15.4312C13.4737 15.6033 13.4903 15.802 13.434 15.9861C13.3777 16.1702 13.2527 16.3255 13.085 16.42L13.045 16.442C11.898 17.015 10.607 15.979 10.918 14.736L11.628 11.9L11.586 11.92C11.4975 11.9692 11.4 11.9999 11.2994 12.0104C11.1987 12.0209 11.097 12.0109 11.0003 11.981C10.9036 11.9511 10.8139 11.902 10.7367 11.8366C10.6595 11.7711 10.5964 11.6907 10.551 11.6002C10.5057 11.5098 10.4792 11.411 10.4731 11.31C10.4669 11.209 10.4813 11.1078 10.5153 11.0124C10.5493 10.9171 10.6022 10.8297 10.6709 10.7553C10.7396 10.681 10.8226 10.6214 10.915 10.58L10.956 10.558ZM12 9C12.1989 9 12.3897 8.92098 12.5303 8.78033C12.671 8.63968 12.75 8.44891 12.75 8.25C12.75 8.05109 12.671 7.86032 12.5303 7.71967C12.3897 7.57902 12.1989 7.5 12 7.5C11.8011 7.5 11.6103 7.57902 11.4697 7.71967C11.329 7.86032 11.25 8.05109 11.25 8.25C11.25 8.44891 11.329 8.63968 11.4697 8.78033C11.6103 8.92098 11.8011 9 12 9Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -3,14 +3,7 @@ import * as React from "react";
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const WorkspaceIcon: React.FC<ISvgIcons> = ({ className }) => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
role="img"
|
||||
aria-label="Workspace icon"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
|
||||
@@ -28,4 +28,3 @@ export * from "./row";
|
||||
export * from "./content-wrapper";
|
||||
export * from "./card";
|
||||
export * from "./tag";
|
||||
export * from "./tabs";
|
||||
|
||||
@@ -6,9 +6,7 @@ type Props = {
|
||||
data: any;
|
||||
noTooltip?: boolean;
|
||||
inPercentage?: boolean;
|
||||
size?: "sm" | "md" | "lg" | "xl";
|
||||
className?: string;
|
||||
barClassName?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
};
|
||||
|
||||
export const LinearProgressIndicator: React.FC<Props> = ({
|
||||
@@ -16,8 +14,6 @@ export const LinearProgressIndicator: React.FC<Props> = ({
|
||||
noTooltip = false,
|
||||
inPercentage = false,
|
||||
size = "sm",
|
||||
className = "",
|
||||
barClassName = "",
|
||||
}) => {
|
||||
const total = data.reduce((acc: any, cur: any) => acc + cur.value, 0);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -35,7 +31,7 @@ export const LinearProgressIndicator: React.FC<Props> = ({
|
||||
else
|
||||
return (
|
||||
<Tooltip key={item.id} tooltipContent={`${item.name} ${Math.round(item.value)}${inPercentage ? "%" : ""}`}>
|
||||
<div style={style} className={cn("first:rounded-l-sm last:rounded-r-sm", barClassName)} />
|
||||
<div style={style} className="first:rounded-l-sm last:rounded-r-sm" />
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
@@ -46,12 +42,13 @@ export const LinearProgressIndicator: React.FC<Props> = ({
|
||||
"h-2": size === "sm",
|
||||
"h-3": size === "md",
|
||||
"h-3.5": size === "lg",
|
||||
"h-[14px]": size === "xl",
|
||||
})}
|
||||
>
|
||||
<div className={cn("flex h-full w-full gap-[1.5px] p-[2px] bg-custom-background-90 rounded-sm", className)}>
|
||||
{bars}
|
||||
</div>
|
||||
{total === 0 ? (
|
||||
<div className="flex h-full w-full gap-[1.5px] p-[2px] bg-custom-background-90 rounded-sm">{bars}</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full gap-[1.5px] p-[2px] bg-custom-background-90 rounded-sm">{bars}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./tabs";
|
||||
@@ -1,94 +0,0 @@
|
||||
import React, { FC, Fragment } from "react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { LucideProps } from "lucide-react";
|
||||
// helpers
|
||||
import { useLocalStorage } from "@plane/helpers";
|
||||
import { cn } from "../../helpers";
|
||||
|
||||
type TabItem = {
|
||||
key: string;
|
||||
icon?: FC<LucideProps>;
|
||||
label?: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type TTabsProps = {
|
||||
tabs: TabItem[];
|
||||
storageKey: string;
|
||||
actions?: React.ReactNode;
|
||||
defaultTab?: string;
|
||||
containerClassName?: string;
|
||||
tabListContainerClassName?: string;
|
||||
tabListClassName?: string;
|
||||
tabClassName?: string;
|
||||
tabPanelClassName?: string;
|
||||
};
|
||||
|
||||
export const Tabs: FC<TTabsProps> = (props: TTabsProps) => {
|
||||
const {
|
||||
tabs,
|
||||
storageKey,
|
||||
actions,
|
||||
defaultTab = tabs[0]?.key,
|
||||
containerClassName = "",
|
||||
tabListContainerClassName = "",
|
||||
tabListClassName = "",
|
||||
tabClassName = "",
|
||||
tabPanelClassName = "",
|
||||
} = props;
|
||||
// local storage
|
||||
const { storedValue, setValue } = useLocalStorage(`tab-${storageKey}`, defaultTab);
|
||||
|
||||
const currentTabIndex = (tabKey: string): number => tabs.findIndex((tab) => tab.key === tabKey);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full">
|
||||
<Tab.Group defaultIndex={currentTabIndex(storedValue ?? defaultTab)}>
|
||||
<div className={cn("flex flex-col w-full h-full gap-2", containerClassName)}>
|
||||
<div className={cn("flex w-full items-center gap-4", tabListContainerClassName)}>
|
||||
<Tab.List
|
||||
as="div"
|
||||
className={cn(
|
||||
"flex w-full min-w-fit items-center justify-between gap-1.5 rounded-md text-sm p-0.5 bg-custom-background-80/60",
|
||||
tabListClassName
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
cn(
|
||||
`flex items-center justify-center p-1 min-w-fit w-full font-medium text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all rounded`,
|
||||
selected
|
||||
? "bg-custom-background-100 text-custom-text-100 shadow-sm"
|
||||
: tab.disabled
|
||||
? "text-custom-text-400 cursor-not-allowed"
|
||||
: "text-custom-text-400 hover:text-custom-text-300 hover:bg-custom-background-80/60",
|
||||
tabClassName
|
||||
)
|
||||
}
|
||||
key={tab.key}
|
||||
onClick={() => {
|
||||
if (!tab.disabled) setValue(tab.key);
|
||||
}}
|
||||
disabled={tab.disabled}
|
||||
>
|
||||
{tab.icon && <tab.icon className="size-4" />}
|
||||
{tab.label}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
{actions && <div className="flex-grow">{actions}</div>}
|
||||
</div>
|
||||
<Tab.Panels as={Fragment}>
|
||||
{tabs.map((tab) => (
|
||||
<Tab.Panel key={tab.key} as="div" className={cn("relative outline-none", tabPanelClassName)}>
|
||||
{tab.content}
|
||||
</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
</div>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -10,8 +10,7 @@ import { isCommentEmpty } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useMention } from "@/hooks/use-mention";
|
||||
|
||||
interface LiteTextEditorWrapperProps
|
||||
extends Omit<ILiteTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
|
||||
interface LiteTextEditorWrapperProps extends Omit<ILiteTextEditor, "fileHandler" | "mentionHandler"> {
|
||||
anchor: string;
|
||||
workspaceId: string;
|
||||
isSubmitting?: boolean;
|
||||
@@ -42,7 +41,6 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
||||
<div className="border border-custom-border-200 rounded p-3 space-y-3">
|
||||
<LiteTextEditorWithRef
|
||||
ref={ref}
|
||||
disabledExtensions={[]}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
anchor,
|
||||
uploadFile,
|
||||
|
||||
@@ -7,10 +7,7 @@ import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useMention } from "@/hooks/use-mention";
|
||||
|
||||
type LiteTextReadOnlyEditorWrapperProps = Omit<
|
||||
ILiteTextReadOnlyEditor,
|
||||
"disabledExtensions" | "fileHandler" | "mentionHandler"
|
||||
> & {
|
||||
type LiteTextReadOnlyEditorWrapperProps = Omit<ILiteTextReadOnlyEditor, "fileHandler" | "mentionHandler"> & {
|
||||
anchor: string;
|
||||
};
|
||||
|
||||
@@ -21,7 +18,6 @@ export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Lit
|
||||
return (
|
||||
<LiteTextReadOnlyEditorWithRef
|
||||
ref={ref}
|
||||
disabledExtensions={[]}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
anchor,
|
||||
})}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { forwardRef } from "react";
|
||||
// editor
|
||||
import { EditorRefApi, IMentionHighlight, IRichTextEditor, RichTextEditorWithRef } from "@plane/editor";
|
||||
// types
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
|
||||
interface RichTextEditorWrapperProps
|
||||
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
|
||||
interface RichTextEditorWrapperProps extends Omit<IRichTextEditor, "fileHandler" | "mentionHandler"> {
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
}
|
||||
|
||||
@@ -26,7 +27,6 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
||||
suggestions: undefined,
|
||||
}}
|
||||
ref={ref}
|
||||
disabledExtensions={[]}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
uploadFile,
|
||||
workspaceId: "",
|
||||
|
||||
@@ -7,10 +7,7 @@ import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useMention } from "@/hooks/use-mention";
|
||||
|
||||
type RichTextReadOnlyEditorWrapperProps = Omit<
|
||||
IRichTextReadOnlyEditor,
|
||||
"disabledExtensions" | "fileHandler" | "mentionHandler"
|
||||
> & {
|
||||
type RichTextReadOnlyEditorWrapperProps = Omit<IRichTextReadOnlyEditor, "fileHandler" | "mentionHandler"> & {
|
||||
anchor: string;
|
||||
};
|
||||
|
||||
@@ -21,7 +18,6 @@ export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Ric
|
||||
return (
|
||||
<RichTextReadOnlyEditorWithRef
|
||||
ref={ref}
|
||||
disabledExtensions={[]}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
anchor,
|
||||
})}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "0.24.0",
|
||||
"version": "0.23.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
// components
|
||||
import { NotificationsSidebar } from "@/plane-web/components/workspace-notifications";
|
||||
import { NotificationsSidebar } from "@/components/workspace-notifications";
|
||||
|
||||
export default function ProjectInboxIssuesLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
@@ -46,6 +46,7 @@ export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects`}
|
||||
label={currentProjectDetails?.name ?? "Project"}
|
||||
icon={
|
||||
currentProjectDetails && (
|
||||
|
||||
+1
@@ -38,6 +38,7 @@ export const ProjectArchivedIssueDetailsHeader = observer(() => {
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects`}
|
||||
label={currentProjectDetails?.name ?? "Project"}
|
||||
icon={
|
||||
currentProjectDetails && (
|
||||
|
||||
+1
@@ -172,6 +172,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
<span className="hidden md:block">
|
||||
<BreadcrumbLink
|
||||
label={currentProjectDetails?.name ?? "Project"}
|
||||
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
||||
icon={
|
||||
currentProjectDetails && (
|
||||
<span className="grid h-4 w-4 flex-shrink-0 place-items-center">
|
||||
|
||||
+1
@@ -38,6 +38,7 @@ export const CyclesListHeader: FC = observer(() => {
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
label={currentProjectDetails?.name ?? "Project"}
|
||||
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
||||
icon={
|
||||
currentProjectDetails && (
|
||||
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
|
||||
|
||||
+1
@@ -99,6 +99,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects`}
|
||||
label={currentProjectDetails?.name ?? "Project"}
|
||||
icon={
|
||||
currentProjectDetails && (
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ export const ProjectIssueDetailsHeader = observer(() => {
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects`}
|
||||
label={currentProjectDetails?.name ?? "Project"}
|
||||
icon={
|
||||
currentProjectDetails && (
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ export const ProjectIssuesHeader = observer(() => {
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects`}
|
||||
label={currentProjectDetails?.name ?? "Project"}
|
||||
icon={
|
||||
currentProjectDetails ? (
|
||||
|
||||
+1
@@ -171,6 +171,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
<span className="hidden md:block">
|
||||
<BreadcrumbLink
|
||||
label={currentProjectDetails?.name ?? "Project"}
|
||||
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
||||
icon={
|
||||
currentProjectDetails && (
|
||||
<span className="grid h-4 w-4 flex-shrink-0 place-items-center">
|
||||
|
||||
+1
@@ -39,6 +39,7 @@ export const ModulesListHeader: React.FC = observer(() => {
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
||||
label={currentProjectDetails?.name ?? "Project"}
|
||||
icon={
|
||||
currentProjectDetails && (
|
||||
|
||||
+1
@@ -77,6 +77,7 @@ export const PageDetailsHeader = observer(() => {
|
||||
<span>
|
||||
<span className="hidden md:block">
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
||||
label={currentProjectDetails?.name ?? "Project"}
|
||||
icon={
|
||||
currentProjectDetails && (
|
||||
|
||||
+13
-3
@@ -13,7 +13,9 @@ import { BreadcrumbLink, Logo } from "@/components/common";
|
||||
// constants
|
||||
import { EPageAccess } from "@/constants/page";
|
||||
// hooks
|
||||
import { useEventTracker, useProject, useProjectPages } from "@/hooks/store";
|
||||
import { useEventTracker, useProject, useProjectPages, useUserPermissions } from "@/hooks/store";
|
||||
// plane web hooks
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
export const PagesListHeader = observer(() => {
|
||||
// states
|
||||
@@ -24,9 +26,16 @@ export const PagesListHeader = observer(() => {
|
||||
const searchParams = useSearchParams();
|
||||
const pageType = searchParams.get("type");
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const { canCurrentUserCreatePage, createPage } = useProjectPages();
|
||||
const { createPage } = useProjectPages();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
// auth
|
||||
const canUserCreatePage = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
// handle page create
|
||||
const handleCreatePage = async () => {
|
||||
setIsCreatingPage(true);
|
||||
@@ -60,6 +69,7 @@ export const PagesListHeader = observer(() => {
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
||||
label={currentProjectDetails?.name ?? "Project"}
|
||||
icon={
|
||||
currentProjectDetails && (
|
||||
@@ -78,7 +88,7 @@ export const PagesListHeader = observer(() => {
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
{canCurrentUserCreatePage ? (
|
||||
{canUserCreatePage ? (
|
||||
<Header.RightItem>
|
||||
<Button variant="primary" size="sm" onClick={handleCreatePage} loading={isCreatingPage}>
|
||||
{isCreatingPage ? "Adding" : "Add page"}
|
||||
|
||||
@@ -34,6 +34,7 @@ export const ProjectSettingHeader: FC = observer(() => {
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
||||
label={currentProjectDetails?.name ?? "Project"}
|
||||
icon={
|
||||
currentProjectDetails && (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user