Compare commits

..

14 Commits

Author SHA1 Message Date
pablohashescobar 7bb04003ea fix: instance trace 2024-10-07 15:56:27 +05:30
sriram veeraghanta 19dab1fad0 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-10-07 13:20:07 +05:30
M. Palanikannan 5f7b6ecf7f fix: image deletion on submit fixed in comments (#5748)
* fix: image deletion on submit fixed in comments

* fix: cleareditor added to read only editor

* fix: image component double drop fixed

* feat: multiple image selection and uploading

* fix: click event on read only instance

* fix: made things async

* fix: prevented default behaviour

* fix: removed extra dep and cleaned up logic
2024-10-07 13:12:16 +05:30
guru_sainath dfd3af13cf fix: handled favorite entity data null (#5756) 2024-10-07 12:57:15 +05:30
pablohashescobar 4cc1b79d81 chore: instance tracing 2024-10-04 21:35:13 +05:30
sriram veeraghanta 4a6f646317 fix: lockfile update 2024-10-04 19:38:19 +05:30
sriram veeraghanta b8e21d92bf Merge branch 'preview' of github.com:makeplane/plane into preview 2024-10-04 19:26:06 +05:30
sriram veeraghanta b87d5c5be6 fix: version upgrade 2024-10-04 19:25:49 +05:30
ach5948 ceda06e88d fix: Remove typo from Contributing doc (#5736) 2024-10-04 19:24:47 +05:30
sriram veeraghanta eb344881c2 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-10-04 19:22:26 +05:30
Satish Gandham 01257a6936 chore: permission layer and updated issues v1 query from workspace to project level (#5753)
Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-10-04 18:34:46 +05:30
Prateek Shourya 51b01ebcac [WEB-2580] chore: improvements for custom search select. (#5744)
* [WEB-2580] chore: improvements for custom search select.

* chore: update optionTooltip prop.

* chore: update option tooltip prop.

* chore: minor updates.
2024-10-04 17:31:09 +05:30
sriram veeraghanta 0a8d66dcc3 fix: trace information setup 2024-10-04 16:40:33 +05:30
sriram veeraghanta a5e3e4fe7d fix: api tracing 2024-10-04 01:14:29 +05:30
54 changed files with 2053 additions and 1695 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ Thank you for showing an interest in contributing to Plane! All kinds of contrib
## Submitting an issue
Before submitting a new issue, please search the [issues](https://github.com/makeplane/plane/issues) tab. Maybe an issue or discussion already exists and might inform you of workarounds. Otherwise, you can give new informplaneation.
Before submitting a new issue, please search the [issues](https://github.com/makeplane/plane/issues) tab. Maybe an issue or discussion already exists and might inform you of workarounds. Otherwise, you can give new information.
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "admin",
"version": "0.22.0",
"version": "0.23.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.22.0"
"version": "0.23.0"
}
@@ -74,7 +74,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
# Get all active workspace members
workspace_members = self.get_queryset()
if workspace_member.role > 5:
serializer = WorkspaceMemberAdminSerializer(
workspace_members,
+97
View File
@@ -0,0 +1,97 @@
# Third party imports
from celery import shared_task
from opentelemetry import trace
# Module imports
from plane.license.models import Instance
from plane.db.models import (
User,
Workspace,
Project,
Issue,
Module,
Cycle,
CycleIssue,
ModuleIssue,
Page,
WorkspaceMember,
)
@shared_task
def instance_traces():
# Get the tracer
tracer = trace.get_tracer(__name__)
# Check if the instance is registered
instance = Instance.objects.first()
# If instance is None then return
if instance is None:
return
if instance.is_telemetry_enabled:
# Instance details
with tracer.start_as_current_span("instance_details") as span:
# Count of all models
workspace_count = Workspace.objects.count()
user_count = User.objects.count()
project_count = Project.objects.count()
issue_count = Issue.objects.count()
module_count = Module.objects.count()
cycle_count = Cycle.objects.count()
cycle_issue_count = CycleIssue.objects.count()
module_issue_count = ModuleIssue.objects.count()
page_count = Page.objects.count()
# Set span attributes
span.set_attribute("instance_id", instance.instance_id)
span.set_attribute("instance_name", instance.instance_name)
span.set_attribute("current_version", instance.current_version)
span.set_attribute("latest_version", instance.latest_version)
span.set_attribute(
"is_telemetry_enabled", instance.is_telemetry_enabled
)
span.set_attribute("user_count", user_count)
span.set_attribute("workspace_count", workspace_count)
span.set_attribute("project_count", project_count)
span.set_attribute("issue_count", issue_count)
span.set_attribute("module_count", module_count)
span.set_attribute("cycle_count", cycle_count)
span.set_attribute("cycle_issue_count", cycle_issue_count)
span.set_attribute("module_issue_count", module_issue_count)
span.set_attribute("page_count", page_count)
# Workspace details
for workspace in Workspace.objects.all():
# Count of all models
project_count = Project.objects.filter(workspace=workspace).count()
issue_count = Issue.objects.filter(workspace=workspace).count()
module_count = Module.objects.filter(workspace=workspace).count()
cycle_count = Cycle.objects.filter(workspace=workspace).count()
cycle_issue_count = CycleIssue.objects.filter(
workspace=workspace
).count()
module_issue_count = ModuleIssue.objects.filter(
workspace=workspace
).count()
page_count = Page.objects.filter(workspace=workspace).count()
member_count = WorkspaceMember.objects.filter(
workspace=workspace
).count()
# Set span attributes
with tracer.start_as_current_span("workspace_details") as span:
span.set_attribute("instance_id", instance.instance_id)
span.set_attribute("workspace_id", str(workspace.id))
span.set_attribute("workspace_slug", workspace.slug)
span.set_attribute("project_count", project_count)
span.set_attribute("issue_count", issue_count)
span.set_attribute("module_count", module_count)
span.set_attribute("cycle_count", cycle_count)
span.set_attribute("cycle_issue_count", cycle_issue_count)
span.set_attribute("module_issue_count", module_issue_count)
span.set_attribute("page_count", page_count)
span.set_attribute("member_count", member_count)
return
@@ -9,7 +9,10 @@ from django.conf import settings
# Module imports
from plane.license.models import Instance
from plane.db.models import User
from plane.db.models import (
User,
)
from plane.license.bgtasks.tracer import instance_traces
class Command(BaseCommand):
@@ -21,16 +24,24 @@ class Command(BaseCommand):
"machine_signature", type=str, help="Machine signature"
)
def read_package_json(self):
with open("package.json", "r") as file:
# Load JSON content from the file
data = json.load(file)
payload = {
"instance_key": settings.INSTANCE_KEY,
"version": data.get("version", 0.1),
"user_count": User.objects.filter(is_bot=False).count(),
}
return payload
def handle(self, *args, **options):
# Check if the instance is registered
instance = Instance.objects.first()
# If instance is None then register this instance
if instance is None:
with open("package.json", "r") as file:
# Load JSON content from the file
data = json.load(file)
machine_signature = options.get(
"machine_signature", "machine-signature"
)
@@ -38,12 +49,7 @@ class Command(BaseCommand):
if not machine_signature:
raise CommandError("Machine signature is required")
payload = {
"instance_key": settings.INSTANCE_KEY,
"version": data.get("version", 0.1),
"machine_signature": machine_signature,
"user_count": User.objects.filter(is_bot=False).count(),
}
payload = self.read_package_json()
instance = Instance.objects.create(
instance_name="Plane Community Edition",
@@ -60,4 +66,15 @@ class Command(BaseCommand):
self.stdout.write(
self.style.SUCCESS("Instance already registered")
)
return
payload = self.read_package_json()
# Update the instance details
instance.last_checked_at = timezone.now()
instance.user_count = payload.get("user_count", 0)
instance.current_version = payload.get("version")
instance.latest_version = payload.get("version")
instance.save()
# Call the instance traces task
instance_traces.delay()
return
+25
View File
@@ -16,6 +16,17 @@ from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from corsheaders.defaults import default_headers
# OpenTelemetry
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter,
)
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.django import DjangoInstrumentor
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Secret Key
@@ -24,6 +35,19 @@ SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key())
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = int(os.environ.get("DEBUG", "0"))
# Initialize Django instrumentation
DjangoInstrumentor().instrument()
# Configure the tracer provider
service_name = os.environ.get("SERVICE_NAME", "plane-ce-api")
resource = Resource.create({"service.name": service_name})
trace.set_tracer_provider(TracerProvider(resource=resource))
# Configure the OTLP exporter
otel_endpoint = os.environ.get("OTLP_ENDPOINT", "https://telemetry.plane.so")
otlp_exporter = OTLPSpanExporter(endpoint=otel_endpoint)
span_processor = BatchSpanProcessor(otlp_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)
# Allowed Hosts
ALLOWED_HOSTS = ["*"]
@@ -279,6 +303,7 @@ CELERY_IMPORTS = (
"plane.bgtasks.file_asset_task",
"plane.bgtasks.email_notification_task",
"plane.bgtasks.api_logs_task",
"plane.license.bgtasks.tracer",
# management tasks
"plane.bgtasks.dummy_data_task",
)
+5
View File
@@ -62,3 +62,8 @@ zxcvbn==4.4.28
pytz==2024.1
# jwt
PyJWT==2.8.0
# OpenTelemetry
opentelemetry-api==1.27.0
opentelemetry-sdk==1.27.0
opentelemetry-instrumentation-django==0.48b0
opentelemetry-exporter-otlp==1.27.0
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "live",
"version": "0.22.0",
"version": "0.23.0",
"description": "",
"main": "./src/server.ts",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"repository": "https://github.com/makeplane/plane.git",
"version": "0.22.0",
"version": "0.23.0",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.22.0",
"version": "0.23.0",
"private": true,
"main": "./index.ts"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor",
"version": "0.22.0",
"version": "0.23.0",
"description": "Core Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
@@ -71,6 +71,17 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
const containerRect = useRef<DOMRect | null>(null);
const imageRef = useRef<HTMLImageElement>(null);
const updateAttributesSafely = useCallback(
(attributes: Partial<ImageAttributes>, errorMessage: string) => {
try {
updateAttributes(attributes);
} catch (error) {
console.error(`${errorMessage}:`, error);
}
},
[updateAttributes]
);
const handleImageLoad = useCallback(() => {
const img = imageRef.current;
if (!img) return;
@@ -105,17 +116,25 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
};
setSize(initialComputedSize);
updateAttributes(initialComputedSize);
updateAttributesSafely(
initialComputedSize,
"Failed to update attributes while initializing an image for the first time:"
);
} else {
// as the aspect ratio in not stored for old images, we need to update the attrs
setSize((prevSize) => {
const newSize = { ...prevSize, aspectRatio };
updateAttributes(newSize);
return newSize;
});
if (!aspectRatio) {
setSize((prevSize) => {
const newSize = { ...prevSize, aspectRatio };
updateAttributesSafely(
newSize,
"Failed to update attributes while initializing images with width but no aspect ratio:"
);
return newSize;
});
}
}
setInitialResizeComplete(true);
}, [width, updateAttributes, editorContainer]);
}, [width, updateAttributes, editorContainer, aspectRatio]);
// for real time resizing
useLayoutEffect(() => {
@@ -142,7 +161,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
const handleResizeEnd = useCallback(() => {
setIsResizing(false);
updateAttributes(size);
updateAttributesSafely(size, "Failed to update attributes at the end of resizing:");
}, [size, updateAttributes]);
const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => {
@@ -5,9 +5,7 @@ import { ImageIcon } from "lucide-react";
// helpers
import { cn } from "@/helpers/common";
// hooks
import { useUploader, useDropZone } from "@/hooks/use-file-upload";
// plugins
import { isFileValid } from "@/plugins/image";
import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload";
// extensions
import { getImageComponentImageFileMap, ImageAttributes } from "@/extensions/custom-image";
@@ -74,7 +72,11 @@ export const CustomImageUploader = (props: {
);
// hooks
const { uploading: isImageBeingUploaded, uploadFile } = useUploader({ onUpload, editor, loadImageFromFileSystem });
const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ uploader: uploadFile });
const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({
uploader: uploadFile,
editor,
pos: getPos(),
});
// the meta data of the image component
const meta = useMemo(
@@ -82,9 +84,6 @@ export const CustomImageUploader = (props: {
[imageComponentImageFileMap, imageEntityId]
);
// if the image component is dropped, we check if it has an existing file
const existingFile = useMemo(() => (meta && meta.event === "drop" ? meta.file : undefined), [meta]);
// after the image component is mounted we start the upload process based on
// it's uploaded
useEffect(() => {
@@ -100,27 +99,20 @@ export const CustomImageUploader = (props: {
}
}, [meta, uploadFile, imageComponentImageFileMap]);
// check if the image is dropped and set the local image as the existing file
useEffect(() => {
if (existingFile) {
uploadFile(existingFile);
}
}, [existingFile, uploadFile]);
const onFileChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
if (isFileValid(file)) {
uploadFile(file);
}
async (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const fileList = e.target.files;
if (!fileList) {
return;
}
await uploadFirstImageAndInsertRemaining(editor, fileList, getPos(), uploadFile);
},
[uploadFile]
[uploadFile, editor, getPos]
);
const getDisplayMessage = useCallback(() => {
const isUploading = isImageBeingUploaded || existingFile;
const isUploading = isImageBeingUploaded;
if (failedToLoadImage) {
return "Error loading image";
}
@@ -134,13 +126,14 @@ export const CustomImageUploader = (props: {
}
return "Add an image";
}, [draggedInside, failedToLoadImage, existingFile, isImageBeingUploaded]);
}, [draggedInside, failedToLoadImage, isImageBeingUploaded]);
return (
<div
className={cn(
"image-upload-component flex items-center justify-start gap-2 py-3 px-2 rounded-lg text-custom-text-300 hover:text-custom-text-200 bg-custom-background-90 hover:bg-custom-background-80 border border-dashed border-custom-border-300 cursor-pointer transition-all duration-200 ease-in-out",
"image-upload-component flex items-center justify-start gap-2 py-3 px-2 rounded-lg text-custom-text-300 hover:text-custom-text-200 bg-custom-background-90 hover:bg-custom-background-80 border border-dashed border-custom-border-300 transition-all duration-200 ease-in-out cursor-default",
{
"hover:text-custom-text-200 cursor-pointer": editor.isEditable,
"bg-custom-background-80 text-custom-text-200": draggedInside,
"text-custom-primary-200 bg-custom-primary-100/10 hover:bg-custom-primary-100/10 hover:text-custom-primary-200 border-custom-primary-200/10":
selected,
@@ -153,7 +146,7 @@ export const CustomImageUploader = (props: {
onDragLeave={onDragLeave}
contentEditable={false}
onClick={() => {
if (!failedToLoadImage) {
if (!failedToLoadImage && editor.isEditable) {
fileInputRef.current?.click();
}
}}
@@ -167,6 +160,7 @@ export const CustomImageUploader = (props: {
type="file"
accept=".jpg,.jpeg,.png,.webp"
onChange={onFileChange}
multiple
/>
</div>
);
+3 -10
View File
@@ -21,7 +21,7 @@ export const DropHandlerExtension = () =>
if (imageFiles.length > 0) {
const pos = view.state.selection.from;
insertImages({ editor, files: imageFiles, initialPos: pos, event: "drop" });
insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" });
}
return true;
}
@@ -41,7 +41,7 @@ export const DropHandlerExtension = () =>
if (coordinates) {
const pos = coordinates.pos;
insertImages({ editor, files: imageFiles, initialPos: pos, event: "drop" });
insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" });
}
return true;
}
@@ -54,7 +54,7 @@ export const DropHandlerExtension = () =>
},
});
const insertImages = async ({
export const insertImagesSafely = async ({
editor,
files,
initialPos,
@@ -72,13 +72,6 @@ const insertImages = async ({
const docSize = editor.state.doc.content.size;
pos = Math.min(pos, docSize);
// Check if the position has a non-empty node
const nodeAtPos = editor.state.doc.nodeAt(pos);
if (nodeAtPos && nodeAtPos.content.size > 0) {
// Move to the end of the current node
pos += nodeAtPos.nodeSize;
}
try {
// Insert the image at the current position
editor.commands.insertImageComponent({ file, pos, event });
+1 -1
View File
@@ -126,7 +126,7 @@ export const useEditor = (props: CustomEditorProps) => {
forwardedRef,
() => ({
clearEditor: (emitUpdate = false) => {
editorRef.current?.commands.clearContent(emitUpdate);
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
},
setEditorValue: (content: string) => {
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" });
@@ -1,6 +1,7 @@
import { DragEvent, useCallback, useEffect, useState } from "react";
import { Editor } from "@tiptap/core";
import { isFileValid } from "@/plugins/image";
import { insertImagesSafely } from "@/extensions/drop";
export const useUploader = ({
onUpload,
@@ -63,7 +64,15 @@ export const useUploader = ({
return { uploading, uploadFile };
};
export const useDropZone = ({ uploader }: { uploader: (file: File) => void }) => {
export const useDropZone = ({
uploader,
editor,
pos,
}: {
uploader: (file: File) => Promise<void>;
editor: Editor;
pos: number;
}) => {
const [isDragging, setIsDragging] = useState<boolean>(false);
const [draggedInside, setDraggedInside] = useState<boolean>(false);
@@ -86,40 +95,16 @@ export const useDropZone = ({ uploader }: { uploader: (file: File) => void }) =>
}, []);
const onDrop = useCallback(
(e: DragEvent<HTMLDivElement>) => {
async (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
setDraggedInside(false);
if (e.dataTransfer.files.length === 0) {
return;
}
const fileList = e.dataTransfer.files;
const files: File[] = [];
for (let i = 0; i < fileList.length; i += 1) {
const item = fileList.item(i);
if (item) {
files.push(item);
}
}
if (files.some((file) => file.type.indexOf("image") === -1)) {
return;
}
e.preventDefault();
const filteredFiles = files.filter((f) => f.type.indexOf("image") !== -1);
const file = filteredFiles.length > 0 ? filteredFiles[0] : undefined;
if (file) {
uploader(file);
} else {
console.error("No file found");
}
await uploadFirstImageAndInsertRemaining(editor, fileList, pos, uploader);
},
[uploader]
[uploader, editor, pos]
);
const onDragEnter = () => {
@@ -143,3 +128,40 @@ function trimFileName(fileName: string, maxLength = 100) {
return fileName;
}
// Upload the first image and insert the remaining images for uploading multiple image
// post insertion of image-component
export async function uploadFirstImageAndInsertRemaining(
editor: Editor,
fileList: FileList,
pos: number,
uploaderFn: (file: File) => Promise<void>
) {
const filteredFiles: File[] = [];
for (let i = 0; i < fileList.length; i += 1) {
const item = fileList.item(i);
if (item && item.type.indexOf("image") !== -1 && isFileValid(item)) {
filteredFiles.push(item);
}
}
if (filteredFiles.length !== fileList.length) {
console.warn("Some files were not images and have been ignored.");
}
if (filteredFiles.length === 0) {
console.error("No image files found to upload");
return;
}
// Upload the first image
const firstFile = filteredFiles[0];
uploaderFn(firstFile);
// Insert the remaining images
const remainingFiles = filteredFiles.slice(1);
if (remainingFiles.length > 0) {
const docSize = editor.state.doc.content.size;
const posOfNextImageToBeInserted = Math.min(pos + 1, docSize);
insertImagesSafely({ editor, files: remainingFiles, initialPos: posOfNextImageToBeInserted, event: "drop" });
}
}
@@ -70,8 +70,8 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
const editorRef: MutableRefObject<Editor | null> = useRef(null);
useImperativeHandle(forwardedRef, () => ({
clearEditor: () => {
editorRef.current?.commands.clearContent();
clearEditor: (emitUpdate = false) => {
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
},
setEditorValue: (content: string) => {
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" });
@@ -17,6 +17,8 @@ export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImag
});
transactions.forEach((transaction) => {
// if the transaction has meta of skipImageDeletion get to true, then return (like while clearing the editor content programatically)
if (transaction.getMeta("skipImageDeletion")) return;
// transaction could be a selection
if (!transaction.docChanged) return;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@plane/eslint-config",
"private": true,
"version": "0.22.0",
"version": "0.23.0",
"files": [
"library.js",
"next.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/helpers",
"version": "0.22.0",
"version": "0.23.0",
"description": "Helper functions shared across multiple apps internally",
"private": true,
"main": "./dist/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tailwind-config-custom",
"version": "0.22.0",
"version": "0.23.0",
"description": "common tailwind configuration across monorepo",
"main": "index.js",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "0.22.0",
"version": "0.23.0",
"private": true,
"types": "./src/index.d.ts",
"main": "./src/index.d.ts"
+1 -2
View File
@@ -77,8 +77,7 @@ export type TIssueParams =
| "show_empty_groups"
| "cursor"
| "per_page"
| "issue_type"
| "layout";
| "issue_type";
export type TCalendarLayouts = "month" | "week";
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/typescript-config",
"version": "0.22.0",
"version": "0.23.0",
"private": true,
"files": [
"base.json",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
"version": "0.22.0",
"version": "0.23.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
@@ -1,7 +1,7 @@
import React, { useRef, useState } from "react";
import { usePopper } from "react-popper";
import { Combobox } from "@headlessui/react";
import { Check, ChevronDown, Search } from "lucide-react";
import { Check, ChevronDown, Info, Search } from "lucide-react";
import { createPortal } from "react-dom";
// plane helpers
import { useOutsideClickDetector } from "@plane/helpers";
@@ -11,6 +11,8 @@ import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
import { cn } from "../../helpers";
// types
import { ICustomSearchSelectProps } from "./helper";
// local components
import { Tooltip } from "../tooltip";
export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
const {
@@ -95,11 +97,10 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
<button
ref={setReferenceElement}
type="button"
className={`flex w-full items-center justify-between gap-1 text-xs ${
disabled
? "cursor-not-allowed text-custom-text-200"
: "cursor-pointer hover:bg-custom-background-80"
} ${customButtonClassName}`}
className={`flex w-full items-center justify-between gap-1 text-xs ${disabled
? "cursor-not-allowed text-custom-text-200"
: "cursor-pointer hover:bg-custom-background-80"
} ${customButtonClassName}`}
onClick={toggleDropdown}
>
{customButton}
@@ -170,17 +171,32 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
"w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none",
{
"bg-custom-background-80": active,
"text-custom-text-400 opacity-60 cursor-not-allowed": option.disabled,
}
)
}
onClick={() => {
if (!multiple) closeDropdown();
}}
disabled={option.disabled}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
{option.tooltip && (
<>
{
typeof option.tooltip === "string" ? (
<Tooltip tooltipContent={option.tooltip}>
<Info className="h-3.5 w-3.5 flex-shrink-0 cursor-pointer text-custom-text-200" />
</Tooltip>
) : (
option.tooltip
)
}
</>
)}
</>
)}
</Combobox.Option>
+8 -6
View File
@@ -44,12 +44,14 @@ interface CustomSearchSelectProps {
onChange: any;
onClose?: () => void;
options:
| {
value: any;
query: string;
content: React.ReactNode;
}[]
| undefined;
| {
value: any;
query: string;
content: React.ReactNode;
disabled?: boolean;
tooltip?: string | React.ReactNode;
}[]
| undefined;
}
interface SingleValueProps {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.22.0",
"version": "0.23.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -1,12 +1,23 @@
import { Control } from "react-hook-form";
// types
import { TIssue } from "@plane/types";
import { TBulkIssueProperties, TIssue } from "@plane/types";
type TIssueTypeSelectProps = {
control: Control<TIssue>;
export type TIssueFields = TIssue & TBulkIssueProperties;
export type TIssueTypeDropdownVariant = "xs" | "sm";
export type TIssueTypeSelectProps<T extends Partial<TIssueFields>> = {
control: Control<T>;
projectId: string | null;
disabled?: boolean;
handleFormChange: () => void;
variant?: TIssueTypeDropdownVariant;
placeholder?: string;
isRequired?: boolean;
renderChevron?: boolean;
dropDownContainerClassName?: string;
showMandatoryFieldInfo?: boolean; // Show info about mandatory fields
handleFormChange?: () => void;
};
export const IssueTypeSelect: React.FC<TIssueTypeSelectProps> = () => <></>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const IssueTypeSelect = <T extends Partial<TIssueFields>>(props: TIssueTypeSelectProps<T>) => <></>;
@@ -4,7 +4,6 @@ import { useParams } from "next/navigation";
import useSWR from "swr";
// mobx store
// components
import { LogoSpinner } from "@/components/common";
import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot, IssuePeekOverview } from "@/components/issues";
import { EIssuesStoreType } from "@/constants/issue";
// ui
@@ -17,7 +16,7 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
// hooks
const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
const { isLoading } = useSWR(
useSWR(
workspaceSlug && projectId ? `ARCHIVED_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
async () => {
if (workspaceSlug && projectId) {
@@ -27,17 +26,7 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const issueFilters = issuesFilter?.getIssueFilters(projectId?.toString());
if (!workspaceSlug || !projectId) return <></>;
if (isLoading && !issueFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.ARCHIVED}>
<ArchivedIssueAppliedFiltersRoot />
@@ -5,7 +5,6 @@ import { useParams } from "next/navigation";
import useSWR from "swr";
// hooks
// components
import { LogoSpinner } from "@/components/common";
import { TransferIssues, TransferIssuesModal } from "@/components/cycles";
import {
CycleAppliedFiltersRoot,
@@ -51,7 +50,7 @@ export const CycleLayoutRoot: React.FC = observer(() => {
// state
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
const { isLoading } = useSWR(
useSWR(
workspaceSlug && projectId && cycleId
? `CYCLE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${cycleId.toString()}`
: null,
@@ -63,8 +62,7 @@ export const CycleLayoutRoot: React.FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const issueFilters = issuesFilter?.getIssueFilters(cycleId?.toString());
const activeLayout = issueFilters?.displayFilters?.layout;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase() ?? "draft";
@@ -77,13 +75,6 @@ export const CycleLayoutRoot: React.FC = observer(() => {
if (!workspaceSlug || !projectId || !cycleId) return <></>;
if (isLoading && !issueFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.CYCLE}>
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
@@ -2,7 +2,6 @@ import React from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
import { LogoSpinner } from "@/components/common";
import { IssuePeekOverview } from "@/components/issues/peek-overview";
import { EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
// hooks
@@ -31,7 +30,7 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
// hooks
const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT);
const { isLoading } = useSWR(
useSWR(
workspaceSlug && projectId ? `DRAFT_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
async () => {
if (workspaceSlug && projectId) {
@@ -41,18 +40,10 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const issueFilters = issuesFilter?.getIssueFilters(projectId?.toString());
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
if (!workspaceSlug || !projectId) return <></>;
if (isLoading && !issueFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.DRAFT}>
<div className="relative flex h-full w-full flex-col overflow-hidden">
@@ -5,7 +5,6 @@ import useSWR from "swr";
// mobx store
// components
import { Row, ERowVariant } from "@plane/ui";
import { LogoSpinner } from "@/components/common";
import {
IssuePeekOverview,
ModuleAppliedFiltersRoot,
@@ -44,7 +43,7 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
// hooks
const { issuesFilter } = useIssues(EIssuesStoreType.MODULE);
const { isLoading } = useSWR(
useSWR(
workspaceSlug && projectId && moduleId
? `MODULE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${moduleId.toString()}`
: null,
@@ -56,18 +55,9 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const issueFilters = issuesFilter?.getIssueFilters(moduleId?.toString());
if (!workspaceSlug || !projectId || !moduleId) return <></>;
if (isLoading && !issueFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.MODULE}>
@@ -6,7 +6,6 @@ import { useParams } from "next/navigation";
import useSWR from "swr";
// components
import { Spinner } from "@plane/ui";
import { LogoSpinner } from "@/components/common";
import {
ListLayout,
CalendarLayout,
@@ -45,7 +44,7 @@ export const ProjectLayoutRoot: FC = observer(() => {
// hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
const { isLoading } = useSWR(
useSWR(
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
async () => {
if (workspaceSlug && projectId) {
@@ -55,18 +54,10 @@ export const ProjectLayoutRoot: FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const issueFilters = issuesFilter?.getIssueFilters(projectId?.toString());
const activeLayout = issueFilters?.displayFilters?.layout;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
if (!workspaceSlug || !projectId) return <></>;
if (isLoading && !issueFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.PROJECT}>
<div className="relative flex h-full w-full flex-col overflow-hidden">
@@ -53,12 +53,11 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const issueFilters = issuesFilter?.getIssueFilters(viewId?.toString());
const activeLayout = issueFilters?.displayFilters?.layout;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
if (!workspaceSlug || !projectId || !viewId) return <></>;
if (isLoading && !issueFilters) {
if (isLoading) {
return (
<div className="relative flex h-screen w-full items-center justify-center">
<LogoSpinner />
@@ -280,6 +280,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
projectId={projectId}
disabled={!!data?.sourceIssueId}
handleFormChange={handleFormChange}
renderChevron
/>
)}
</div>
@@ -182,7 +182,7 @@ export const SidebarFavoritesMenu = observer(() => {
.map((fav, index) => (
<Tooltip
key={fav.id}
tooltipContent={fav.entity_data ? fav.entity_data.name : fav.name}
tooltipContent={fav?.entity_data ? fav.entity_data?.name : fav?.name}
position="right"
className="ml-2"
disabled={!sidebarCollapsed}
+1 -1
View File
@@ -13,7 +13,7 @@ import { useProject, usePage, useProjectView, useCycle, useModule } from "@/hook
export const useFavoriteItemDetails = (workspaceSlug: string, favorite: IFavorite) => {
const favoriteItemId = favorite?.entity_data?.id;
const favoriteItemLogoProps = favorite?.entity_data?.logo_props;
const favoriteItemName = favorite?.entity_data.name || favorite?.name;
const favoriteItemName = favorite?.entity_data?.name || favorite?.name;
const favoriteItemEntityType = favorite?.entity_type;
// store hooks
+4 -4
View File
@@ -148,7 +148,7 @@ export class Storage {
};
syncWorkspace = async () => {
if (document.hidden || !rootStore.user.localDBEnabled || !this.db) return; // return if the window gets hidden
if (document.hidden || !rootStore.user.localDBEnabled) return; // return if the window gets hidden
await Sentry.startSpan({ name: "LOAD_WS", attributes: { slug: this.workspaceSlug } }, async () => {
await loadWorkSpaceData(this.workspaceSlug);
});
@@ -173,7 +173,7 @@ export class Storage {
};
syncIssues = async (projectId: string) => {
if (document.hidden || !rootStore.user.localDBEnabled || !this.db) return false; // return if the window gets hidden
if (document.hidden || !rootStore.user.localDBEnabled) return false; // return if the window gets hidden
try {
const sync = Sentry.startSpan({ name: `SYNC_ISSUES` }, () => this._syncIssues(projectId));
@@ -191,7 +191,7 @@ export class Storage {
log("### Sync started");
let status = this.getStatus(projectId);
if (status === "loading" || status === "syncing") {
log(`Project ${projectId} is already loading or syncing`);
logInfo(`Project ${projectId} is already loading or syncing`);
return;
}
const syncPromise = this.getSync(projectId);
@@ -290,7 +290,7 @@ export class Storage {
!rootStore.user.localDBEnabled
) {
if (rootStore.user.localDBEnabled) {
log(`Project ${projectId} is loading, falling back to server`);
logInfo(`Project ${projectId} is loading, falling back to server`);
}
const issueService = new IssueService();
return await issueService.getIssuesFromServer(workspaceSlug, projectId, queries);
+5 -5
View File
@@ -8,7 +8,7 @@ import { issueSchema } from "./schemas";
export const PROJECT_OFFLINE_STATUS: Record<string, boolean> = {};
export const addIssue = async (issue: any) => {
if (document.hidden || !rootStore.user.localDBEnabled || !persistence.db) return;
if (document.hidden || !rootStore.user.localDBEnabled) return;
persistence.db.exec("BEGIN TRANSACTION;");
stageIssueInserts(issue);
@@ -16,7 +16,7 @@ export const addIssue = async (issue: any) => {
};
export const addIssuesBulk = async (issues: any, batchSize = 100) => {
if (!rootStore.user.localDBEnabled || !persistence.db) return;
if (!rootStore.user.localDBEnabled) return;
for (let i = 0; i < issues.length; i += batchSize) {
const batch = issues.slice(i, i + batchSize);
@@ -32,7 +32,7 @@ export const addIssuesBulk = async (issues: any, batchSize = 100) => {
}
};
export const deleteIssueFromLocal = async (issue_id: any) => {
if (!rootStore.user.localDBEnabled || !persistence.db) return;
if (!rootStore.user.localDBEnabled) return;
const deleteQuery = `delete from issues where id='${issue_id}'`;
const deleteMetaQuery = `delete from issue_meta where issue_id='${issue_id}'`;
@@ -44,7 +44,7 @@ export const deleteIssueFromLocal = async (issue_id: any) => {
};
// @todo: Update deletes the issue description from local. Implement a separate update.
export const updateIssue = async (issue: TIssue & { is_local_update: number }) => {
if (document.hidden || !rootStore.user.localDBEnabled || !persistence.db) return;
if (document.hidden || !rootStore.user.localDBEnabled) return;
const issue_id = issue.id;
// delete the issue and its meta data
@@ -53,7 +53,7 @@ export const updateIssue = async (issue: TIssue & { is_local_update: number }) =
};
export const syncDeletesToLocal = async (workspaceId: string, projectId: string, queries: any) => {
if (!rootStore.user.localDBEnabled || !persistence.db) return;
if (!rootStore.user.localDBEnabled) return;
const issueService = new IssueService();
const response = await issueService.getDeletedIssues(workspaceId, projectId, queries);
+3 -1
View File
@@ -1,7 +1,9 @@
// import { SQL } from "./sqlite";
import { persistence } from "../storage.sqlite";
export const runQuery = async (sql: string) => {
const data = await persistence.db?.exec({
const data = await persistence.db.exec({
sql,
rowMode: "object",
returnValue: "resultRows",
+3 -15
View File
@@ -4,20 +4,8 @@ import { issueSchema } from "./schemas";
import { wrapDateTime } from "./utils";
export const translateQueryParams = (queries: any) => {
const {
group_by,
layout,
sub_group_by,
labels,
assignees,
state,
cycle,
module,
priority,
type,
issue_type,
...otherProps
} = queries;
const { group_by, sub_group_by, labels, assignees, state, cycle, module, priority, type, issue_type, ...otherProps } =
queries;
const order_by = queries.order_by;
if (state) otherProps.state_id = state;
@@ -45,7 +33,7 @@ export const translateQueryParams = (queries: any) => {
}
// Fix invalid orderby when switching from spreadsheet layout
if (layout === "spreadsheet" && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
if ((group_by || sub_group_by) && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
otherProps.order_by = "sort_order";
}
// For each property value, replace None with empty string
+1 -1
View File
@@ -15,7 +15,7 @@ export const logError = (e: any) => {
e = parseSQLite3Error(e);
}
Sentry.captureException(e);
console.error(e);
console.log(e);
};
export const logInfo = console.info;
@@ -33,7 +33,6 @@ export interface IArchivedIssuesFilter extends IBaseIssueFilterStore {
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
getIssueFilters(projectId: string): IIssueFilters | undefined;
// action
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
updateFilters: (
@@ -31,7 +31,6 @@ export interface ICycleIssuesFilter extends IBaseIssueFilterStore {
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
getIssueFilters(cycleId: string): IIssueFilters | undefined;
// action
fetchFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
updateFilters: (
@@ -33,7 +33,6 @@ export interface IDraftIssuesFilter extends IBaseIssueFilterStore {
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
getIssueFilters(projectId: string): IIssueFilters | undefined;
// action
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
updateFilters: (
@@ -13,7 +13,7 @@ import {
TStaticViewTypes,
} from "@plane/types";
// constants
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
// helpers
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper";
// lib
@@ -114,8 +114,6 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
: nonEmptyArrayValue;
});
if (displayFilters?.layout) issueFiltersParams.layout = displayFilters?.layout;
return issueFiltersParams;
};
@@ -31,7 +31,6 @@ export interface IModuleIssuesFilter extends IBaseIssueFilterStore {
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
getIssueFilters(moduleId: string): IIssueFilters | undefined;
// action
fetchFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>;
updateFilters: (
@@ -31,7 +31,6 @@ export interface IProjectViewIssuesFilter extends IBaseIssueFilterStore {
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
getIssueFilters(viewId: string): IIssueFilters | undefined;
// action
fetchFilters: (workspaceSlug: string, projectId: string, viewId: string) => Promise<void>;
updateFilters: (
@@ -265,16 +264,9 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I
const currentUserId = this.rootIssueStore.currentUserId;
if (currentUserId)
this.handleIssuesLocalFilters.set(
EIssuesStoreType.PROJECT_VIEW,
type,
workspaceSlug,
viewId,
currentUserId,
{
kanban_filters: _filters.kanbanFilters,
}
);
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROJECT_VIEW, type, workspaceSlug, viewId, currentUserId, {
kanban_filters: _filters.kanbanFilters,
});
runInAction(() => {
Object.keys(updatedKanbanFilters).forEach((_key) => {
@@ -31,7 +31,6 @@ export interface IProjectIssuesFilter extends IBaseIssueFilterStore {
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
getIssueFilters(projectId: string): IIssueFilters | undefined;
// action
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
updateFilters: (
+2
View File
@@ -20,6 +20,8 @@ const nextConfig = {
key: "Referrer-Policy",
value: "origin-when-cross-origin",
},
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
{ key: "Cross-Origin-Embedder-Policy", value: "credentialless" },
],
},
];
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.22.0",
"version": "0.23.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
+1694 -1466
View File
File diff suppressed because it is too large Load Diff