Compare commits

..
Author SHA1 Message Date
Aaryan Khandelwal 801a40acd5 fix: web app lint issues 2025-08-28 17:40:14 +05:30
94 changed files with 582 additions and 709 deletions
+2
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -0,0 +1 @@
lts/jod
+2 -2
View File
@@ -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",
-1
View File
@@ -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,
+2 -10
View File
@@ -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)
+7 -9
View File
@@ -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)
+1 -3
View File
@@ -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"),
},
)
+24 -24
View File
@@ -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),
),
]
+1 -3
View File
@@ -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"
)
+3 -2
View File
@@ -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);
+3 -3
View File
@@ -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",
+1 -1
View File
@@ -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?.();
}
},
{
+4 -4
View File
@@ -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();
+1 -2
View File
@@ -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) {
+4 -2
View File
@@ -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;
+2 -2
View File
@@ -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",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 25 KiB

+4
View File
@@ -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",
-9
View File
@@ -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,
},
};
-5
View File
@@ -1,5 +0,0 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
+5 -1
View File
@@ -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";
+16 -4
View File
@@ -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
}
}
}
+28 -67
View File
@@ -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");
}
});
}
}
});
}
+1
View File
@@ -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";
+5 -2
View File
@@ -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;
}
+1
View File
@@ -6,6 +6,7 @@
"lib": ["ES2020"],
"rootDir": ".",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
+1 -1
View File
@@ -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"
}
+4 -3
View File
@@ -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"
}
+63 -11
View File
@@ -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,
});
+22 -10
View File
@@ -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();
};
+2 -2
View File
@@ -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:*",
+2 -1
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
import * as React from "react";
import { cn } from "../utils/classname";
import { cn } from "@plane/utils";
import {
ECardDirection,
ECardSpacing,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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} />;
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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) => (
+1 -1
View File
@@ -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;
+3 -1
View File
@@ -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 -1
View File
@@ -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 = {
+2 -3
View File
@@ -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);
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+3 -5
View File
@@ -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;
+9 -2
View File
@@ -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:
+189 -389
View File
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -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