Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
801a40acd5 |
@@ -17,6 +17,8 @@ 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,6 +36,8 @@ 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
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"lucide-react": "^0.469.0",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"next": "14.2.32",
|
||||
"next": "14.2.30",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.49",
|
||||
"react": "^18.3.1",
|
||||
@@ -45,7 +45,7 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
|
||||
@@ -91,7 +91,6 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
"project_lead": UserLiteSerializer,
|
||||
"state": StateLiteSerializer,
|
||||
"created_by": UserLiteSerializer,
|
||||
"updated_by": UserLiteSerializer,
|
||||
"issue": IssueSerializer,
|
||||
"actor": UserLiteSerializer,
|
||||
"owned_by": UserLiteSerializer,
|
||||
|
||||
@@ -441,11 +441,7 @@ 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,
|
||||
disposition="attachment",
|
||||
filename=asset.attributes.get("name"),
|
||||
)
|
||||
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
|
||||
# Redirect to the signed URL
|
||||
return HttpResponseRedirect(signed_url)
|
||||
|
||||
@@ -645,11 +641,7 @@ 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,
|
||||
disposition="attachment",
|
||||
filename=asset.attributes.get("name"),
|
||||
)
|
||||
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
|
||||
# Redirect to the signed URL
|
||||
return HttpResponseRedirect(signed_url)
|
||||
|
||||
|
||||
@@ -198,7 +198,6 @@ 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
|
||||
@@ -231,14 +230,13 @@ class PageViewSet(BaseViewSet):
|
||||
).values_list("entity_identifier", flat=True)
|
||||
data = PageDetailSerializer(page).data
|
||||
data["issue_ids"] = issue_ids
|
||||
if track_visit:
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
entity_name="page",
|
||||
entity_identifier=pk,
|
||||
user_id=request.user.id,
|
||||
project_id=project_id,
|
||||
)
|
||||
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,14 +172,12 @@ 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": project_id,
|
||||
"project_id": request.data.get("project_id", None),
|
||||
"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 is not None and mongo_available:
|
||||
if mongo_collection 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": str(record["id"]),
|
||||
"id": record["id"],
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"token_identifier": str(record["token_identifier"]),
|
||||
"token_identifier": 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": str(record["created_by_id"]),
|
||||
"created_by_id": record["created_by_id"],
|
||||
}
|
||||
|
||||
|
||||
def transform_email_log(record: Dict) -> Dict:
|
||||
"""Transform email notification log record."""
|
||||
return {
|
||||
"id": str(record["id"]),
|
||||
"id": record["id"],
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"receiver_id": str(record["receiver_id"]),
|
||||
"triggered_by_id": str(record["triggered_by_id"]),
|
||||
"entity_identifier": str(record["entity_identifier"]),
|
||||
"receiver_id": record["receiver_id"],
|
||||
"triggered_by_id": record["triggered_by_id"],
|
||||
"entity_identifier": 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": str(record["old_value"]),
|
||||
"new_value": str(record["new_value"]),
|
||||
"created_by_id": str(record["created_by_id"]),
|
||||
"old_value": record["old_value"],
|
||||
"new_value": record["new_value"],
|
||||
"created_by_id": record["created_by_id"],
|
||||
}
|
||||
|
||||
|
||||
def transform_page_version(record: Dict) -> Dict:
|
||||
"""Transform page version record."""
|
||||
return {
|
||||
"id": str(record["id"]),
|
||||
"id": record["id"],
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"page_id": str(record["page_id"]),
|
||||
"workspace_id": str(record["workspace_id"]),
|
||||
"owned_by_id": str(record["owned_by_id"]),
|
||||
"page_id": record["page_id"],
|
||||
"workspace_id": record["workspace_id"],
|
||||
"owned_by_id": 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": str(record["created_by_id"]),
|
||||
"updated_by_id": str(record["updated_by_id"]),
|
||||
"created_by_id": record["created_by_id"],
|
||||
"updated_by_id": 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": str(record["id"]),
|
||||
"id": record["id"],
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"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"]),
|
||||
"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"],
|
||||
"last_saved_at": (
|
||||
str(record["last_saved_at"]) if record.get("last_saved_at") else None
|
||||
),
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# 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,7 +57,6 @@ 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)
|
||||
@@ -99,9 +98,8 @@ 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, blank=True)
|
||||
entity_identifier = models.UUIDField(null=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"
|
||||
)
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"mobx-utils": "^6.0.8",
|
||||
"next": "14.2.32",
|
||||
"next": "14.2.30",
|
||||
"next-themes": "^0.2.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "^18.3.1",
|
||||
@@ -58,12 +58,13 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "18.14.1",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.36.0",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
const payload: any = { ...data };
|
||||
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
|
||||
|
||||
updateIssue && (await updateIssue(issue.project_id, issue.id, payload));
|
||||
await updateIssue?.(issue.project_id, issue.id, payload);
|
||||
};
|
||||
|
||||
const isAllowed = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT);
|
||||
|
||||
@@ -112,8 +112,8 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
const issueDetails = getIssueById(issueId);
|
||||
const projectIdentifier = getProjectIdentifierById(issueDetails?.project_id);
|
||||
|
||||
const handleIssuePeekOverview = (e: any) => {
|
||||
e.stopPropagation(true);
|
||||
const handleIssuePeekOverview = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleRedirection(workspaceSlug, issueDetails, isMobile);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-sc
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { EIssueFilterType, EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { EIssueServiceType, EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
||||
import { EIssueServiceType, EIssuesStoreType, EIssueLayoutTypes, TIssue } from "@plane/types";
|
||||
//constants
|
||||
//hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
@@ -42,7 +42,7 @@ export type KanbanStoreType =
|
||||
|
||||
export interface IBaseKanBanLayout {
|
||||
QuickActions: FC<IQuickActionProps>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
|
||||
isCompletedCycle?: boolean;
|
||||
viewId?: string | undefined;
|
||||
|
||||
@@ -238,7 +238,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [cardRef?.current, issue?.id, isDragAllowed, canDropOverIssue, setIsCurrentBlockDragging, setIsDraggingOverBlock]);
|
||||
}, [issue?.id, isDragAllowed, canDropOverIssue, setIsCurrentBlockDragging, setIsDraggingOverBlock]);
|
||||
|
||||
if (!issue) return null;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { MutableRefObject } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// plane imports
|
||||
import {
|
||||
GroupByColumnTypes,
|
||||
IGroupByColumn,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
} from "@plane/types";
|
||||
// constants
|
||||
import { ContentWrapper } from "@plane/ui";
|
||||
// components
|
||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||
@@ -92,8 +90,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
subGroupIndex = 0,
|
||||
isEpic = false,
|
||||
} = props;
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const storeType = useIssueStoreType();
|
||||
const issueKanBanView = useKanbanView();
|
||||
|
||||
@@ -78,7 +78,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
title: "Success!",
|
||||
message: "Work items added to the cycle successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
import { IssueBlockRoot } from "./block-root";
|
||||
import { TRenderQuickActions } from "./list-view-types";
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
issueIds: TGroupedIssues | any;
|
||||
issuesMap: TIssueMap;
|
||||
groupId: string;
|
||||
@@ -20,7 +20,7 @@ interface Props {
|
||||
canDropOverIssue: boolean;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
isEpic?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export const IssueBlocksList: FC<Props> = (props) => {
|
||||
const {
|
||||
|
||||
@@ -81,7 +81,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
|
||||
title: "Success!",
|
||||
message: "Work items added to the cycle successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
|
||||
@@ -192,7 +192,9 @@ export const ListGroup = observer((props: Props) => {
|
||||
const sourceGroupId = source?.data?.groupId as string | undefined;
|
||||
const currentGroupId = group.id;
|
||||
|
||||
sourceGroupId && handleWorkFlowState(sourceGroupId, currentGroupId);
|
||||
if (sourceGroupId) {
|
||||
handleWorkFlowState(sourceGroupId, currentGroupId);
|
||||
}
|
||||
|
||||
const sourceIndex = getGroupIndex(sourceGroupId);
|
||||
const currentIndex = getGroupIndex(currentGroupId);
|
||||
@@ -230,15 +232,7 @@ export const ListGroup = observer((props: Props) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [
|
||||
groupRef?.current,
|
||||
group,
|
||||
orderBy,
|
||||
getGroupIndex,
|
||||
setDragColumnOrientation,
|
||||
setIsDraggingOverColumn,
|
||||
isWorkflowDropDisabled,
|
||||
]);
|
||||
}, [group, orderBy, getGroupIndex, setDragColumnOrientation, setIsDraggingOverColumn, isWorkflowDropDisabled]);
|
||||
|
||||
const isDragAllowed = !!group_by && DRAG_ALLOWED_GROUPS.includes(group_by);
|
||||
const canOverlayBeVisible = isWorkflowDropDisabled || orderBy !== "sort_order" || !!group.isDropDisabled;
|
||||
|
||||
@@ -232,6 +232,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
...getChangedIssuefields(formData, dirtyFields as { [key: string]: boolean | undefined }),
|
||||
project_id: getValues<"project_id">("project_id"),
|
||||
id: data.id,
|
||||
description_html: formData.description_html ?? "<p></p>",
|
||||
type_id: getValues<"type_id">("type_id"),
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { FC, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { Link2, MoveDiagonal, MoveRight } from "lucide-react";
|
||||
import { Link2, type LucideIcon, MoveDiagonal, MoveRight } from "lucide-react";
|
||||
// plane imports
|
||||
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CenterPanelIcon,
|
||||
CustomSelect,
|
||||
FullScreenPanelIcon,
|
||||
type ISvgIcons,
|
||||
SidePanelIcon,
|
||||
TOAST_TYPE,
|
||||
Tooltip,
|
||||
@@ -33,7 +34,7 @@ import { NameDescriptionUpdateStatus } from "../issue-update-status";
|
||||
|
||||
export type TPeekModes = "side-peek" | "modal" | "full-screen";
|
||||
|
||||
const PEEK_OPTIONS: { key: TPeekModes; icon: any; i18n_title: string }[] = [
|
||||
const PEEK_OPTIONS: { key: TPeekModes; icon: LucideIcon | React.FC<ISvgIcons>; i18n_title: string }[] = [
|
||||
{
|
||||
key: "side-peek",
|
||||
icon: SidePanelIcon,
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// types
|
||||
import { PROJECT_SETTINGS_TRACKER_ELEMENTS, PROJECT_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { PROJECT_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
|
||||
import type { IIssueLabel } from "@plane/types";
|
||||
// ui
|
||||
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
|
||||
@@ -154,7 +154,7 @@ export const LabelDndHOC = observer((props: Props) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [labelRef?.current, dragHandleRef?.current, label, isChild, isGroup, isLastChild, onDrop]);
|
||||
}, [dragHandleRef?.current, label, isChild, isGroup, isLastChild, onDrop]);
|
||||
|
||||
const isMakeChild = instruction == "make-child";
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ 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) => {
|
||||
@@ -30,10 +31,11 @@ export const BasePaidPlanCard: FC<TBasePaidPlanCardProps> = observer((props) =>
|
||||
extraFeatures,
|
||||
renderPriceContent,
|
||||
renderActionButton,
|
||||
isSelfHosted,
|
||||
} = props;
|
||||
// states
|
||||
const [selectedPlan, setSelectedPlan] = useState<TBillingFrequency>("month");
|
||||
const basePlan = getBaseSubscriptionName(planVariant);
|
||||
const basePlan = getBaseSubscriptionName(planVariant, isSelfHosted);
|
||||
const upgradeCardVariantStyle = getUpgradeCardVariantStyle(planVariant);
|
||||
// Plane details
|
||||
const planeName = getSubscriptionName(planVariant);
|
||||
|
||||
@@ -107,6 +107,7 @@ export const PlanUpgradeCard: FC<PlanUpgradeCardProps> = observer((props) => {
|
||||
isTrialAllowed={isTrialAllowed}
|
||||
/>
|
||||
)}
|
||||
isSelfHosted={isSelfHosted}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -107,6 +107,7 @@ export const TalkToSalesCard: FC<TalkToSalesCardProps> = observer((props) => {
|
||||
extraFeatures={extraFeatures}
|
||||
renderPriceContent={renderPriceContent}
|
||||
renderActionButton={renderActionButton}
|
||||
isSelfHosted={isSelfHosted}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -110,7 +110,7 @@ export const ModuleAnalyticsProgress: FC<TModuleAnalyticsProgress> = observer((p
|
||||
await fetchModuleDetails(workspaceSlug, projectId, moduleId);
|
||||
}
|
||||
setLoader(false);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setLoader(false);
|
||||
setPlotType(moduleId, plotType);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "@plane/constants";
|
||||
// plane types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
|
||||
import { ILinkDetails, IModule, ModuleLink, TModuleStatus } from "@plane/types";
|
||||
// plane ui
|
||||
import { Loader, LayersIcon, CustomSelect, ModuleStatusIcon, TOAST_TYPE, setToast, TextArea } from "@plane/ui";
|
||||
// components
|
||||
@@ -270,7 +270,7 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||
</span>
|
||||
}
|
||||
value={value}
|
||||
onChange={(value: any) => {
|
||||
onChange={(value: TModuleStatus) => {
|
||||
submitChanges({ status: value });
|
||||
}}
|
||||
disabled={!isEditingAllowed || isArchived}
|
||||
|
||||
@@ -27,7 +27,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
|
||||
const { invitations, totalSteps, stepChange, finishOnboarding } = props;
|
||||
const { invitations, totalSteps: _totalSteps, stepChange, finishOnboarding } = props;
|
||||
// states
|
||||
const [currentView, setCurrentView] = useState<ECreateOrJoinWorkspaceViews | null>(null);
|
||||
// store hooks
|
||||
|
||||
@@ -262,7 +262,7 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = observer((props) => {
|
||||
});
|
||||
|
||||
export const InviteMembers: React.FC<Props> = (props) => {
|
||||
const { finishOnboarding, totalSteps, workspace } = props;
|
||||
const { finishOnboarding, totalSteps: _totalSteps, workspace } = props;
|
||||
|
||||
const [isInvitationDisabled, setIsInvitationDisabled] = useState(true);
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Listbox } from "@headlessui/react";
|
||||
import { ROLE, ROLE_DETAILS, EUserPermissions, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { EOnboardingSteps, IWorkspace } from "@plane/types";
|
||||
import { EOnboardingSteps } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
@@ -30,7 +30,6 @@ import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUser, useUserProfile } from "@/hooks/store/user";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// components
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
|
||||
// icons
|
||||
import { FileText, Layers } from "lucide-react";
|
||||
import { ContrastIcon, DiceIcon, LayersIcon } from "@plane/ui";
|
||||
// types
|
||||
import { TTourSteps } from "./root";
|
||||
import { FileText, Layers, type LucideIcon } from "lucide-react";
|
||||
// plane imports
|
||||
import { ContrastIcon, DiceIcon, type ISvgIcons, LayersIcon } from "@plane/ui";
|
||||
// local imports
|
||||
import type { TTourSteps } from "./root";
|
||||
|
||||
const sidebarOptions: {
|
||||
key: TTourSteps;
|
||||
label: string;
|
||||
Icon: any;
|
||||
Icon: LucideIcon | React.FC<ISvgIcons>;
|
||||
}[] = [
|
||||
{
|
||||
key: "work-items",
|
||||
|
||||
@@ -27,7 +27,7 @@ import { PageEditorToolbarRoot } from "./toolbar";
|
||||
export type TPageRootHandlers = {
|
||||
create: (payload: Partial<TPage>) => Promise<Partial<TPage> | undefined>;
|
||||
fetchAllVersions: (pageId: string) => Promise<TPageVersion[] | undefined>;
|
||||
fetchDescriptionBinary: () => Promise<any>;
|
||||
fetchDescriptionBinary: () => Promise<ArrayBuffer>;
|
||||
fetchVersionDetails: (pageId: string, versionId: string) => Promise<TPageVersion | undefined>;
|
||||
getRedirectionLink: (pageId: string) => string;
|
||||
updateDescription: (document: TDocumentPayload) => Promise<void>;
|
||||
|
||||
@@ -77,7 +77,7 @@ export const PageForm: React.FC<Props> = (props) => {
|
||||
</>
|
||||
</span>
|
||||
}
|
||||
onChange={(val: any) => {
|
||||
onChange={(val) => {
|
||||
let logoValue = {};
|
||||
|
||||
if (val?.type === "emoji")
|
||||
|
||||
@@ -56,7 +56,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
|
||||
<>
|
||||
{userProfileActivity ? (
|
||||
<ul role="list">
|
||||
{userProfileActivity.results.map((activityItem: any) => {
|
||||
{userProfileActivity.results.map((activityItem) => {
|
||||
if (activityItem.field === "comment")
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-2">
|
||||
@@ -98,7 +98,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
|
||||
editable={false}
|
||||
id={activityItem.id}
|
||||
initialValue={
|
||||
activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value
|
||||
(activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value) ?? ""
|
||||
}
|
||||
containerClassName="text-xs bg-custom-background-100"
|
||||
workspaceId={activityItem?.workspace_detail?.id?.toString() ?? ""}
|
||||
@@ -155,7 +155,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
|
||||
<span className="text-gray font-medium">{activityItem.actor_detail.first_name} Bot</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`}
|
||||
href={`/${activityItem.workspace_detail?.slug}/profile/${activityItem.actor_detail.id}`}
|
||||
className="inline"
|
||||
>
|
||||
<span className="text-gray font-medium">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { IFavorite, InstructionType, IPragmaticPayloadLocation, TDropTarget } from "@plane/types";
|
||||
|
||||
export type TargetData = {
|
||||
|
||||
@@ -7,7 +7,6 @@ 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";
|
||||
@@ -48,13 +47,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" && data?.id ? joinUrlPath(slug, item.href, data?.id) : joinUrlPath(slug, item.href);
|
||||
const itemHref = item.key === "your_work" ? `/${slug}${item.href}${data?.id}/` : `/${slug}${item.href}`;
|
||||
const isActive = itemHref === pathname;
|
||||
const icon = getSidebarNavigationItemIcon(item.key);
|
||||
|
||||
return (
|
||||
<Link href={itemHref} onClick={handleLinkClick}>
|
||||
<SidebarNavItem isActive={item.highlight(pathname, itemHref)}>
|
||||
<SidebarNavItem isActive={isActive}>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
{icon}
|
||||
<p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>
|
||||
|
||||
@@ -66,7 +66,7 @@ export const UserMenuRoot = observer((props: Props) => {
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative flex-shrink-0">
|
||||
{({ open, close }: { open: boolean; close: () => void }) => {
|
||||
{({ open }: { open: boolean; close: () => void }) => {
|
||||
// Update local state directly
|
||||
if (isUserMenuOpen !== open) {
|
||||
setIsUserMenuOpen(open);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
List,
|
||||
ListOrdered,
|
||||
ListTodo,
|
||||
LucideIcon,
|
||||
type LucideIcon,
|
||||
Strikethrough,
|
||||
Table,
|
||||
TextQuote,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from "lucide-react";
|
||||
// plane imports
|
||||
import type { TCommandExtraProps, TEditorCommands, TEditorFontStyle } from "@plane/editor";
|
||||
import { MonospaceIcon, SansSerifIcon, SerifIcon } from "@plane/ui";
|
||||
import { type ISvgIcons, MonospaceIcon, SansSerifIcon, SerifIcon } from "@plane/ui";
|
||||
import { convertRemToPixel } from "@plane/utils";
|
||||
|
||||
type TEditorTypes = "lite" | "document" | "sticky";
|
||||
@@ -191,7 +191,7 @@ export const TOOLBAR_ITEMS: {
|
||||
export const EDITOR_FONT_STYLES: {
|
||||
key: TEditorFontStyle;
|
||||
label: string;
|
||||
icon: any;
|
||||
icon: LucideIcon | React.FC<ISvgIcons>;
|
||||
}[] = [
|
||||
{
|
||||
key: "sans-serif",
|
||||
|
||||
@@ -90,7 +90,7 @@ export const useGroupIssuesDragNDrop = (
|
||||
delete data[moduleKey];
|
||||
}
|
||||
|
||||
updateIssue && updateIssue(projectId, issueId, data).catch(() => setToast(errorToastProps));
|
||||
updateIssue?.(projectId, issueId, data).catch(() => setToast(errorToastProps));
|
||||
};
|
||||
|
||||
const handleOnDrop = async (source: GroupDropLocation, destination: GroupDropLocation) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const useIntersectionObserver = (
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[entries.length - 1].isIntersecting) {
|
||||
callback && callback();
|
||||
callback?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
export const getValueFromLocalStorage = (key: string, defaultValue: any) => {
|
||||
export const getValueFromLocalStorage = (key: string, defaultValue: unknown) => {
|
||||
if (typeof window === undefined || typeof window === "undefined") return defaultValue;
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : defaultValue;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
window.localStorage.removeItem(key);
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
export const setValueIntoLocalStorage = (key: string, value: any) => {
|
||||
export const setValueIntoLocalStorage = (key: string, value: unknown) => {
|
||||
if (typeof window === undefined || typeof window === "undefined") return false;
|
||||
try {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ const useReloadConfirmations = (isActive = true, message?: string, defaultShowAl
|
||||
// show confirm dialog
|
||||
const isLeaving = confirm(alertMessage);
|
||||
if (isLeaving) {
|
||||
onLeave && onLeave();
|
||||
onLeave?.();
|
||||
} else {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -18,6 +18,7 @@ import { runQuery } from "./utils/query-executor";
|
||||
import { sanitizeWorkItemQueries } from "./utils/query-sanitizer.ts";
|
||||
import { createTables } from "./utils/tables";
|
||||
import { clearOPFS, getGroupedIssueResults, getSubGroupedIssueResults, log, logError } from "./utils/utils";
|
||||
import type { DBClass } from "./worker/db";
|
||||
|
||||
const DB_VERSION = 1.3;
|
||||
const PAGE_SIZE = 500;
|
||||
@@ -70,7 +71,6 @@ export class Storage {
|
||||
};
|
||||
|
||||
private initializeWorker = async (workspaceSlug: string) => {
|
||||
const { DBClass } = await import("./worker/db");
|
||||
const worker = new Worker(new URL("./worker/db.ts", import.meta.url));
|
||||
const MyWorker = Comlink.wrap<typeof DBClass>(worker);
|
||||
|
||||
@@ -367,7 +367,6 @@ export class Storage {
|
||||
issueResults = getGroupedIssueResults(issueResults);
|
||||
}
|
||||
}
|
||||
const groupCount = group_by ? Object.keys(issueResults).length : undefined;
|
||||
// const subGroupCount = sub_group_by ? Object.keys(issueResults[Object.keys(issueResults)[0]]).length : undefined;
|
||||
const groupingEnd = performance.now();
|
||||
|
||||
|
||||
@@ -23,12 +23,8 @@ export class ProjectPageService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
})
|
||||
async fetchById(workspaceSlug: string, projectId: string, pageId: string): Promise<TPage> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
||||
@@ -49,12 +49,7 @@ export interface IProjectPageStore {
|
||||
projectId: string,
|
||||
pageType?: TPageNavigationTabs
|
||||
) => Promise<TPage[] | undefined>;
|
||||
fetchPageDetails: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
options?: { trackVisit?: boolean }
|
||||
) => Promise<TPage | undefined>;
|
||||
fetchPageDetails: (workspaceSlug: string, projectId: string, pageId: string) => 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>;
|
||||
@@ -244,9 +239,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
* @description fetch the details of a page
|
||||
* @param {string} pageId
|
||||
*/
|
||||
fetchPageDetails = async (...args: Parameters<IProjectPageStore["fetchPageDetails"]>) => {
|
||||
const [workspaceSlug, projectId, pageId, options] = args;
|
||||
const { trackVisit } = options || {};
|
||||
fetchPageDetails = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !pageId) return undefined;
|
||||
|
||||
@@ -256,7 +249,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
this.error = undefined;
|
||||
});
|
||||
|
||||
const page = await this.service.fetchById(workspaceSlug, projectId, pageId, trackVisit ?? true);
|
||||
const page = await this.service.fetchById(workspaceSlug, projectId, pageId);
|
||||
|
||||
runInAction(() => {
|
||||
if (page?.id) {
|
||||
|
||||
@@ -98,9 +98,11 @@ export class UserSettingsStore implements IUserSettingsStore {
|
||||
} else if (workspaceSlug) {
|
||||
await persistence.initialize(workspaceSlug);
|
||||
persistence.syncWorkspace();
|
||||
projectId && persistence.syncIssues(projectId);
|
||||
if (projectId) {
|
||||
persistence.syncIssues(projectId);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
console.warn("error while toggling local DB");
|
||||
runInAction(() => {
|
||||
this.canUseLocalDB = currentLocalDBValue;
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"mobx-utils": "^6.0.8",
|
||||
"next": "14.2.32",
|
||||
"next": "14.2.30",
|
||||
"next-themes": "^0.2.1",
|
||||
"posthog-js": "^1.131.3",
|
||||
"react": "^18.3.1",
|
||||
@@ -72,7 +72,7 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-color": "^3.0.6",
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 25 KiB |
@@ -5,6 +5,10 @@
|
||||
"version": "0.28.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --concurrency=18",
|
||||
|
||||
@@ -244,7 +244,6 @@ 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> = {
|
||||
@@ -253,28 +252,24 @@ 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),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -291,28 +286,24 @@ 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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -52,7 +52,11 @@ 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,23 +11,35 @@
|
||||
"dist/**"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit && tsup --minify",
|
||||
"dev": "tsup --watch",
|
||||
"check:lint": "eslint . --max-warnings 1",
|
||||
"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",
|
||||
"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",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">=4.21.2",
|
||||
"ws": ">=8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import type { RequestHandler, Router, Request } from "express";
|
||||
import type { WebSocket } from "ws";
|
||||
|
||||
import { RequestHandler, Router } from "express";
|
||||
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;
|
||||
@@ -16,85 +22,40 @@ interface ControllerConstructor {
|
||||
|
||||
export function registerControllers(
|
||||
router: Router,
|
||||
controllers: ControllerConstructor[],
|
||||
dependencies: any[] = []
|
||||
Controller: ControllerConstructor,
|
||||
): 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,6 +3,7 @@ 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,7 +21,9 @@ 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);
|
||||
@@ -44,7 +46,8 @@ 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);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
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,6 +6,7 @@
|
||||
"lib": ["ES2020"],
|
||||
"rootDir": ".",
|
||||
"baseUrl": ".",
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/react": "^18.3.11",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
|
||||
@@ -28,14 +28,15 @@
|
||||
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"express-winston": "^4.2.0",
|
||||
"winston": "^3.17.0"
|
||||
"express": "^4.21.2",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.9",
|
||||
"@types/node": "^22.5.4",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
|
||||
@@ -1,14 +1,66 @@
|
||||
import { createLogger, format, LoggerOptions, transports } from "winston";
|
||||
import path from "path";
|
||||
import winston from "winston";
|
||||
import DailyRotateFile from "winston-daily-rotate-file";
|
||||
|
||||
export const loggerConfig: LoggerOptions = {
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
format: format.combine(
|
||||
format.timestamp({
|
||||
format: "YYYY-MM-DD HH:mm:ss:ms",
|
||||
}),
|
||||
format.json()
|
||||
),
|
||||
transports: [new transports.Console()],
|
||||
// Define log levels
|
||||
const levels = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
http: 3,
|
||||
debug: 4,
|
||||
};
|
||||
|
||||
export const logger = createLogger(loggerConfig);
|
||||
// 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({
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
levels,
|
||||
format,
|
||||
transports,
|
||||
});
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import type { RequestHandler } from "express";
|
||||
import expressWinston from "express-winston";
|
||||
import { transports } from "winston";
|
||||
import { loggerConfig } from "./config";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { logger } from "./config";
|
||||
|
||||
export const loggerMiddleware: RequestHandler = expressWinston.logger({
|
||||
...loggerConfig,
|
||||
transports: [new transports.Console()],
|
||||
msg: "{{req.method}} {{req.url}} {{res.statusCode}} {{res.responseTime}}ms",
|
||||
expressFormat: true,
|
||||
});
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -33,14 +33,14 @@
|
||||
"@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",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
"recharts": "^2.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { Avatar as AvatarPrimitive } from "@base-ui-components/react/avatar";
|
||||
import { cn } from "../utils/classname";
|
||||
// utils
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
export type TAvatarSize = "sm" | "md" | "base" | "lg" | number;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../utils/classname";
|
||||
import { cn } from "@plane/utils";
|
||||
import {
|
||||
ECardDirection,
|
||||
ECardSpacing,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from "react";
|
||||
// plane imports
|
||||
import { TBarChartShapeVariant, TBarItem, TChartData } from "@plane/types";
|
||||
import { cn } from "../../utils/classname";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
// 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 "../../utils/classname";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
export const getLegendProps = (args: TChartLegend): LegendProps => {
|
||||
const { align, layout, verticalAlign } = args;
|
||||
|
||||
@@ -2,7 +2,8 @@ import React from "react";
|
||||
import { NameType, Payload, ValueType } from "recharts/types/component/DefaultTooltipContent";
|
||||
// plane imports
|
||||
import { Card, ECardSpacing } from "../../card";
|
||||
import { cn } from "../../utils/classname";
|
||||
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
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 "../../utils/classname";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
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 "../utils/classname";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
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 "../utils/classname";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
// enums
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { Menu as BaseMenu } from "@base-ui-components/react/menu";
|
||||
import { ChevronDown, ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
import { cn } from "../utils/classname";
|
||||
// plane imports
|
||||
import { cn } from "@plane/utils";
|
||||
import { TMenuProps, TSubMenuProps, TMenuItemProps } from "./types";
|
||||
|
||||
// Context for main menu to communicate with submenus
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { Switch as BaseSwitch } from "@base-ui-components/react/switch";
|
||||
import { cn } from "../utils/classname";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
interface IToggleSwitchProps {
|
||||
value: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "../utils/classname";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
const Table = React.forwardRef<React.ComponentRef<"table">, React.ComponentPropsWithoutRef<"table">>(
|
||||
({ className, ...props }, ref) => (
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FC } from "react";
|
||||
import { Tabs as BaseTabs } from "@base-ui-components/react/tabs";
|
||||
import { LucideProps } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "../utils/classname";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
export type TabListItem = {
|
||||
key: string;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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 "../utils/classname";
|
||||
import { cn } from "@plane/utils";
|
||||
// types
|
||||
import { TabList, TabListItem } from "./list";
|
||||
|
||||
export type TabContent = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { Tooltip as BaseTooltip } from "@base-ui-components/react/tooltip";
|
||||
import { cn } from "../utils/classname";
|
||||
import { cn } from "@plane/utils";
|
||||
import { TPlacement, TSide, TAlign, convertPlacementToSideAndAlign } from "../utils/placement";
|
||||
|
||||
type ITooltipProps = {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import clsx, { type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
|
||||
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
||||
export const cn = (...inputs: ClassValue[]) => clsx(inputs);
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"@storybook/react": "^8.1.1",
|
||||
"@storybook/react-webpack5": "^8.1.1",
|
||||
"@storybook/test": "^8.1.1",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.5.2",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-color": "^3.0.9",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/uuid": "^9.0.8",
|
||||
|
||||
@@ -317,7 +317,7 @@ export const joinUrlPath = (...segments: string[]): string => {
|
||||
if (validSegments.length === 0) return "";
|
||||
|
||||
// Process segments to normalize slashes
|
||||
const processedSegments = validSegments.map((segment, index) => {
|
||||
const processedSegments = validSegments.map((segment) => {
|
||||
let processed = segment;
|
||||
|
||||
// Remove leading slashes from all segments except the first
|
||||
@@ -326,10 +326,8 @@ export const joinUrlPath = (...segments: string[]): string => {
|
||||
}
|
||||
|
||||
// Remove trailing slashes from all segments except the last
|
||||
if (index < validSegments.length - 1) {
|
||||
while (processed.endsWith("/")) {
|
||||
processed = processed.substring(0, processed.length - 1);
|
||||
}
|
||||
while (processed.endsWith("/")) {
|
||||
processed = processed.substring(0, processed.length - 1);
|
||||
}
|
||||
|
||||
return processed;
|
||||
|
||||
@@ -41,14 +41,21 @@ export const getSubscriptionName = (planVariant: EProductSubscriptionEnum): stri
|
||||
/**
|
||||
* Gets the base subscription name for upgrade/downgrade paths
|
||||
* @param planVariant - The current subscription plan variant
|
||||
* @param isSelfHosted - Whether the instance is self-hosted / community
|
||||
* @returns The name of the base subscription plan
|
||||
*
|
||||
* @remarks
|
||||
* - For self-hosted / community instances, the upgrade path differs from cloud instances
|
||||
* - Returns the immediate lower tier subscription name
|
||||
*/
|
||||
export const getBaseSubscriptionName = (planVariant: TProductSubscriptionType): string => {
|
||||
export const getBaseSubscriptionName = (planVariant: TProductSubscriptionType, isSelfHosted: boolean): string => {
|
||||
switch (planVariant) {
|
||||
case EProductSubscriptionEnum.ONE:
|
||||
return getSubscriptionName(EProductSubscriptionEnum.FREE);
|
||||
case EProductSubscriptionEnum.PRO:
|
||||
return getSubscriptionName(EProductSubscriptionEnum.FREE);
|
||||
return isSelfHosted
|
||||
? getSubscriptionName(EProductSubscriptionEnum.ONE)
|
||||
: getSubscriptionName(EProductSubscriptionEnum.FREE);
|
||||
case EProductSubscriptionEnum.BUSINESS:
|
||||
return getSubscriptionName(EProductSubscriptionEnum.PRO);
|
||||
case EProductSubscriptionEnum.ENTERPRISE:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
packages:
|
||||
- apps/*
|
||||
- packages/*
|
||||
- "!apps/api"
|
||||
- "!apps/proxy"
|
||||
- apps/*
|
||||
- packages/*
|
||||
- "!apps/api"
|
||||
- "!apps/proxy"
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- turbo
|
||||
- sharp
|
||||
- turbo
|
||||
- sharp
|
||||
|
||||
useNodeVersion: 22.18.0
|
||||
|
||||