Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a71ff8f0d9 | ||
|
|
f42eeec2c0 | ||
|
|
e679dc3d12 | ||
|
|
f2edf637de | ||
|
|
7437deaa86 | ||
|
|
64b95daff4 | ||
|
|
34181fba80 | ||
|
|
0358e9b965 | ||
|
|
ee471c772a | ||
|
|
e2c0d0f23c | ||
|
|
3ecebc02ae | ||
|
|
16d531cc7a | ||
|
|
ab283c7c78 | ||
|
|
7c1bbf4a6f | ||
|
|
4293892178 | ||
|
|
ebd517bb7d | ||
|
|
4042af9f32 | ||
|
|
258d24bf06 | ||
|
|
489a6e1e94 | ||
|
|
4f349807be | ||
|
|
7a43137620 | ||
|
|
e144ce8cf2 | ||
|
|
ba7303b7af | ||
|
|
e0912ccefc | ||
|
|
aef465415b | ||
|
|
3b3bd3e54e | ||
|
|
f2fabff10a | ||
|
|
ddeabeeeb1 | ||
|
|
a89bee8975 | ||
|
|
a986068e9f | ||
|
|
d8ae355314 |
@@ -17,8 +17,6 @@ jobs:
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Get PR Branch version
|
||||
run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
|
||||
|
||||
@@ -36,8 +36,6 @@ jobs:
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Enable Corepack and pnpm
|
||||
run: corepack enable pnpm
|
||||
|
||||
@@ -14,7 +14,7 @@ strict-peer-dependencies=false
|
||||
# Turbo occasionally performs postinstall tasks for optimal performance
|
||||
# moved to pnpm-workspace.yaml: onlyBuiltDependencies (e.g., allow turbo)
|
||||
|
||||
public-hoist-pattern[]=eslint
|
||||
public-hoist-pattern[]=*eslint*
|
||||
public-hoist-pattern[]=prettier
|
||||
public-hoist-pattern[]=typescript
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"description": "Admin UI for Plane",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -31,11 +31,11 @@
|
||||
"lucide-react": "^0.469.0",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"next": "14.2.30",
|
||||
"next": "14.2.32",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.49",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-hook-form": "7.51.5",
|
||||
"sharp": "^0.33.5",
|
||||
"swr": "^2.2.4",
|
||||
@@ -45,11 +45,11 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"typescript": "5.8.3"
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"description": "API server powering Plane's backend"
|
||||
|
||||
@@ -91,6 +91,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
"project_lead": UserLiteSerializer,
|
||||
"state": StateLiteSerializer,
|
||||
"created_by": UserLiteSerializer,
|
||||
"updated_by": UserLiteSerializer,
|
||||
"issue": IssueSerializer,
|
||||
"actor": UserLiteSerializer,
|
||||
"owned_by": UserLiteSerializer,
|
||||
|
||||
@@ -441,7 +441,11 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
|
||||
signed_url = storage.generate_presigned_url(
|
||||
object_name=asset.asset.name,
|
||||
disposition="attachment",
|
||||
filename=asset.attributes.get("name"),
|
||||
)
|
||||
# Redirect to the signed URL
|
||||
return HttpResponseRedirect(signed_url)
|
||||
|
||||
@@ -641,7 +645,11 @@ class ProjectAssetEndpoint(BaseAPIView):
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
|
||||
signed_url = storage.generate_presigned_url(
|
||||
object_name=asset.asset.name,
|
||||
disposition="attachment",
|
||||
filename=asset.attributes.get("name"),
|
||||
)
|
||||
# Redirect to the signed URL
|
||||
return HttpResponseRedirect(signed_url)
|
||||
|
||||
|
||||
@@ -198,6 +198,7 @@ class PageViewSet(BaseViewSet):
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
page = self.get_queryset().filter(pk=pk).first()
|
||||
project = Project.objects.get(pk=project_id)
|
||||
track_visit = request.query_params.get("track_visit", "true").lower() == "true"
|
||||
|
||||
"""
|
||||
if the role is guest and guest_view_all_features is false and owned by is not
|
||||
@@ -230,13 +231,14 @@ class PageViewSet(BaseViewSet):
|
||||
).values_list("entity_identifier", flat=True)
|
||||
data = PageDetailSerializer(page).data
|
||||
data["issue_ids"] = issue_ids
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
entity_name="page",
|
||||
entity_identifier=pk,
|
||||
user_id=request.user.id,
|
||||
project_id=project_id,
|
||||
)
|
||||
if track_visit:
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
entity_name="page",
|
||||
entity_identifier=pk,
|
||||
user_id=request.user.id,
|
||||
project_id=project_id,
|
||||
)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
|
||||
|
||||
@@ -172,12 +172,14 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
{"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
project_id = request.data.get("project_id", issue.project_id)
|
||||
|
||||
serializer = DraftIssueCreateSerializer(
|
||||
issue,
|
||||
data=request.data,
|
||||
partial=True,
|
||||
context={
|
||||
"project_id": request.data.get("project_id", None),
|
||||
"project_id": project_id,
|
||||
"cycle_id": request.data.get("cycle_id", "not_provided"),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ def flush_to_mongo_and_delete(
|
||||
mongo_archival_failed = False
|
||||
|
||||
# Try to insert into MongoDB if available
|
||||
if mongo_collection and mongo_available:
|
||||
if mongo_collection is not None and mongo_available:
|
||||
try:
|
||||
mongo_collection.bulk_write([InsertOne(doc) for doc in buffer])
|
||||
except BulkWriteError as bwe:
|
||||
@@ -166,9 +166,9 @@ def process_cleanup_task(
|
||||
def transform_api_log(record: Dict) -> Dict:
|
||||
"""Transform API activity log record."""
|
||||
return {
|
||||
"id": record["id"],
|
||||
"id": str(record["id"]),
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"token_identifier": record["token_identifier"],
|
||||
"token_identifier": str(record["token_identifier"]),
|
||||
"path": record["path"],
|
||||
"method": record["method"],
|
||||
"query_params": record.get("query_params"),
|
||||
@@ -178,18 +178,18 @@ def transform_api_log(record: Dict) -> Dict:
|
||||
"response_body": record["response_body"],
|
||||
"ip_address": record["ip_address"],
|
||||
"user_agent": record["user_agent"],
|
||||
"created_by_id": record["created_by_id"],
|
||||
"created_by_id": str(record["created_by_id"]),
|
||||
}
|
||||
|
||||
|
||||
def transform_email_log(record: Dict) -> Dict:
|
||||
"""Transform email notification log record."""
|
||||
return {
|
||||
"id": record["id"],
|
||||
"id": str(record["id"]),
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"receiver_id": record["receiver_id"],
|
||||
"triggered_by_id": record["triggered_by_id"],
|
||||
"entity_identifier": record["entity_identifier"],
|
||||
"receiver_id": str(record["receiver_id"]),
|
||||
"triggered_by_id": str(record["triggered_by_id"]),
|
||||
"entity_identifier": str(record["entity_identifier"]),
|
||||
"entity_name": record["entity_name"],
|
||||
"data": record["data"],
|
||||
"processed_at": (
|
||||
@@ -197,27 +197,27 @@ def transform_email_log(record: Dict) -> Dict:
|
||||
),
|
||||
"sent_at": str(record["sent_at"]) if record.get("sent_at") else None,
|
||||
"entity": record["entity"],
|
||||
"old_value": record["old_value"],
|
||||
"new_value": record["new_value"],
|
||||
"created_by_id": record["created_by_id"],
|
||||
"old_value": str(record["old_value"]),
|
||||
"new_value": str(record["new_value"]),
|
||||
"created_by_id": str(record["created_by_id"]),
|
||||
}
|
||||
|
||||
|
||||
def transform_page_version(record: Dict) -> Dict:
|
||||
"""Transform page version record."""
|
||||
return {
|
||||
"id": record["id"],
|
||||
"id": str(record["id"]),
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"page_id": record["page_id"],
|
||||
"workspace_id": record["workspace_id"],
|
||||
"owned_by_id": record["owned_by_id"],
|
||||
"page_id": str(record["page_id"]),
|
||||
"workspace_id": str(record["workspace_id"]),
|
||||
"owned_by_id": str(record["owned_by_id"]),
|
||||
"description_html": record["description_html"],
|
||||
"description_binary": record["description_binary"],
|
||||
"description_stripped": record["description_stripped"],
|
||||
"description_json": record["description_json"],
|
||||
"sub_pages_data": record["sub_pages_data"],
|
||||
"created_by_id": record["created_by_id"],
|
||||
"updated_by_id": record["updated_by_id"],
|
||||
"created_by_id": str(record["created_by_id"]),
|
||||
"updated_by_id": str(record["updated_by_id"]),
|
||||
"deleted_at": str(record["deleted_at"]) if record.get("deleted_at") else None,
|
||||
"last_saved_at": (
|
||||
str(record["last_saved_at"]) if record.get("last_saved_at") else None
|
||||
@@ -228,14 +228,14 @@ def transform_page_version(record: Dict) -> Dict:
|
||||
def transform_issue_description_version(record: Dict) -> Dict:
|
||||
"""Transform issue description version record."""
|
||||
return {
|
||||
"id": record["id"],
|
||||
"id": str(record["id"]),
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"issue_id": record["issue_id"],
|
||||
"workspace_id": record["workspace_id"],
|
||||
"project_id": record["project_id"],
|
||||
"created_by_id": record["created_by_id"],
|
||||
"updated_by_id": record["updated_by_id"],
|
||||
"owned_by_id": record["owned_by_id"],
|
||||
"issue_id": str(record["issue_id"]),
|
||||
"workspace_id": str(record["workspace_id"]),
|
||||
"project_id": str(record["project_id"]),
|
||||
"created_by_id": str(record["created_by_id"]),
|
||||
"updated_by_id": str(record["updated_by_id"]),
|
||||
"owned_by_id": str(record["owned_by_id"]),
|
||||
"last_saved_at": (
|
||||
str(record["last_saved_at"]) if record.get("last_saved_at") else None
|
||||
),
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.2.22 on 2025-08-29 11:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0101_description_descriptionversion"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="page",
|
||||
name="sort_order",
|
||||
field=models.FloatField(default=65535),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="pagelog",
|
||||
name="entity_type",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=30, null=True, verbose_name="Entity Type"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="pagelog",
|
||||
name="entity_identifier",
|
||||
field=models.UUIDField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -57,6 +57,7 @@ class Page(BaseModel):
|
||||
)
|
||||
moved_to_page = models.UUIDField(null=True, blank=True)
|
||||
moved_to_project = models.UUIDField(null=True, blank=True)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
|
||||
external_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
@@ -98,8 +99,9 @@ class PageLog(BaseModel):
|
||||
)
|
||||
transaction = models.UUIDField(default=uuid.uuid4)
|
||||
page = models.ForeignKey(Page, related_name="page_log", on_delete=models.CASCADE)
|
||||
entity_identifier = models.UUIDField(null=True)
|
||||
entity_identifier = models.UUIDField(null=True, blank=True)
|
||||
entity_name = models.CharField(max_length=30, verbose_name="Transaction Type")
|
||||
entity_type = models.CharField(max_length=30, verbose_name="Entity Type", null=True, blank=True)
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_page_log"
|
||||
)
|
||||
|
||||
@@ -465,3 +465,7 @@ if ENABLE_DRF_SPECTACULAR:
|
||||
REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema"
|
||||
INSTALLED_APPS.append("drf_spectacular")
|
||||
from .openapi import SPECTACULAR_SETTINGS # noqa: F401
|
||||
|
||||
# MongoDB Settings
|
||||
MONGO_DB_URL = os.environ.get("MONGO_DB_URL", False)
|
||||
MONGO_DB_DATABASE = os.environ.get("MONGO_DB_DATABASE", False)
|
||||
|
||||
@@ -118,4 +118,7 @@ class MongoConnection:
|
||||
Returns:
|
||||
bool: True if MongoDB is configured and connected, False otherwise
|
||||
"""
|
||||
|
||||
if cls._client is None:
|
||||
cls._instance = cls()
|
||||
return cls._client is not None and cls._db is not None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "live",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "A realtime collaborative server powers Plane's rich text editor",
|
||||
"main": "./src/server.ts",
|
||||
@@ -58,8 +58,8 @@
|
||||
"concurrently": "^9.0.1",
|
||||
"nodemon": "^3.1.7",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3",
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// plane editor
|
||||
import {
|
||||
getAllDocumentFormatsFromDocumentEditorBinaryData,
|
||||
getAllDocumentFormatsFromRichTextEditorBinaryData,
|
||||
getBinaryDataFromDocumentEditorHTMLString,
|
||||
getBinaryDataFromRichTextEditorHTMLString,
|
||||
} from "@plane/editor";
|
||||
// plane types
|
||||
import { TDocumentPayload } from "@plane/types";
|
||||
|
||||
type TArgs = {
|
||||
document_html: string;
|
||||
variant: "rich" | "document";
|
||||
};
|
||||
|
||||
export const convertHTMLDocumentToAllFormats = (args: TArgs): TDocumentPayload => {
|
||||
const { document_html, variant } = args;
|
||||
|
||||
let allFormats: TDocumentPayload;
|
||||
|
||||
if (variant === "rich") {
|
||||
const contentBinary = getBinaryDataFromRichTextEditorHTMLString(document_html);
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
||||
getAllDocumentFormatsFromRichTextEditorBinaryData(contentBinary);
|
||||
allFormats = {
|
||||
description: contentJSON,
|
||||
description_html: contentHTML,
|
||||
description_binary: contentBinaryEncoded,
|
||||
};
|
||||
} else if (variant === "document") {
|
||||
const contentBinary = getBinaryDataFromDocumentEditorHTMLString(document_html);
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
||||
getAllDocumentFormatsFromDocumentEditorBinaryData(contentBinary);
|
||||
allFormats = {
|
||||
description: contentJSON,
|
||||
description_html: contentHTML,
|
||||
description_binary: contentBinaryEncoded,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Invalid variant provided: ${variant}`);
|
||||
}
|
||||
|
||||
return allFormats;
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import { getSchema } from "@tiptap/core";
|
||||
import { generateHTML, generateJSON } from "@tiptap/html";
|
||||
import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
|
||||
import * as Y from "yjs";
|
||||
// plane editor
|
||||
import { CoreEditorExtensionsWithoutProps, DocumentEditorExtensionsWithoutProps } from "@plane/editor/lib";
|
||||
|
||||
const DOCUMENT_EDITOR_EXTENSIONS = [...CoreEditorExtensionsWithoutProps, ...DocumentEditorExtensionsWithoutProps];
|
||||
const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
|
||||
|
||||
export const getAllDocumentFormatsFromBinaryData = (
|
||||
description: Uint8Array
|
||||
): {
|
||||
contentBinaryEncoded: string;
|
||||
contentJSON: object;
|
||||
contentHTML: string;
|
||||
} => {
|
||||
// encode binary description data
|
||||
const base64Data = Buffer.from(description).toString("base64");
|
||||
const yDoc = new Y.Doc();
|
||||
Y.applyUpdate(yDoc, description);
|
||||
// convert to JSON
|
||||
const type = yDoc.getXmlFragment("default");
|
||||
const contentJSON = yXmlFragmentToProseMirrorRootNode(type, documentEditorSchema).toJSON();
|
||||
// convert to HTML
|
||||
const contentHTML = generateHTML(contentJSON, DOCUMENT_EDITOR_EXTENSIONS);
|
||||
|
||||
return {
|
||||
contentBinaryEncoded: base64Data,
|
||||
contentJSON,
|
||||
contentHTML,
|
||||
};
|
||||
};
|
||||
|
||||
export const getBinaryDataFromHTMLString = (
|
||||
descriptionHTML: string
|
||||
): {
|
||||
contentBinary: Uint8Array;
|
||||
} => {
|
||||
// convert HTML to JSON
|
||||
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", DOCUMENT_EDITOR_EXTENSIONS);
|
||||
// convert JSON to Y.Doc format
|
||||
const transformedData = prosemirrorJSONToYDoc(documentEditorSchema, contentJSON, "default");
|
||||
// convert Y.Doc to Uint8Array format
|
||||
const encodedData = Y.encodeStateAsUpdate(transformedData);
|
||||
|
||||
return {
|
||||
contentBinary: encodedData,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
// helpers
|
||||
import { getAllDocumentFormatsFromBinaryData, getBinaryDataFromHTMLString } from "@/core/helpers/page.js";
|
||||
import { getAllDocumentFormatsFromBinaryData, getBinaryDataFromHTMLString } from "@plane/editor/lib";
|
||||
// services
|
||||
import { PageService } from "@/core/services/page.service.js";
|
||||
import { manualLogger } from "../helpers/logger.js";
|
||||
@@ -19,7 +19,9 @@ export const updatePageDescription = async (
|
||||
const projectId = params.get("projectId")?.toString();
|
||||
if (!workspaceSlug || !projectId || !cookie) return;
|
||||
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromBinaryData(updatedDescription);
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromBinaryData({
|
||||
descriptionBinary: updatedDescription,
|
||||
});
|
||||
try {
|
||||
const payload = {
|
||||
description_binary: contentBinaryEncoded,
|
||||
@@ -44,7 +46,9 @@ const fetchDescriptionHTMLAndTransform = async (
|
||||
|
||||
try {
|
||||
const pageDetails = await pageService.fetchDetails(workspaceSlug, projectId, pageId, cookie);
|
||||
const { contentBinary } = getBinaryDataFromHTMLString(pageDetails.description_html ?? "<p></p>");
|
||||
const contentBinary = getBinaryDataFromHTMLString({
|
||||
descriptionHTML: pageDetails.description_html ?? "<p></p>",
|
||||
});
|
||||
return contentBinary;
|
||||
} catch (error) {
|
||||
manualLogger.error("Error while transforming from HTML to Uint8Array", error);
|
||||
|
||||
@@ -5,11 +5,11 @@ import expressWs from "express-ws";
|
||||
import helmet from "helmet";
|
||||
// hocuspocus server
|
||||
// helpers
|
||||
import { convertHTMLDocumentToAllFormats } from "@/core/helpers/convert-document.js";
|
||||
import { logger, manualLogger } from "@/core/helpers/logger.js";
|
||||
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
|
||||
// types
|
||||
import { TConvertDocumentRequestBody } from "@/core/types/common.js";
|
||||
import { getAllDocumentFormatsFromHTMLString } from "@plane/editor/lib";
|
||||
|
||||
export class Server {
|
||||
private app: any;
|
||||
@@ -72,9 +72,8 @@ export class Server {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { description, description_binary } = convertHTMLDocumentToAllFormats({
|
||||
document_html: description_html,
|
||||
variant,
|
||||
const { description, description_binary } = getAllDocumentFormatsFromHTMLString({
|
||||
descriptionHTML: description_html,
|
||||
});
|
||||
res.status(200).json({
|
||||
description,
|
||||
|
||||
@@ -58,7 +58,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
||||
flaggedExtensions={richTextEditorExtensions.flagged}
|
||||
{...rest}
|
||||
containerClassName={containerClassName}
|
||||
editorClassName="min-h-[100px] max-h-[200px] border-[0.5px] border-custom-border-300 rounded-md pl-3 py-2 overflow-hidden"
|
||||
editorClassName="min-h-[100px] py-2 overflow-hidden"
|
||||
displayConfig={{ fontSize: "large-font" }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -41,11 +41,11 @@
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"mobx-utils": "^6.0.8",
|
||||
"next": "14.2.30",
|
||||
"next": "14.2.32",
|
||||
"next-themes": "^0.2.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "7.51.5",
|
||||
"react-popper": "^2.3.0",
|
||||
@@ -58,13 +58,12 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "18.14.1",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.36.0",
|
||||
"typescript": "5.8.3"
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ export const ListGroup = observer((props: Props) => {
|
||||
isWorkflowDropDisabled,
|
||||
]);
|
||||
|
||||
const isDragAllowed = !!group_by && DRAG_ALLOWED_GROUPS.includes(group_by);
|
||||
const isDragAllowed = group_by ? DRAG_ALLOWED_GROUPS.includes(group_by) : true;
|
||||
const canOverlayBeVisible = isWorkflowDropDisabled || orderBy !== "sort_order" || !!group.isDropDisabled;
|
||||
const isDropDisabled = isWorkflowDropDisabled || !!group.isDropDisabled;
|
||||
|
||||
|
||||
@@ -521,7 +521,7 @@ export const handleGroupDragDrop = async (
|
||||
subGroupBy: TIssueGroupByOptions | undefined,
|
||||
shouldAddIssueAtTop = false
|
||||
) => {
|
||||
if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return;
|
||||
if (!source.id || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return;
|
||||
|
||||
let updatedIssue: Partial<TIssue> = {};
|
||||
const issueUpdates: IssueUpdates = {};
|
||||
@@ -549,7 +549,7 @@ export const handleGroupDragDrop = async (
|
||||
};
|
||||
|
||||
// update updatedIssue values based on the source and destination groupIds
|
||||
if (source.groupId && destination.groupId && source.groupId !== destination.groupId) {
|
||||
if (source.groupId && destination.groupId && source.groupId !== destination.groupId && groupBy) {
|
||||
const groupKey = ISSUE_FILTER_DEFAULT_DATA[groupBy];
|
||||
let groupValue: any = clone(sourceIssue[groupKey]);
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ export type TBasePaidPlanCardProps = {
|
||||
extraFeatures?: string | React.ReactNode;
|
||||
renderPriceContent: (price: TSubscriptionPrice) => React.ReactNode;
|
||||
renderActionButton: (price: TSubscriptionPrice) => React.ReactNode;
|
||||
isSelfHosted: boolean;
|
||||
};
|
||||
|
||||
export const BasePaidPlanCard: FC<TBasePaidPlanCardProps> = observer((props) => {
|
||||
@@ -31,11 +30,10 @@ export const BasePaidPlanCard: FC<TBasePaidPlanCardProps> = observer((props) =>
|
||||
extraFeatures,
|
||||
renderPriceContent,
|
||||
renderActionButton,
|
||||
isSelfHosted,
|
||||
} = props;
|
||||
// states
|
||||
const [selectedPlan, setSelectedPlan] = useState<TBillingFrequency>("month");
|
||||
const basePlan = getBaseSubscriptionName(planVariant, isSelfHosted);
|
||||
const basePlan = getBaseSubscriptionName(planVariant);
|
||||
const upgradeCardVariantStyle = getUpgradeCardVariantStyle(planVariant);
|
||||
// Plane details
|
||||
const planeName = getSubscriptionName(planVariant);
|
||||
|
||||
@@ -107,7 +107,6 @@ export const PlanUpgradeCard: FC<PlanUpgradeCardProps> = observer((props) => {
|
||||
isTrialAllowed={isTrialAllowed}
|
||||
/>
|
||||
)}
|
||||
isSelfHosted={isSelfHosted}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -107,7 +107,6 @@ export const TalkToSalesCard: FC<TalkToSalesCardProps> = observer((props) => {
|
||||
extraFeatures={extraFeatures}
|
||||
renderPriceContent={renderPriceContent}
|
||||
renderActionButton={renderActionButton}
|
||||
isSelfHosted={isSelfHosted}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel, EPageAccess, PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import {
|
||||
EUserPermissionsLevel,
|
||||
EPageAccess,
|
||||
PROJECT_PAGE_TRACKER_ELEMENTS,
|
||||
PROJECT_PAGE_TRACKER_EVENTS,
|
||||
} from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EUserProjectRoles, TPageNavigationTabs } from "@plane/types";
|
||||
import { EUserProjectRoles, TPage, TPageNavigationTabs } from "@plane/types";
|
||||
// components
|
||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
|
||||
import { PageLoader } from "@/components/pages/loaders/page-loader";
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { captureClick, captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
// plane web hooks
|
||||
@@ -25,10 +34,16 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { loader, isAnyPageAvailable, getCurrentProjectFilteredPageIdsByTab, getCurrentProjectPageIdsByTab, filters } =
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const { isAnyPageAvailable, getCurrentProjectFilteredPageIdsByTab, getCurrentProjectPageIdsByTab, filters } =
|
||||
usePageStore(storeType);
|
||||
const { toggleCreatePageModal } = useCommandPalette();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { createPage } = usePageStore(EPageStoreType.PROJECT);
|
||||
// states
|
||||
const [isCreatingPage, setIsCreatingPage] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = useParams();
|
||||
// derived values
|
||||
const pageIds = getCurrentProjectPageIdsByTab(pageType);
|
||||
const filteredPageIds = getCurrentProjectFilteredPageIdsByTab(pageType);
|
||||
@@ -54,6 +69,42 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
||||
extension: "svg",
|
||||
});
|
||||
|
||||
// handle page create
|
||||
const handleCreatePage = async () => {
|
||||
setIsCreatingPage(true);
|
||||
|
||||
const payload: Partial<TPage> = {
|
||||
access: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC,
|
||||
};
|
||||
|
||||
await createPage(payload)
|
||||
.then((res) => {
|
||||
captureSuccess({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
|
||||
payload: {
|
||||
id: res?.id,
|
||||
state: "SUCCESS",
|
||||
},
|
||||
});
|
||||
const pageId = `/${workspaceSlug}/projects/${currentProjectDetails?.id}/pages/${res?.id}`;
|
||||
router.push(pageId);
|
||||
})
|
||||
.catch((err) => {
|
||||
captureError({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
|
||||
payload: {
|
||||
state: "ERROR",
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: err?.data?.error || "Page could not be created. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => setIsCreatingPage(false));
|
||||
};
|
||||
|
||||
if (loader === "init-loader") return <PageLoader />;
|
||||
// if no pages exist in the active page type
|
||||
if (!isAnyPageAvailable || pageIds?.length === 0) {
|
||||
@@ -64,12 +115,12 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
||||
description={t("project_page.empty_state.general.description")}
|
||||
assetPath={generalPageResolvedPath}
|
||||
primaryButton={{
|
||||
text: t("project_page.empty_state.general.primary_button.text"),
|
||||
text: isCreatingPage ? t("creating") : t("project_page.empty_state.general.primary_button.text"),
|
||||
onClick: () => {
|
||||
toggleCreatePageModal({ isOpen: true });
|
||||
handleCreatePage();
|
||||
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
disabled: !canPerformEmptyStateActions || isCreatingPage,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -81,12 +132,12 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
||||
description={t("project_page.empty_state.public.description")}
|
||||
assetPath={publicPageResolvedPath}
|
||||
primaryButton={{
|
||||
text: t("project_page.empty_state.public.primary_button.text"),
|
||||
text: isCreatingPage ? t("creating") : t("project_page.empty_state.public.primary_button.text"),
|
||||
onClick: () => {
|
||||
toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PUBLIC });
|
||||
handleCreatePage();
|
||||
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
disabled: !canPerformEmptyStateActions || isCreatingPage,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -97,12 +148,12 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
||||
description={t("project_page.empty_state.private.description")}
|
||||
assetPath={privatePageResolvedPath}
|
||||
primaryButton={{
|
||||
text: t("project_page.empty_state.private.primary_button.text"),
|
||||
text: isCreatingPage ? t("creating") : t("project_page.empty_state.private.primary_button.text"),
|
||||
onClick: () => {
|
||||
toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PRIVATE });
|
||||
handleCreatePage();
|
||||
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
disabled: !canPerformEmptyStateActions || isCreatingPage,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useParams, usePathname } from "next/navigation";
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel, IWorkspaceSidebarNavigationItem } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { joinUrlPath } from "@plane/utils";
|
||||
// components
|
||||
import { SidebarNavItem } from "@/components/sidebar/sidebar-navigation";
|
||||
import { NotificationAppSidebarOption } from "@/components/workspace-notifications/notification-app-sidebar-option";
|
||||
@@ -47,13 +48,13 @@ export const SidebarItemBase: FC<Props> = observer(({ item, additionalRender, ad
|
||||
const isPinned = sidebarPreference?.[item.key]?.is_pinned;
|
||||
if (!isPinned && !staticItems.includes(item.key)) return null;
|
||||
|
||||
const itemHref = item.key === "your_work" ? `/${slug}${item.href}${data?.id}/` : `/${slug}${item.href}`;
|
||||
const isActive = itemHref === pathname;
|
||||
const itemHref =
|
||||
item.key === "your_work" && data?.id ? joinUrlPath(slug, item.href, data?.id) : joinUrlPath(slug, item.href);
|
||||
const icon = getSidebarNavigationItemIcon(item.key);
|
||||
|
||||
return (
|
||||
<Link href={itemHref} onClick={handleLinkClick}>
|
||||
<SidebarNavItem isActive={isActive}>
|
||||
<SidebarNavItem isActive={item.highlight(pathname, itemHref)}>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
{icon}
|
||||
<p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
// plane editor
|
||||
import { type EditorRefApi, getBinaryDataFromDocumentEditorHTMLString } from "@plane/editor";
|
||||
import { type EditorRefApi, getBinaryDataFromHTMLString } from "@plane/editor";
|
||||
// plane types
|
||||
import { TDocumentPayload } from "@plane/types";
|
||||
// hooks
|
||||
@@ -27,7 +27,9 @@ export const usePageFallback = (args: TArgs) => {
|
||||
if (latestEncodedDescription && latestEncodedDescription.byteLength > 0) {
|
||||
latestDecodedDescription = new Uint8Array(latestEncodedDescription);
|
||||
} else {
|
||||
latestDecodedDescription = getBinaryDataFromDocumentEditorHTMLString("<p></p>");
|
||||
latestDecodedDescription = getBinaryDataFromHTMLString({
|
||||
descriptionHTML: "<p></p>",
|
||||
});
|
||||
}
|
||||
|
||||
editor.setProviderDocument(latestDecodedDescription);
|
||||
|
||||
@@ -23,8 +23,12 @@ export class ProjectPageService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async fetchById(workspaceSlug: string, projectId: string, pageId: string): Promise<TPage> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`)
|
||||
async fetchById(workspaceSlug: string, projectId: string, pageId: string, trackVisit: boolean): Promise<TPage> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`, {
|
||||
params: {
|
||||
track_visit: trackVisit,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
||||
@@ -49,7 +49,12 @@ export interface IProjectPageStore {
|
||||
projectId: string,
|
||||
pageType?: TPageNavigationTabs
|
||||
) => Promise<TPage[] | undefined>;
|
||||
fetchPageDetails: (workspaceSlug: string, projectId: string, pageId: string) => Promise<TPage | undefined>;
|
||||
fetchPageDetails: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
options?: { trackVisit?: boolean }
|
||||
) => Promise<TPage | undefined>;
|
||||
createPage: (pageData: Partial<TPage>) => Promise<TPage | undefined>;
|
||||
removePage: (pageId: string) => Promise<void>;
|
||||
movePage: (workspaceSlug: string, projectId: string, pageId: string, newProjectId: string) => Promise<void>;
|
||||
@@ -239,7 +244,9 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
* @description fetch the details of a page
|
||||
* @param {string} pageId
|
||||
*/
|
||||
fetchPageDetails = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
fetchPageDetails = async (...args: Parameters<IProjectPageStore["fetchPageDetails"]>) => {
|
||||
const [workspaceSlug, projectId, pageId, options] = args;
|
||||
const { trackVisit } = options || {};
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !pageId) return undefined;
|
||||
|
||||
@@ -249,7 +256,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
this.error = undefined;
|
||||
});
|
||||
|
||||
const page = await this.service.fetchById(workspaceSlug, projectId, pageId);
|
||||
const page = await this.service.fetchById(workspaceSlug, projectId, pageId, trackVisit ?? true);
|
||||
|
||||
runInAction(() => {
|
||||
if (page?.id) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -47,13 +47,13 @@
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"mobx-utils": "^6.0.8",
|
||||
"next": "14.2.30",
|
||||
"next": "14.2.32",
|
||||
"next-themes": "^0.2.1",
|
||||
"posthog-js": "^1.131.3",
|
||||
"react": "^18.3.1",
|
||||
"react": "catalog:",
|
||||
"react-color": "^2.19.3",
|
||||
"react-day-picker": "9.5.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dom": "catalog:",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "7.51.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
@@ -72,13 +72,13 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"prettier": "^3.2.5",
|
||||
"typescript": "5.8.3"
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 8.5 KiB |
@@ -2,13 +2,9 @@
|
||||
"name": "plane",
|
||||
"description": "Open-source project management that unlocks customer value",
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --concurrency=18",
|
||||
@@ -36,7 +32,7 @@
|
||||
"tar-fs": "3.0.9",
|
||||
"prosemirror-view": "1.40.0",
|
||||
"@types/express": "4.17.23",
|
||||
"typescript": "5.8.3",
|
||||
"typescript": "catalog:",
|
||||
"sharp": "0.33.5"
|
||||
}
|
||||
},
|
||||
@@ -44,4 +40,4 @@
|
||||
"engines": {
|
||||
"node": ">=22.18.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"files": [
|
||||
@@ -33,8 +33,8 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/react": "^18.3.11",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
"@types/react": "catalog:",
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +244,7 @@ export interface IWorkspaceSidebarNavigationItem {
|
||||
labelTranslationKey: string;
|
||||
href: string;
|
||||
access: EUserWorkspaceRoles[];
|
||||
highlight: (pathname: string, url: string) => boolean;
|
||||
}
|
||||
|
||||
export const WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS: Record<string, IWorkspaceSidebarNavigationItem> = {
|
||||
@@ -252,24 +253,28 @@ export const WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS: Record<string, IWorkspa
|
||||
labelTranslationKey: "views",
|
||||
href: `/workspace-views/all-issues/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
highlight: (pathname: string, url: string) => pathname === url,
|
||||
},
|
||||
analytics: {
|
||||
key: "analytics",
|
||||
labelTranslationKey: "analytics",
|
||||
href: `/analytics/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
highlight: (pathname: string, url: string) => pathname.includes(url),
|
||||
},
|
||||
drafts: {
|
||||
key: "drafts",
|
||||
labelTranslationKey: "drafts",
|
||||
href: `/drafts/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
highlight: (pathname: string, url: string) => pathname.includes(url),
|
||||
},
|
||||
archives: {
|
||||
key: "archives",
|
||||
labelTranslationKey: "archives",
|
||||
href: `/projects/archives/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
highlight: (pathname: string, url: string) => pathname.includes(url),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -286,24 +291,28 @@ export const WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS: Record<string, IWorkspac
|
||||
labelTranslationKey: "home.title",
|
||||
href: `/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
highlight: (pathname: string, url: string) => pathname === url,
|
||||
},
|
||||
inbox: {
|
||||
key: "inbox",
|
||||
labelTranslationKey: "notification.label",
|
||||
href: `/notifications/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
highlight: (pathname: string, url: string) => pathname.includes(url),
|
||||
},
|
||||
"your-work": {
|
||||
key: "your_work",
|
||||
labelTranslationKey: "your_work",
|
||||
href: `/profile/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
highlight: (pathname: string, url: string) => pathname.includes(url),
|
||||
},
|
||||
projects: {
|
||||
key: "projects",
|
||||
labelTranslationKey: "projects",
|
||||
href: `/projects/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
highlight: (pathname: string, url: string) => pathname === url,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -52,11 +52,7 @@ userController.registerRoutes(router);
|
||||
### WebSocket Controller
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Controller,
|
||||
WebSocket,
|
||||
BaseWebSocketController,
|
||||
} from "@plane/decorators";
|
||||
import { Controller, WebSocket, BaseWebSocketController } from "@plane/decorators";
|
||||
import { Request } from "express";
|
||||
import { WebSocket as WS } from "ws";
|
||||
|
||||
|
||||
@@ -11,35 +11,23 @@
|
||||
"dist/**"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm,cjs --dts --external express,ws",
|
||||
"dev": "tsup src/index.ts --format esm,cjs --watch --dts --external express,ws",
|
||||
"check:lint": "eslint . --max-warnings 0",
|
||||
"build": "tsc --noEmit && tsup --minify",
|
||||
"dev": "tsup --watch",
|
||||
"check:lint": "eslint . --max-warnings 1",
|
||||
"check:types": "tsc --noEmit",
|
||||
"check:format": "prettier --check \"**/*.{ts,tsx,md,json,css,scss}\"",
|
||||
"fix:lint": "eslint . --fix",
|
||||
"fix:format": "prettier --write \"**/*.{ts,tsx,md,json,css,scss}\"",
|
||||
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.2",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.9",
|
||||
"@types/ws": "^8.5.10",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">=4.21.2",
|
||||
"ws": ">=8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
}
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { RequestHandler, Router } from "express";
|
||||
import type { RequestHandler, Router, Request } from "express";
|
||||
import type { WebSocket } from "ws";
|
||||
|
||||
import "reflect-metadata";
|
||||
|
||||
type HttpMethod =
|
||||
| "get"
|
||||
| "post"
|
||||
| "put"
|
||||
| "delete"
|
||||
| "patch"
|
||||
| "options"
|
||||
| "head"
|
||||
| "ws";
|
||||
type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "ws";
|
||||
|
||||
interface ControllerInstance {
|
||||
[key: string]: unknown;
|
||||
@@ -22,40 +16,85 @@ interface ControllerConstructor {
|
||||
|
||||
export function registerControllers(
|
||||
router: Router,
|
||||
Controller: ControllerConstructor,
|
||||
controllers: ControllerConstructor[],
|
||||
dependencies: any[] = []
|
||||
): void {
|
||||
controllers.forEach((Controller) => {
|
||||
// Create the controller instance with dependencies
|
||||
const instance = new Controller(...dependencies);
|
||||
|
||||
// Determine if it's a WebSocket controller or REST controller by checking
|
||||
// if it has any methods with the "ws" method metadata
|
||||
const isWebsocket = Object.getOwnPropertyNames(Controller.prototype).some((methodName) => {
|
||||
if (methodName === "constructor") return false;
|
||||
return Reflect.getMetadata("method", instance, methodName) === "ws";
|
||||
});
|
||||
|
||||
if (isWebsocket) {
|
||||
// Register as WebSocket controller
|
||||
// Pass the existing instance with dependencies to avoid creating a new instance without them
|
||||
registerWebSocketController(router, Controller, instance);
|
||||
} else {
|
||||
// Register as REST controller - doesn't accept an instance parameter
|
||||
registerRestController(router, Controller);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerRestController(router: Router, Controller: ControllerConstructor): void {
|
||||
const instance = new Controller();
|
||||
const baseRoute = Reflect.getMetadata("baseRoute", Controller) as string;
|
||||
|
||||
Object.getOwnPropertyNames(Controller.prototype).forEach((methodName) => {
|
||||
if (methodName === "constructor") return; // Skip the constructor
|
||||
|
||||
const method = Reflect.getMetadata(
|
||||
"method",
|
||||
instance,
|
||||
methodName,
|
||||
) as HttpMethod;
|
||||
const method = Reflect.getMetadata("method", instance, methodName) as HttpMethod;
|
||||
const route = Reflect.getMetadata("route", instance, methodName) as string;
|
||||
const middlewares =
|
||||
(Reflect.getMetadata(
|
||||
"middlewares",
|
||||
instance,
|
||||
methodName,
|
||||
) as RequestHandler[]) || [];
|
||||
const middlewares = (Reflect.getMetadata("middlewares", instance, methodName) as RequestHandler[]) || [];
|
||||
|
||||
if (method && route) {
|
||||
const handler = instance[methodName] as unknown;
|
||||
|
||||
if (typeof handler === "function") {
|
||||
if (method !== "ws") {
|
||||
(
|
||||
router[method] as (
|
||||
path: string,
|
||||
...handlers: RequestHandler[]
|
||||
) => void
|
||||
)(`${baseRoute}${route}`, ...middlewares, handler.bind(instance));
|
||||
(router[method] as (path: string, ...handlers: RequestHandler[]) => void)(
|
||||
`${baseRoute}${route}`,
|
||||
...middlewares,
|
||||
handler.bind(instance)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerWebSocketController(
|
||||
router: Router,
|
||||
Controller: ControllerConstructor,
|
||||
existingInstance?: ControllerInstance
|
||||
): void {
|
||||
const instance = existingInstance || new Controller();
|
||||
const baseRoute = Reflect.getMetadata("baseRoute", Controller) as string;
|
||||
|
||||
Object.getOwnPropertyNames(Controller.prototype).forEach((methodName) => {
|
||||
if (methodName === "constructor") return; // Skip the constructor
|
||||
|
||||
const method = Reflect.getMetadata("method", instance, methodName) as string;
|
||||
const route = Reflect.getMetadata("route", instance, methodName) as string;
|
||||
|
||||
if (method === "ws" && route) {
|
||||
const handler = instance[methodName] as unknown;
|
||||
|
||||
if (typeof handler === "function" && "ws" in router && typeof router.ws === "function") {
|
||||
router.ws(`${baseRoute}${route}`, (ws: WebSocket, req: Request) => {
|
||||
try {
|
||||
handler.call(instance, ws, req);
|
||||
} catch (error) {
|
||||
console.error(`WebSocket error in ${Controller.name}.${methodName}`, error);
|
||||
ws.close(1011, error instanceof Error ? error.message : "Internal server error");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ export { Controller, Middleware } from "./rest";
|
||||
export { Get, Post, Put, Patch, Delete } from "./rest";
|
||||
export { WebSocket } from "./websocket";
|
||||
export { registerControllers } from "./controller";
|
||||
export { registerWebSocketControllers } from "./websocket-controller";
|
||||
|
||||
// Also provide namespaced exports for better organization
|
||||
import * as RestDecorators from "./rest";
|
||||
|
||||
@@ -21,9 +21,7 @@ export function Controller(baseRoute: string = ""): ClassDecorator {
|
||||
* @param method HTTP method to handle
|
||||
* @returns Method decorator
|
||||
*/
|
||||
function createHttpMethodDecorator(
|
||||
method: RestMethod,
|
||||
): (route: string) => MethodDecorator {
|
||||
function createHttpMethodDecorator(method: RestMethod): (route: string) => MethodDecorator {
|
||||
return function (route: string): MethodDecorator {
|
||||
return function (target: object, propertyKey: string | symbol) {
|
||||
Reflect.defineMetadata("method", method, target, propertyKey);
|
||||
@@ -46,8 +44,7 @@ export const Delete = createHttpMethodDecorator("delete");
|
||||
*/
|
||||
export function Middleware(middleware: RequestHandler): MethodDecorator {
|
||||
return function (target: object, propertyKey: string | symbol) {
|
||||
const middlewares =
|
||||
Reflect.getMetadata("middlewares", target, propertyKey) || [];
|
||||
const middlewares = Reflect.getMetadata("middlewares", target, propertyKey) || [];
|
||||
middlewares.push(middleware);
|
||||
Reflect.defineMetadata("middlewares", middlewares, target, propertyKey);
|
||||
};
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Router, Request } from "express";
|
||||
import type { WebSocket } from "ws";
|
||||
import "reflect-metadata";
|
||||
|
||||
interface ControllerInstance {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ControllerConstructor {
|
||||
new (...args: unknown[]): ControllerInstance;
|
||||
prototype: ControllerInstance;
|
||||
}
|
||||
|
||||
export function registerWebSocketControllers(
|
||||
router: Router,
|
||||
Controller: ControllerConstructor,
|
||||
existingInstance?: ControllerInstance,
|
||||
): void {
|
||||
const instance = existingInstance || new Controller();
|
||||
const baseRoute = Reflect.getMetadata("baseRoute", Controller) as string;
|
||||
|
||||
Object.getOwnPropertyNames(Controller.prototype).forEach((methodName) => {
|
||||
if (methodName === "constructor") return; // Skip the constructor
|
||||
|
||||
const method = Reflect.getMetadata(
|
||||
"method",
|
||||
instance,
|
||||
methodName,
|
||||
) as string;
|
||||
const route = Reflect.getMetadata("route", instance, methodName) as string;
|
||||
|
||||
if (method === "ws" && route) {
|
||||
const handler = instance[methodName] as unknown;
|
||||
|
||||
if (
|
||||
typeof handler === "function" &&
|
||||
"ws" in router &&
|
||||
typeof router.ws === "function"
|
||||
) {
|
||||
router.ws(`${baseRoute}${route}`, (ws: WebSocket, req: Request) => {
|
||||
try {
|
||||
handler.call(instance, ws, req);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`WebSocket error in ${Controller.name}.${methodName}`,
|
||||
error,
|
||||
);
|
||||
ws.close(
|
||||
1011,
|
||||
error instanceof Error ? error.message : "Internal server error",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Base controller class for WebSocket endpoints
|
||||
*/
|
||||
export abstract class BaseWebSocketController {
|
||||
protected router: Router;
|
||||
|
||||
constructor() {
|
||||
this.router = Router();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base route for this controller
|
||||
*/
|
||||
protected getBaseRoute(): string {
|
||||
return Reflect.getMetadata("baseRoute", this.constructor) || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method to handle WebSocket connections
|
||||
* Implement this in your derived class
|
||||
*/
|
||||
abstract handleConnection(ws: WebSocket, req: Request): void;
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
"lib": ["ES2020"],
|
||||
"rootDir": ".",
|
||||
"baseUrl": ".",
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
@@ -32,8 +32,8 @@
|
||||
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.1",
|
||||
@@ -85,11 +85,11 @@
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/node": "18.15.3",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"postcss": "^8.4.38",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"keywords": [
|
||||
"editor",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { Extensions } from "@tiptap/core";
|
||||
|
||||
export const CoreEditorAdditionalExtensionsWithoutProps: Extensions = [];
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Extensions } from "@tiptap/core";
|
||||
|
||||
export const PARSER_KIT_ADDITIONAL_EXTENSIONS: Extensions = [];
|
||||
@@ -1,13 +1,10 @@
|
||||
import type { Extensions } from "@tiptap/core";
|
||||
import React, { useMemo } from "react";
|
||||
import React from "react";
|
||||
// plane imports
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { PageRenderer } from "@/components/editors";
|
||||
// constants
|
||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||
// extensions
|
||||
import { WorkItemEmbedExtension } from "@/extensions";
|
||||
// helpers
|
||||
import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
@@ -21,7 +18,7 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
|
||||
bubbleMenuEnabled = true,
|
||||
containerClassName,
|
||||
documentLoaderClassName,
|
||||
extensions: externalExtensions = [],
|
||||
extensions,
|
||||
disabledExtensions,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editable,
|
||||
@@ -47,20 +44,6 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
|
||||
user,
|
||||
} = props;
|
||||
|
||||
const extensions: Extensions = useMemo(() => {
|
||||
const allExtensions = [...externalExtensions];
|
||||
|
||||
if (embedHandler?.issue) {
|
||||
allExtensions.push(
|
||||
WorkItemEmbedExtension({
|
||||
widgetCallback: embedHandler.issue.widgetCallback,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return allExtensions;
|
||||
}, [externalExtensions, embedHandler.issue]);
|
||||
|
||||
// use document editor
|
||||
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({
|
||||
disabledExtensions,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PageRenderer } from "@/components/editors";
|
||||
// constants
|
||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||
// extensions
|
||||
import { HeadingListExtension, WorkItemEmbedExtension, SideMenuExtension } from "@/extensions";
|
||||
import { HeadingListExtension, SideMenuExtension } from "@/extensions";
|
||||
// helpers
|
||||
import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
@@ -39,13 +39,6 @@ const DocumentEditor = (props: IDocumentEditorProps) => {
|
||||
} = props;
|
||||
const extensions: Extensions = useMemo(() => {
|
||||
const additionalExtensions: Extensions = [];
|
||||
if (embedHandler?.issue) {
|
||||
additionalExtensions.push(
|
||||
WorkItemEmbedExtension({
|
||||
widgetCallback: embedHandler.issue.widgetCallback,
|
||||
})
|
||||
);
|
||||
}
|
||||
additionalExtensions.push(
|
||||
SideMenuExtension({
|
||||
aiEnabled: !disabledExtensions?.includes("ai"),
|
||||
|
||||
@@ -8,8 +8,6 @@ export * from "./mentions";
|
||||
export * from "./slash-commands";
|
||||
export * from "./table";
|
||||
export * from "./typography";
|
||||
export * from "./work-item-embed";
|
||||
export * from "./core-without-props";
|
||||
export * from "./custom-color";
|
||||
export * from "./enter-key";
|
||||
export * from "./extensions";
|
||||
|
||||
@@ -6,7 +6,7 @@ import StarterKit from "@tiptap/starter-kit";
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
// plane editor imports
|
||||
import { CoreEditorAdditionalExtensionsWithoutProps } from "@/plane-editor/extensions/core/without-props";
|
||||
import { PARSER_KIT_ADDITIONAL_EXTENSIONS } from "@/plane-editor/extensions/parser-kit";
|
||||
// extensions
|
||||
import { CustomCalloutExtensionConfig } from "./callout/extension-config";
|
||||
import { CustomCodeBlockExtensionWithoutProps } from "./code/without-props";
|
||||
@@ -21,9 +21,8 @@ import { CustomMentionExtensionConfig } from "./mentions/extension-config";
|
||||
import { CustomQuoteExtension } from "./quote";
|
||||
import { TableHeader, TableCell, TableRow, Table } from "./table";
|
||||
import { CustomTextAlignExtension } from "./text-align";
|
||||
import { WorkItemEmbedExtensionConfig } from "./work-item-embed/extension-config";
|
||||
|
||||
export const CoreEditorExtensionsWithoutProps = [
|
||||
export const PARSER_KIT = [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
@@ -99,7 +98,5 @@ export const CoreEditorExtensionsWithoutProps = [
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutExtensionConfig,
|
||||
CustomColorExtension,
|
||||
...CoreEditorAdditionalExtensionsWithoutProps,
|
||||
...PARSER_KIT_ADDITIONAL_EXTENSIONS,
|
||||
];
|
||||
|
||||
export const DocumentEditorExtensionsWithoutProps = [WorkItemEmbedExtensionConfig];
|
||||
@@ -1,43 +0,0 @@
|
||||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
|
||||
export const WorkItemEmbedExtensionConfig = Node.create({
|
||||
name: CORE_EXTENSIONS.WORK_ITEM_EMBED,
|
||||
group: "block",
|
||||
atom: true,
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
entity_identifier: {
|
||||
default: undefined,
|
||||
},
|
||||
project_identifier: {
|
||||
default: undefined,
|
||||
},
|
||||
workspace_identifier: {
|
||||
default: undefined,
|
||||
},
|
||||
id: {
|
||||
default: undefined,
|
||||
},
|
||||
entity_name: {
|
||||
default: undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "issue-embed-component",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["issue-embed-component", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { ReactNodeViewRenderer, NodeViewWrapper, type NodeViewProps } from "@tiptap/react";
|
||||
// local imports
|
||||
import { WorkItemEmbedExtensionConfig } from "./extension-config";
|
||||
|
||||
type Props = {
|
||||
widgetCallback: ({
|
||||
issueId,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
}: {
|
||||
issueId: string;
|
||||
projectId: string | undefined;
|
||||
workspaceSlug: string | undefined;
|
||||
}) => React.ReactNode;
|
||||
};
|
||||
|
||||
export const WorkItemEmbedExtension = (props: Props) =>
|
||||
WorkItemEmbedExtensionConfig.extend({
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer((issueProps: NodeViewProps) => (
|
||||
<NodeViewWrapper>
|
||||
{props.widgetCallback({
|
||||
issueId: issueProps.node.attrs.entity_identifier,
|
||||
projectId: issueProps.node.attrs.project_identifier,
|
||||
workspaceSlug: issueProps.node.attrs.workspace_identifier,
|
||||
})}
|
||||
</NodeViewWrapper>
|
||||
));
|
||||
},
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./extension";
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Editor, Range } from "@tiptap/core";
|
||||
import type { Level } from "@tiptap/extension-heading";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// extensions
|
||||
@@ -12,7 +13,7 @@ export const setText = (editor: Editor, range?: Range) => {
|
||||
else editor.chain().focus().setNode(CORE_EXTENSIONS.PARAGRAPH).run();
|
||||
};
|
||||
|
||||
export const toggleHeading = (editor: Editor, level: 1 | 2 | 3 | 4 | 5 | 6, range?: Range) => {
|
||||
export const toggleHeading = (editor: Editor, level: Level, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode(CORE_EXTENSIONS.HEADING, { level }).run();
|
||||
else editor.chain().focus().toggleHeading({ level }).run();
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
replaceAdditionalAssetsInHTMLContent,
|
||||
} from "@/plane-editor/helpers/parser";
|
||||
// local imports
|
||||
import { convertHTMLDocumentToAllFormats } from "./yjs-utils";
|
||||
import { getAllDocumentFormatsFromHTMLString } from "./yjs-utils";
|
||||
|
||||
/**
|
||||
* @description function to extract all assets from HTML content
|
||||
@@ -62,10 +62,9 @@ export const getEditorContentWithReplacedAssets = async (props: {
|
||||
entityId: string;
|
||||
entityType: TEditorAssetType;
|
||||
projectId: string | undefined;
|
||||
variant: "rich" | "document";
|
||||
duplicateAssetService: (params: TDuplicateAssetData) => Promise<TDuplicateAssetResponse>;
|
||||
}): Promise<TDocumentPayload> => {
|
||||
const { descriptionHTML, entityId, entityType, projectId, variant, duplicateAssetService } = props;
|
||||
const { descriptionHTML, entityId, entityType, projectId, duplicateAssetService } = props;
|
||||
let replacedDescription = descriptionHTML;
|
||||
// step 1: extract image assets from the description
|
||||
const assetIds = extractAssetsFromHTMLContent(descriptionHTML);
|
||||
@@ -86,9 +85,8 @@ export const getEditorContentWithReplacedAssets = async (props: {
|
||||
}
|
||||
}
|
||||
// step 4: convert the description to the document payload
|
||||
const documentPayload = convertHTMLDocumentToAllFormats({
|
||||
document_html: replacedDescription,
|
||||
variant,
|
||||
const documentPayload = getAllDocumentFormatsFromHTMLString({
|
||||
descriptionHTML: replacedDescription,
|
||||
});
|
||||
return documentPayload;
|
||||
};
|
||||
|
||||
@@ -4,17 +4,10 @@ import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-pros
|
||||
import * as Y from "yjs";
|
||||
// extensions
|
||||
import { TDocumentPayload } from "@plane/types";
|
||||
import {
|
||||
CoreEditorExtensionsWithoutProps,
|
||||
DocumentEditorExtensionsWithoutProps,
|
||||
} from "@/extensions/core-without-props";
|
||||
import { PARSER_KIT } from "@/extensions/parser-kit";
|
||||
|
||||
// editor extension configs
|
||||
const RICH_TEXT_EDITOR_EXTENSIONS = CoreEditorExtensionsWithoutProps;
|
||||
const DOCUMENT_EDITOR_EXTENSIONS = [...CoreEditorExtensionsWithoutProps, ...DocumentEditorExtensionsWithoutProps];
|
||||
// editor schemas
|
||||
const richTextEditorSchema = getSchema(RICH_TEXT_EDITOR_EXTENSIONS);
|
||||
const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
|
||||
// parser schema
|
||||
const parserSchema = getSchema(PARSER_KIT);
|
||||
|
||||
/**
|
||||
* @description apply updates to a doc and return the updated doc in binary format
|
||||
@@ -49,56 +42,41 @@ export const convertBinaryDataToBase64String = (document: Uint8Array): string =>
|
||||
export const convertBase64StringToBinaryData = (document: string): ArrayBuffer => Buffer.from(document, "base64");
|
||||
|
||||
/**
|
||||
* @description this function generates the binary equivalent of html content for the rich text editor
|
||||
* @param {string} descriptionHTML
|
||||
* @description Converts HTML string to binary data
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export const getBinaryDataFromRichTextEditorHTMLString = (descriptionHTML: string): Uint8Array => {
|
||||
export const getBinaryDataFromHTMLString = ({ descriptionHTML }: { descriptionHTML: string }): Uint8Array => {
|
||||
// convert HTML to JSON
|
||||
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", RICH_TEXT_EDITOR_EXTENSIONS);
|
||||
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", PARSER_KIT);
|
||||
// convert JSON to Y.Doc format
|
||||
const transformedData = prosemirrorJSONToYDoc(richTextEditorSchema, contentJSON, "default");
|
||||
const transformedData = prosemirrorJSONToYDoc(parserSchema, contentJSON, "default");
|
||||
// convert Y.Doc to Uint8Array format
|
||||
const encodedData = Y.encodeStateAsUpdate(transformedData);
|
||||
return encodedData;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function generates the binary equivalent of html content for the document editor
|
||||
* @param {string} descriptionHTML
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export const getBinaryDataFromDocumentEditorHTMLString = (descriptionHTML: string): Uint8Array => {
|
||||
// convert HTML to JSON
|
||||
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", DOCUMENT_EDITOR_EXTENSIONS);
|
||||
// convert JSON to Y.Doc format
|
||||
const transformedData = prosemirrorJSONToYDoc(documentEditorSchema, contentJSON, "default");
|
||||
// convert Y.Doc to Uint8Array format
|
||||
const encodedData = Y.encodeStateAsUpdate(transformedData);
|
||||
return encodedData;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function generates all document formats for the provided binary data for the rich text editor
|
||||
* @param {Uint8Array} description
|
||||
* @description Converts binary data to all supported document formats (JSON, HTML, and binary)
|
||||
* @returns
|
||||
*/
|
||||
export const getAllDocumentFormatsFromRichTextEditorBinaryData = (
|
||||
description: Uint8Array
|
||||
): {
|
||||
export const getAllDocumentFormatsFromBinaryData = ({
|
||||
descriptionBinary,
|
||||
}: {
|
||||
descriptionBinary: Uint8Array;
|
||||
}): {
|
||||
contentBinaryEncoded: string;
|
||||
contentJSON: object;
|
||||
contentHTML: string;
|
||||
} => {
|
||||
// encode binary description data
|
||||
const base64Data = convertBinaryDataToBase64String(description);
|
||||
const base64Data = convertBinaryDataToBase64String(descriptionBinary);
|
||||
const yDoc = new Y.Doc();
|
||||
Y.applyUpdate(yDoc, description);
|
||||
Y.applyUpdate(yDoc, descriptionBinary);
|
||||
// convert to JSON
|
||||
const type = yDoc.getXmlFragment("default");
|
||||
const contentJSON = yXmlFragmentToProseMirrorRootNode(type, richTextEditorSchema).toJSON();
|
||||
const contentJSON = yXmlFragmentToProseMirrorRootNode(type, parserSchema).toJSON();
|
||||
// convert to HTML
|
||||
const contentHTML = generateHTML(contentJSON, RICH_TEXT_EDITOR_EXTENSIONS);
|
||||
const contentHTML = generateHTML(contentJSON, PARSER_KIT);
|
||||
|
||||
return {
|
||||
contentBinaryEncoded: base64Data,
|
||||
@@ -108,77 +86,24 @@ export const getAllDocumentFormatsFromRichTextEditorBinaryData = (
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function generates all document formats for the provided binary data for the document editor
|
||||
* @param {Uint8Array} description
|
||||
* @returns
|
||||
*/
|
||||
export const getAllDocumentFormatsFromDocumentEditorBinaryData = (
|
||||
description: Uint8Array
|
||||
): {
|
||||
contentBinaryEncoded: string;
|
||||
contentJSON: object;
|
||||
contentHTML: string;
|
||||
} => {
|
||||
// encode binary description data
|
||||
const base64Data = convertBinaryDataToBase64String(description);
|
||||
const yDoc = new Y.Doc();
|
||||
Y.applyUpdate(yDoc, description);
|
||||
// convert to JSON
|
||||
const type = yDoc.getXmlFragment("default");
|
||||
const contentJSON = yXmlFragmentToProseMirrorRootNode(type, documentEditorSchema).toJSON();
|
||||
// convert to HTML
|
||||
const contentHTML = generateHTML(contentJSON, DOCUMENT_EDITOR_EXTENSIONS);
|
||||
|
||||
return {
|
||||
contentBinaryEncoded: base64Data,
|
||||
contentJSON,
|
||||
contentHTML,
|
||||
};
|
||||
};
|
||||
|
||||
type TConvertHTMLDocumentToAllFormatsArgs = {
|
||||
document_html: string;
|
||||
variant: "rich" | "document";
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts HTML content to all supported document formats (JSON, HTML, and binary)
|
||||
* @param {TConvertHTMLDocumentToAllFormatsArgs} args - Arguments containing HTML content and variant type
|
||||
* @param {string} args.document_html - The HTML content to convert
|
||||
* @param {"rich" | "document"} args.variant - The type of editor variant to use for conversion
|
||||
* @description Converts HTML string to all supported document formats (JSON, HTML, and binary)
|
||||
* @returns {TDocumentPayload} Object containing the document in all supported formats
|
||||
* @throws {Error} If an invalid variant is provided
|
||||
*/
|
||||
export const convertHTMLDocumentToAllFormats = (args: TConvertHTMLDocumentToAllFormatsArgs): TDocumentPayload => {
|
||||
const { document_html, variant } = args;
|
||||
export const getAllDocumentFormatsFromHTMLString = ({
|
||||
descriptionHTML,
|
||||
}: {
|
||||
descriptionHTML: string;
|
||||
}): TDocumentPayload => {
|
||||
// Convert HTML to binary format for rich text editor
|
||||
const contentBinary = getBinaryDataFromHTMLString({ descriptionHTML });
|
||||
// Generate all document formats from the binary data
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromBinaryData({
|
||||
descriptionBinary: contentBinary,
|
||||
});
|
||||
|
||||
let allFormats: TDocumentPayload;
|
||||
|
||||
if (variant === "rich") {
|
||||
// Convert HTML to binary format for rich text editor
|
||||
const contentBinary = getBinaryDataFromRichTextEditorHTMLString(document_html);
|
||||
// Generate all document formats from the binary data
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
||||
getAllDocumentFormatsFromRichTextEditorBinaryData(contentBinary);
|
||||
allFormats = {
|
||||
description: contentJSON,
|
||||
description_html: contentHTML,
|
||||
description_binary: contentBinaryEncoded,
|
||||
};
|
||||
} else if (variant === "document") {
|
||||
// Convert HTML to binary format for document editor
|
||||
const contentBinary = getBinaryDataFromDocumentEditorHTMLString(document_html);
|
||||
// Generate all document formats from the binary data
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
||||
getAllDocumentFormatsFromDocumentEditorBinaryData(contentBinary);
|
||||
allFormats = {
|
||||
description: contentJSON,
|
||||
description_html: contentHTML,
|
||||
description_binary: contentBinaryEncoded,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Invalid variant provided: ${variant}`);
|
||||
}
|
||||
|
||||
return allFormats;
|
||||
return {
|
||||
description: contentJSON,
|
||||
description_html: contentHTML,
|
||||
description_binary: contentBinaryEncoded,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "@/extensions/core-without-props";
|
||||
export * from "@/constants/document-collaborative-events";
|
||||
export * from "@/helpers/get-document-server-event";
|
||||
export * from "@/helpers/yjs-utils";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@plane/eslint-config",
|
||||
"private": true,
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"files": [
|
||||
"library.js",
|
||||
@@ -18,6 +18,6 @@
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"typescript": "5.8.3"
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/hooks",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "React hooks that are shared across multiple apps internally",
|
||||
"private": true,
|
||||
@@ -21,14 +21,14 @@
|
||||
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1"
|
||||
"react": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/react": "^18.3.11",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
"@types/react": "catalog:",
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/i18n",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "I18n shared across multiple apps internally",
|
||||
"private": true,
|
||||
@@ -25,8 +25,8 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/react": "^18.3.11",
|
||||
"typescript": "5.8.3"
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/react": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/logger",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Logger shared across multiple apps internally",
|
||||
"private": true,
|
||||
@@ -28,16 +28,15 @@
|
||||
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.2",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
"express-winston": "^4.2.0",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.5.4",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
"@types/node": "^20.14.9",
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +1,14 @@
|
||||
import path from "path";
|
||||
import winston from "winston";
|
||||
import DailyRotateFile from "winston-daily-rotate-file";
|
||||
import { createLogger, format, LoggerOptions, transports } from "winston";
|
||||
|
||||
// Define log levels
|
||||
const levels = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
http: 3,
|
||||
debug: 4,
|
||||
};
|
||||
|
||||
// Define colors for each level
|
||||
const colors = {
|
||||
error: "red",
|
||||
warn: "yellow",
|
||||
info: "green",
|
||||
http: "magenta",
|
||||
debug: "white",
|
||||
};
|
||||
|
||||
// Tell winston about our colors
|
||||
winston.addColors(colors);
|
||||
|
||||
// Custom format for logging
|
||||
const format = winston.format.combine(
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss:ms" }),
|
||||
winston.format.colorize({ all: true }),
|
||||
winston.format.printf(
|
||||
(info: winston.Logform.TransformableInfo) => `[${info?.timestamp}] ${info.level}: ${info.message}`
|
||||
)
|
||||
);
|
||||
|
||||
// Define which transports to use
|
||||
const transports = [
|
||||
// Console transport
|
||||
new winston.transports.Console(),
|
||||
|
||||
// Rotating file transport for errors
|
||||
new DailyRotateFile({
|
||||
filename: path.join(process.cwd(), "logs", "error-%DATE%.log"),
|
||||
datePattern: "YYYY-MM-DD",
|
||||
zippedArchive: true,
|
||||
maxSize: process.env.LOG_MAX_SIZE || "20m",
|
||||
maxFiles: process.env.LOG_RETENTION || "7d",
|
||||
level: "error",
|
||||
}),
|
||||
|
||||
// Rotating file transport for all logs
|
||||
new DailyRotateFile({
|
||||
filename: path.join(process.cwd(), "logs", "combined-%DATE%.log"),
|
||||
datePattern: "YYYY-MM-DD",
|
||||
zippedArchive: true,
|
||||
maxSize: process.env.LOG_MAX_SIZE || "20m",
|
||||
maxFiles: process.env.LOG_RETENTION || "7d",
|
||||
}),
|
||||
];
|
||||
|
||||
// Create the logger
|
||||
export const logger = winston.createLogger({
|
||||
export const loggerConfig: LoggerOptions = {
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
levels,
|
||||
format,
|
||||
transports,
|
||||
});
|
||||
format: format.combine(
|
||||
format.timestamp({
|
||||
format: "YYYY-MM-DD HH:mm:ss:ms",
|
||||
}),
|
||||
format.json()
|
||||
),
|
||||
transports: [new transports.Console()],
|
||||
};
|
||||
|
||||
export const logger = createLogger(loggerConfig);
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { logger } from "./config";
|
||||
import type { RequestHandler } from "express";
|
||||
import expressWinston from "express-winston";
|
||||
import { transports } from "winston";
|
||||
import { loggerConfig } from "./config";
|
||||
|
||||
export const requestLogger = (req: Request, res: Response, next: NextFunction) => {
|
||||
// Log when the request starts
|
||||
const startTime = Date.now();
|
||||
|
||||
// Log request details
|
||||
logger.http(`Incoming ${req.method} request to ${req.url} from ${req.ip}`);
|
||||
|
||||
// Log request body if present
|
||||
if (Object.keys(req.body).length > 0) {
|
||||
logger.debug("Request body:", req.body);
|
||||
}
|
||||
|
||||
// Capture response
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - startTime;
|
||||
logger.http(`Completed ${req.method} ${req.url} with status ${res.statusCode} in ${duration}ms`);
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
export const loggerMiddleware: RequestHandler = expressWinston.logger({
|
||||
...loggerConfig,
|
||||
transports: [new transports.Console()],
|
||||
msg: "{{req.method}} {{req.url}} {{res.statusCode}} {{res.responseTime}}ms",
|
||||
expressFormat: true,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/propel",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -25,32 +25,33 @@
|
||||
"./combobox": "./src/combobox/index.ts",
|
||||
"./tooltip": "./src/tooltip/index.ts",
|
||||
"./styles/fonts": "./src/styles/fonts/index.css",
|
||||
"./card": "./src/card/index.ts",
|
||||
"./switch": "./src/switch/index.ts"
|
||||
"./switch": "./src/switch/index.ts",
|
||||
"./accordion": "./src/accordion/index.ts",
|
||||
"./card": "./src/card/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui-components/react": "^1.0.0-beta.2",
|
||||
"@plane/constants": "workspace:*",
|
||||
"@plane/hooks": "workspace:*",
|
||||
"@plane/types": "workspace:*",
|
||||
"@plane/utils": "workspace:*",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.469.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"recharts": "^2.15.1"
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"recharts": "^2.15.1",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@storybook/react-vite": "^9.1.2",
|
||||
"@types/react": "18.3.1",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"eslint-plugin-storybook": "^9.1.2",
|
||||
"storybook": "^9.1.2",
|
||||
"typescript": "5.8.3"
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Accordion as BaseAccordion } from "@base-ui-components/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
interface AccordionRootProps {
|
||||
defaultValue?: string[];
|
||||
allowMultiple?: boolean;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface AccordionItemProps {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface AccordionTriggerProps {
|
||||
className?: string;
|
||||
icon?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
asChild?: boolean;
|
||||
iconClassName?: string;
|
||||
}
|
||||
|
||||
interface AccordionContentProps {
|
||||
className?: string;
|
||||
contentWrapperClassName?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const AccordionRoot: React.FC<AccordionRootProps> = ({
|
||||
defaultValue = [],
|
||||
allowMultiple = false,
|
||||
className = "",
|
||||
children,
|
||||
}) => (
|
||||
<BaseAccordion.Root defaultValue={defaultValue} openMultiple={allowMultiple} className={`text-base ${className}`}>
|
||||
{children}
|
||||
</BaseAccordion.Root>
|
||||
);
|
||||
|
||||
const AccordionItem: React.FC<AccordionItemProps> = ({ value, disabled, className = "", children }) => (
|
||||
<BaseAccordion.Item value={value} disabled={disabled} className={`relative ${className}`}>
|
||||
{children}
|
||||
</BaseAccordion.Item>
|
||||
);
|
||||
|
||||
const AccordionTrigger: React.FC<AccordionTriggerProps> = ({
|
||||
className = "",
|
||||
icon = <PlusIcon aria-hidden="true" className="transition-all ease-out group-data-[panel-open]:rotate-45" />,
|
||||
iconClassName = "",
|
||||
children,
|
||||
asChild = false,
|
||||
}) => (
|
||||
<BaseAccordion.Header>
|
||||
{asChild ? (
|
||||
<BaseAccordion.Trigger className={`w-full py-2 ${className}`}>{children}</BaseAccordion.Trigger>
|
||||
) : (
|
||||
<BaseAccordion.Trigger className={`flex w-full items-center justify-between gap-2 py-2 ${className}`}>
|
||||
{children}
|
||||
<span aria-hidden="true" className={`flex-shrink-0 ${iconClassName}`}>
|
||||
{icon}
|
||||
</span>
|
||||
</BaseAccordion.Trigger>
|
||||
)}
|
||||
</BaseAccordion.Header>
|
||||
);
|
||||
|
||||
const AccordionContent: React.FC<AccordionContentProps> = ({
|
||||
className = "",
|
||||
contentWrapperClassName = "",
|
||||
children,
|
||||
}) => (
|
||||
<BaseAccordion.Panel
|
||||
className={`h-[var(--accordion-panel-height)] overflow-hidden transition-[height] ease-out data-[ending-style]:h-0 data-[starting-style]:h-0 ${className}`}
|
||||
>
|
||||
<div className={`py-2 ${contentWrapperClassName}`}>{children}</div>
|
||||
</BaseAccordion.Panel>
|
||||
);
|
||||
|
||||
export const Accordion = {
|
||||
Root: AccordionRoot,
|
||||
Item: AccordionItem,
|
||||
Trigger: AccordionTrigger,
|
||||
Content: AccordionContent,
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./accordion";
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import { Avatar as AvatarPrimitive } from "@base-ui-components/react/avatar";
|
||||
// utils
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn } from "../utils/classname";
|
||||
|
||||
export type TAvatarSize = "sm" | "md" | "base" | "lg" | number;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn } from "../utils/classname";
|
||||
import {
|
||||
ECardDirection,
|
||||
ECardSpacing,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from "react";
|
||||
// plane imports
|
||||
import { TBarChartShapeVariant, TBarItem, TChartData } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn } from "../../utils/classname";
|
||||
|
||||
// Constants
|
||||
const MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT = 14; // Minimum height required to show text inside bar
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import { LegendProps } from "recharts";
|
||||
// plane imports
|
||||
import { TChartLegend } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn } from "../../utils/classname";
|
||||
|
||||
export const getLegendProps = (args: TChartLegend): LegendProps => {
|
||||
const { align, layout, verticalAlign } = args;
|
||||
|
||||
@@ -2,8 +2,7 @@ import React from "react";
|
||||
import { NameType, Payload, ValueType } from "recharts/types/component/DefaultTooltipContent";
|
||||
// plane imports
|
||||
import { Card, ECardSpacing } from "../../card";
|
||||
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn } from "../../utils/classname";
|
||||
|
||||
type Props = {
|
||||
active: boolean | undefined;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useMemo } from "react";
|
||||
// plane imports
|
||||
import { TBottomSectionConfig, TContentVisibility, TTopSectionConfig } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn } from "../../utils/classname";
|
||||
|
||||
const LAYOUT = {
|
||||
PADDING: 2,
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from "react";
|
||||
import { Treemap, ResponsiveContainer, Tooltip } from "recharts";
|
||||
// plane imports
|
||||
import { TreeMapChartProps } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// local imports
|
||||
import { cn } from "../../utils/classname";
|
||||
import { CustomTreeMapContent } from "./map-content";
|
||||
import { TreeMapTooltip } from "./tooltip";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@plane/utils";
|
||||
import { Command } from "../command/command";
|
||||
import { Popover } from "../popover/root";
|
||||
import { cn } from "../utils/classname";
|
||||
|
||||
export interface ComboboxOption {
|
||||
value: unknown;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn } from "../utils/classname";
|
||||
|
||||
function CommandComponent({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return <CommandPrimitive data-slot="command" className={cn("", className)} {...props} />;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { Dialog as BaseDialog } from "@base-ui-components/react";
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn } from "../utils/classname";
|
||||
|
||||
// enums
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { Menu as BaseMenu } from "@base-ui-components/react/menu";
|
||||
import { ChevronDown, ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
// plane imports
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn } from "../utils/classname";
|
||||
import { TMenuProps, TSubMenuProps, TMenuItemProps } from "./types";
|
||||
|
||||
// Context for main menu to communicate with submenus
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Separator } from "./separator";
|
||||
|
||||
const meta: Meta<typeof Separator> = {
|
||||
title: "Components/Separator",
|
||||
component: Separator,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Separator>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<div className="w-[300px] space-y-4">
|
||||
<div>Content Above</div>
|
||||
<Separator />
|
||||
<div>Content Below</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const Vertical: Story = {
|
||||
render: () => (
|
||||
<div className="flex h-[100px] items-center space-x-4">
|
||||
<div>Left Content</div>
|
||||
<Separator orientation="vertical" />
|
||||
<div>Right Content</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithinContainer: Story = {
|
||||
render: () => (
|
||||
<div className="w-[300px] rounded-lg border p-6 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium leading-none">Section 1</h4>
|
||||
<p className="text-sm text-muted-foreground">Description for section 1</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium leading-none">Section 2</h4>
|
||||
<p className="text-sm text-muted-foreground">Description for section 2</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from "react";
|
||||
import { Separator as SeparatorPrimitive } from "@base-ui-components/react/separator";
|
||||
import { cn } from "../utils";
|
||||
|
||||
interface SeparatorProps extends React.ComponentProps<typeof SeparatorPrimitive> {
|
||||
/**
|
||||
* The orientation of the separator
|
||||
* @default "horizontal"
|
||||
*/
|
||||
orientation?: "horizontal" | "vertical";
|
||||
}
|
||||
|
||||
const Separator = React.forwardRef<React.ElementRef<typeof SeparatorPrimitive>, SeparatorProps>(
|
||||
({ orientation = "horizontal", ...props }, ref) => (
|
||||
<SeparatorPrimitive
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
data-slot="separator"
|
||||
data-orientation={orientation}
|
||||
{...props}
|
||||
className={cn("bg-custom-border-200", "shrink-0", orientation === "horizontal" ? "h-px w-full" : "h-full w-px")}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
Separator.displayName = "Separator";
|
||||
|
||||
export { Separator };
|
||||
export type { SeparatorProps };
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { Switch as BaseSwitch } from "@base-ui-components/react/switch";
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn } from "../utils/classname";
|
||||
|
||||
interface IToggleSwitchProps {
|
||||
value: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn } from "../utils/classname";
|
||||
|
||||
const Table = React.forwardRef<React.ComponentRef<"table">, React.ComponentPropsWithoutRef<"table">>(
|
||||
({ className, ...props }, ref) => (
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./tabs";
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import React, { FC } from "react";
|
||||
import { Tabs as BaseTabs } from "@base-ui-components/react/tabs";
|
||||
import { LucideProps } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
export type TabListItem = {
|
||||
key: string;
|
||||
icon?: FC<LucideProps>;
|
||||
label?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type TTabListProps = {
|
||||
tabs: TabListItem[];
|
||||
tabListClassName?: string;
|
||||
tabClassName?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
selectedTab?: string;
|
||||
};
|
||||
|
||||
export const TabList: FC<TTabListProps> = ({ tabs, tabListClassName, tabClassName, size = "md", selectedTab }) => (
|
||||
<BaseTabs.List
|
||||
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 relative",
|
||||
tabListClassName
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<BaseTabs.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",
|
||||
(selectedTab ? selectedTab === tab.key : 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",
|
||||
{
|
||||
"text-xs": size === "sm",
|
||||
"text-sm": size === "md",
|
||||
"text-base": size === "lg",
|
||||
},
|
||||
tabClassName
|
||||
)
|
||||
}
|
||||
key={tab.key}
|
||||
disabled={tab.disabled}
|
||||
>
|
||||
{tab.icon && <tab.icon className="size-4" />}
|
||||
{tab.label}
|
||||
</BaseTabs.Tab>
|
||||
))}
|
||||
|
||||
<BaseTabs.Indicator className="absolute left-0 top-[50%] z-[-1] h-6 w-[var(--active-tab-width)] translate-x-[var(--active-tab-left)] -translate-y-[50%] rounded-sm bg-custom-background-100 shadow-sm transition-[width,transform] duration-200 ease-in-out" />
|
||||
</BaseTabs.List>
|
||||
);
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Fragment } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Tabs } from "./tabs";
|
||||
|
||||
const meta: Meta<typeof Tabs> = {
|
||||
title: "Components/Tabs",
|
||||
component: Tabs,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Basic: Story = {
|
||||
render: () => (
|
||||
<div className="w-[400px]">
|
||||
<Tabs defaultValue="account">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="account">Overview</Tabs.Trigger>
|
||||
<Tabs.Trigger value="password">Settings</Tabs.Trigger>
|
||||
<Tabs.Indicator />
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="account" className="p-4">
|
||||
Overview settings go here
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="password" className="p-4">
|
||||
Settings settings go here
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const Sizes: Story = {
|
||||
render: () => {
|
||||
const sizes = ["sm", "md", "lg"] as const;
|
||||
const labels = {
|
||||
sm: "Small",
|
||||
md: "Medium",
|
||||
lg: "Large",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-[400px]">
|
||||
{sizes.map((size, index) => (
|
||||
<Fragment key={size}>
|
||||
{index > 0 && <div className="h-4" />}
|
||||
<div className="text-lg">{labels[size]}</div>
|
||||
<Tabs defaultValue="overview">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="overview" size={size}>
|
||||
Overview
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="settings" size={size}>
|
||||
Settings
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -1,91 +1,96 @@
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { Tabs as BaseTabs } from "@base-ui-components/react/tabs";
|
||||
// helpers
|
||||
import { useLocalStorage } from "@plane/hooks";
|
||||
import { cn } from "@plane/utils";
|
||||
// types
|
||||
import { TabList, TabListItem } from "./list";
|
||||
import * as React from "react";
|
||||
import { Tabs as TabsPrimitive } from "@base-ui-components/react/tabs";
|
||||
import { cn } from "../utils/classname";
|
||||
|
||||
export type TabContent = {
|
||||
content: React.ReactNode;
|
||||
type TabsCompound = React.ForwardRefExoticComponent<
|
||||
React.ComponentProps<typeof TabsPrimitive.Root> & React.RefAttributes<React.ElementRef<typeof TabsPrimitive.Root>>
|
||||
> & {
|
||||
List: React.ForwardRefExoticComponent<
|
||||
React.ComponentProps<typeof TabsPrimitive.List> & React.RefAttributes<React.ElementRef<typeof TabsPrimitive.List>>
|
||||
>;
|
||||
Trigger: React.ForwardRefExoticComponent<
|
||||
React.ComponentProps<typeof TabsPrimitive.Tab> & { size?: "sm" | "md" | "lg" } & React.RefAttributes<
|
||||
React.ElementRef<typeof TabsPrimitive.Tab>
|
||||
>
|
||||
>;
|
||||
Content: React.ForwardRefExoticComponent<
|
||||
React.ComponentProps<typeof TabsPrimitive.Panel> & React.RefAttributes<React.ElementRef<typeof TabsPrimitive.Panel>>
|
||||
>;
|
||||
Indicator: React.ForwardRefExoticComponent<React.ComponentProps<"div"> & React.RefAttributes<HTMLDivElement>>;
|
||||
};
|
||||
|
||||
export type TabItem = TabListItem & TabContent;
|
||||
const TabsRoot = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Root>,
|
||||
React.ComponentProps<typeof TabsPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Root data-slot="tabs" className={cn("flex flex-col w-full h-full", className)} {...props} ref={ref} />
|
||||
));
|
||||
|
||||
type TTabsProps = {
|
||||
tabs: TabItem[];
|
||||
storageKey?: string;
|
||||
actions?: React.ReactNode;
|
||||
defaultTab?: string;
|
||||
containerClassName?: string;
|
||||
tabListContainerClassName?: string;
|
||||
tabListClassName?: string;
|
||||
tabClassName?: string;
|
||||
tabPanelClassName?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
storeInLocalStorage?: boolean;
|
||||
};
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentProps<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
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 relative overflow-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
|
||||
export const Tabs: FC<TTabsProps> = (props: TTabsProps) => {
|
||||
const {
|
||||
tabs,
|
||||
storageKey,
|
||||
actions,
|
||||
defaultTab = tabs[0]?.key,
|
||||
containerClassName = "",
|
||||
tabListContainerClassName = "",
|
||||
tabListClassName = "",
|
||||
tabClassName = "",
|
||||
tabPanelClassName = "",
|
||||
size = "md",
|
||||
storeInLocalStorage = true,
|
||||
} = props;
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Tab>,
|
||||
React.ComponentProps<typeof TabsPrimitive.Tab> & { size?: "sm" | "md" | "lg" }
|
||||
>(({ className, size = "md", ...props }, ref) => (
|
||||
<TabsPrimitive.Tab
|
||||
data-slot="tabs-trigger"
|
||||
className={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 duration-200 ease-in-out rounded",
|
||||
"data-[selected]:bg-custom-background-100 data-[selected]:text-custom-text-100 data-[selected]:shadow-sm",
|
||||
"text-custom-text-400 hover:text-custom-text-300 hover:bg-custom-background-80/60",
|
||||
"disabled:text-custom-text-400 disabled:cursor-not-allowed",
|
||||
{
|
||||
"text-xs": size === "sm",
|
||||
"text-sm": size === "md",
|
||||
"text-base": size === "lg",
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
|
||||
const { storedValue, setValue } = useLocalStorage(
|
||||
storeInLocalStorage && storageKey ? `tab-${storageKey}` : `tab-${tabs[0]?.key}`,
|
||||
defaultTab
|
||||
);
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Panel>,
|
||||
React.ComponentProps<typeof TabsPrimitive.Panel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Panel
|
||||
data-slot="tabs-content"
|
||||
className={cn("relative outline-none", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
const TabsIndicator = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-0 top-[50%] z-[-1] h-6 w-[var(--active-tab-width)] translate-x-[var(--active-tab-left)] -translate-y-[50%] rounded-sm bg-custom-background-100 shadow-sm transition-[width,transform] duration-200 ease-in-out",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
|
||||
const [activeIndex, setActiveIndex] = useState(() => {
|
||||
const initialTab = storedValue ?? defaultTab;
|
||||
return tabs.findIndex((tab) => tab.key === initialTab);
|
||||
});
|
||||
export const Tabs = Object.assign(TabsRoot, {
|
||||
List: TabsList,
|
||||
Trigger: TabsTrigger,
|
||||
Content: TabsContent,
|
||||
Indicator: TabsIndicator,
|
||||
}) satisfies TabsCompound;
|
||||
|
||||
useEffect(() => {
|
||||
if (storeInLocalStorage && tabs[activeIndex]) {
|
||||
setValue(tabs[activeIndex].key);
|
||||
}
|
||||
}, [activeIndex, setValue, storeInLocalStorage, tabs]);
|
||||
|
||||
const handleTabChange = (index: number) => {
|
||||
setActiveIndex(index);
|
||||
if (!tabs[index].disabled) {
|
||||
tabs[index].onClick?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseTabs.Root
|
||||
value={activeIndex}
|
||||
onValueChange={handleTabChange}
|
||||
className={cn("flex flex-col w-full h-full overflow-hidden", containerClassName)}
|
||||
>
|
||||
<div className={cn("flex w-full items-center gap-4", tabListContainerClassName)}>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
tabListClassName={tabListClassName}
|
||||
tabClassName={tabClassName}
|
||||
size={size}
|
||||
selectedTab={tabs[activeIndex]?.key}
|
||||
/>
|
||||
{actions && <div className="flex-grow">{actions}</div>}
|
||||
</div>
|
||||
|
||||
{tabs.map((tab) => (
|
||||
<BaseTabs.Panel key={tab.key} className={cn("relative h-full overflow-auto", tabPanelClassName)}>
|
||||
{tab.content}
|
||||
</BaseTabs.Panel>
|
||||
))}
|
||||
</BaseTabs.Root>
|
||||
);
|
||||
};
|
||||
export { TabsList, TabsTrigger, TabsContent, TabsIndicator };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { Tooltip as BaseTooltip } from "@base-ui-components/react/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn } from "../utils/classname";
|
||||
import { TPlacement, TSide, TAlign, convertPlacementToSideAndAlign } from "../utils/placement";
|
||||
|
||||
type ITooltipProps = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import clsx, { type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const cn = (...inputs: ClassValue[]) => clsx(inputs);
|
||||
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/services",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
@@ -22,7 +22,7 @@
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/shared-state",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Shared state shared across multiple apps internally",
|
||||
"private": true,
|
||||
@@ -22,6 +22,6 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.5.4",
|
||||
"typescript": "5.8.3"
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/tailwind-config",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "tailwind.config.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
@@ -27,15 +27,15 @@
|
||||
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/react": "18.3.11",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/typescript-config",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"files": [
|
||||
@@ -25,8 +25,8 @@
|
||||
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.1.10",
|
||||
@@ -66,16 +66,16 @@
|
||||
"@storybook/react": "^8.1.1",
|
||||
"@storybook/react-webpack5": "^8.1.1",
|
||||
"@storybook/test": "^8.1.1",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "^20.5.2",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-color": "^3.0.9",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-dom": "catalog:",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"postcss-nested": "^6.0.1",
|
||||
"storybook": "^8.1.1",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/utils",
|
||||
"version": "0.28.0",
|
||||
"version": "1.0.0",
|
||||
"description": "Helper functions shared across multiple apps internally",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
@@ -28,7 +28,7 @@
|
||||
"isomorphic-dompurify": "^2.16.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.469.0",
|
||||
"react": "^18.3.1",
|
||||
"react": "catalog:",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tlds": "1.259.0",
|
||||
"uuid": "^10.0.0"
|
||||
@@ -36,11 +36,11 @@
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react": "catalog:",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||