Sync: Community Changes #5027
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"description": "Admin UI for Plane",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"description": "API server powering Plane's backend"
|
||||
|
||||
@@ -981,7 +981,7 @@ class DuplicateAssetEndpoint(BaseAPIView):
|
||||
return {"entity_identifier": entity_id}
|
||||
return {}
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def post(self, request, slug, asset_id):
|
||||
project_id = request.data.get("project_id", None)
|
||||
entity_id = request.data.get("entity_id", None)
|
||||
@@ -1000,7 +1000,7 @@ class DuplicateAssetEndpoint(BaseAPIView):
|
||||
return Response({"error": "Project not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
storage = S3Storage(request=request)
|
||||
original_asset = FileAsset.objects.filter(workspace=workspace, id=asset_id, is_uploaded=True).first()
|
||||
original_asset = FileAsset.objects.filter(id=asset_id, is_uploaded=True).first()
|
||||
|
||||
if not original_asset:
|
||||
return Response({"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Python imports
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
# Third party imports
|
||||
from pymongo.collection import Collection
|
||||
from celery import shared_task
|
||||
|
||||
# Django imports
|
||||
from plane.settings.mongo import MongoConnection
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.db.models import APIActivityLog
|
||||
|
||||
|
||||
logger = logging.getLogger("plane.worker")
|
||||
|
||||
|
||||
def get_mongo_collection() -> Optional[Collection]:
|
||||
"""
|
||||
Returns the MongoDB collection for external API activity logs.
|
||||
"""
|
||||
if not MongoConnection.is_configured():
|
||||
logger.info("MongoDB not configured")
|
||||
return None
|
||||
|
||||
try:
|
||||
return MongoConnection.get_collection("api_activity_logs")
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting MongoDB collection: {str(e)}")
|
||||
log_exception(e)
|
||||
return None
|
||||
|
||||
|
||||
def safe_decode_body(content: bytes) -> Optional[str]:
|
||||
"""
|
||||
Safely decodes request/response body content, handling binary data.
|
||||
Returns "[Binary Content]" if the content is binary, or a string representation of the content.
|
||||
Returns None if the content is None or empty.
|
||||
"""
|
||||
# If the content is None, return None
|
||||
if content is None:
|
||||
return None
|
||||
|
||||
# If the content is an empty bytes object, return None
|
||||
if content == b"":
|
||||
return None
|
||||
|
||||
# Check if content is binary by looking for common binary file signatures
|
||||
if content.startswith(b"\x89PNG") or content.startswith(b"\xff\xd8\xff") or content.startswith(b"%PDF"):
|
||||
return "[Binary Content]"
|
||||
|
||||
try:
|
||||
return content.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return "[Could not decode content]"
|
||||
|
||||
|
||||
def log_to_mongo(log_document: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Logs the request to MongoDB if available.
|
||||
"""
|
||||
mongo_collection = get_mongo_collection()
|
||||
if mongo_collection is None:
|
||||
logger.error("MongoDB not configured")
|
||||
return False
|
||||
|
||||
try:
|
||||
mongo_collection.insert_one(log_document)
|
||||
return True
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return False
|
||||
|
||||
|
||||
def log_to_postgres(log_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Fallback to logging to PostgreSQL if MongoDB is unavailable.
|
||||
"""
|
||||
try:
|
||||
APIActivityLog.objects.create(**log_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return False
|
||||
|
||||
|
||||
@shared_task
|
||||
def process_logs(log_data: Dict[str, Any], mongo_log: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Process logs to save to MongoDB or Postgres based on the configuration
|
||||
"""
|
||||
|
||||
if MongoConnection.is_configured():
|
||||
log_to_mongo(mongo_log)
|
||||
else:
|
||||
log_to_postgres(log_data)
|
||||
@@ -237,3 +237,30 @@ class ChangeTrackerMixin:
|
||||
all non-deferred fields).
|
||||
"""
|
||||
return self._original_values
|
||||
|
||||
def save(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""
|
||||
Override save to automatically capture changed fields and reset tracking.
|
||||
|
||||
Before saving, the current changed_fields are captured and stored in
|
||||
_changes_on_save. After saving, the tracked fields are reset so
|
||||
that subsequent saves correctly detect changes relative to the last
|
||||
saved state, not the original load-time state.
|
||||
|
||||
Models that need to access the changed fields after save (e.g., for
|
||||
syncing related models) can use self._changes_on_save.
|
||||
"""
|
||||
self._changes_on_save = self.changed_fields
|
||||
super().save(*args, **kwargs)
|
||||
self._reset_tracked_fields()
|
||||
|
||||
def _reset_tracked_fields(self) -> None:
|
||||
"""
|
||||
Reset the tracked field values to the current state.
|
||||
|
||||
This is called automatically after save() to ensure that subsequent
|
||||
saves correctly detect changes relative to the last saved state,
|
||||
rather than the original load-time state.
|
||||
"""
|
||||
self._original_values = {}
|
||||
self._track_fields()
|
||||
|
||||
@@ -570,10 +570,12 @@ class IssueComment(ChangeTrackerMixin, ProjectBaseModel):
|
||||
"comment_json": "description_json",
|
||||
}
|
||||
|
||||
# Use _changes_on_save which is captured by ChangeTrackerMixin.save()
|
||||
# before the tracked fields are reset
|
||||
changed_fields = {
|
||||
desc_field: getattr(self, comment_field)
|
||||
for comment_field, desc_field in field_mapping.items()
|
||||
if self.has_changed(comment_field)
|
||||
if comment_field in self._changes_on_save
|
||||
}
|
||||
|
||||
# Update description only if comment fields changed
|
||||
|
||||
@@ -4,13 +4,15 @@ import time
|
||||
|
||||
# Django imports
|
||||
from django.http import HttpRequest
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.request import Request
|
||||
|
||||
# Module imports
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
from plane.db.models import APIActivityLog
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.bgtasks.logger_task import process_logs
|
||||
|
||||
api_logger = logging.getLogger("plane.api.request")
|
||||
|
||||
@@ -70,6 +72,10 @@ class RequestLoggerMiddleware:
|
||||
|
||||
|
||||
class APITokenLogMiddleware:
|
||||
"""
|
||||
Middleware to log External API requests to MongoDB or PostgreSQL.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
@@ -104,24 +110,41 @@ class APITokenLogMiddleware:
|
||||
def process_request(self, request, response, request_body):
|
||||
api_key_header = "X-Api-Key"
|
||||
api_key = request.headers.get(api_key_header)
|
||||
# If the API key is present, log the request
|
||||
if api_key:
|
||||
try:
|
||||
APIActivityLog.objects.create(
|
||||
token_identifier=api_key,
|
||||
path=request.path,
|
||||
method=request.method,
|
||||
query_params=request.META.get("QUERY_STRING", ""),
|
||||
headers=str(request.headers),
|
||||
body=(self._safe_decode_body(request_body) if request_body else None),
|
||||
response_body=(self._safe_decode_body(response.content) if response.content else None),
|
||||
response_code=response.status_code,
|
||||
ip_address=get_client_ip(request=request),
|
||||
user_agent=request.META.get("HTTP_USER_AGENT", None),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
api_logger.exception(e)
|
||||
# If the token does not exist, you can decide whether to log this as an invalid attempt
|
||||
# If the API key is not present, return
|
||||
if not api_key:
|
||||
return
|
||||
|
||||
try:
|
||||
log_data = {
|
||||
"token_identifier": api_key,
|
||||
"path": request.path,
|
||||
"method": request.method,
|
||||
"query_params": request.META.get("QUERY_STRING", ""),
|
||||
"headers": str(request.headers),
|
||||
"body": self._safe_decode_body(request_body) if request_body else None,
|
||||
"response_body": self._safe_decode_body(response.content) if response.content else None,
|
||||
"response_code": response.status_code,
|
||||
"ip_address": get_client_ip(request=request),
|
||||
"user_agent": request.META.get("HTTP_USER_AGENT", None),
|
||||
}
|
||||
user_id = (
|
||||
str(request.user.id)
|
||||
if getattr(request, "user") and getattr(request.user, "is_authenticated", False)
|
||||
else None
|
||||
)
|
||||
# Additional fields for MongoDB
|
||||
mongo_log = {
|
||||
**log_data,
|
||||
"created_at": timezone.now(),
|
||||
"updated_at": timezone.now(),
|
||||
"created_by": user_id,
|
||||
"updated_by": user_id,
|
||||
}
|
||||
|
||||
process_logs.delay(log_data=log_data, mongo_log=mongo_log)
|
||||
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
|
||||
return None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "live",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "A realtime collaborative server powers Plane's rich text editor",
|
||||
"main": "./dist/start.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -18,7 +18,7 @@ import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper";
|
||||
// plane web helpers
|
||||
import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper";
|
||||
|
||||
function CreateWorkspacePage() {
|
||||
const CreateWorkspacePage = observer(function CreateWorkspacePage() {
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
@@ -104,6 +104,6 @@ function CreateWorkspacePage() {
|
||||
</div>
|
||||
</AuthenticationWrapper>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default observer(CreateWorkspacePage);
|
||||
export default CreateWorkspacePage;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./dependency";
|
||||
export * from "./layers";
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
itemsContainerWidth: number;
|
||||
blockCount: number;
|
||||
};
|
||||
|
||||
export const GanttAdditionalLayers: FC<Props> = () => null;
|
||||
@@ -0,0 +1 @@
|
||||
export { GanttAdditionalLayers } from "./additional-layers";
|
||||
@@ -59,13 +59,12 @@ export const DescriptionVersionsModal = observer(function DescriptionVersionsMod
|
||||
|
||||
const handleCopyMarkdown = useCallback(() => {
|
||||
if (!editorRef.current) return;
|
||||
copyTextToClipboard(editorRef.current.getMarkDown()).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("toast.success"),
|
||||
message: "Markdown copied to clipboard.",
|
||||
})
|
||||
);
|
||||
editorRef.current.copyMarkdownToClipboard();
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("toast.success"),
|
||||
message: "Markdown copied to clipboard.",
|
||||
});
|
||||
}, [t]);
|
||||
|
||||
if (!workspaceId) return null;
|
||||
|
||||
@@ -17,7 +17,11 @@ import { GanttChartSidebar, MonthChartView, QuarterChartView, WeekChartView } fr
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
// plane web components
|
||||
import { TimelineDependencyPaths, TimelineDraggablePath } from "@/plane-web/components/gantt-chart";
|
||||
import {
|
||||
TimelineDependencyPaths,
|
||||
TimelineDraggablePath,
|
||||
GanttAdditionalLayers,
|
||||
} from "@/plane-web/components/gantt-chart";
|
||||
import { GanttChartRowList } from "@/plane-web/components/gantt-chart/blocks/block-row-list";
|
||||
import { GanttChartBlocksList } from "@/plane-web/components/gantt-chart/blocks/blocks-list";
|
||||
import { IssueBulkOperationsRoot } from "@/plane-web/components/issues/bulk-operations";
|
||||
@@ -212,6 +216,7 @@ export const GanttChartMainContent = observer(function GanttChartMainContent(pro
|
||||
/>
|
||||
<TimelineDependencyPaths isEpic={isEpic} />
|
||||
<TimelineDraggablePath />
|
||||
<GanttAdditionalLayers itemsContainerWidth={itemsContainerWidth} blockCount={blockIds.length} />
|
||||
<GanttChartBlocksList
|
||||
blockIds={blockIds}
|
||||
blockToRender={blockToRender}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { CircleCheck } from "lucide-react";
|
||||
@@ -71,47 +71,46 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({
|
||||
const handleCreateWorkspace = async (formData: IWorkspace) => {
|
||||
if (isSubmitting) return;
|
||||
|
||||
await workspaceService
|
||||
.workspaceSlugCheck(formData.slug)
|
||||
.then(async (res) => {
|
||||
if (res.status === true && !RESTRICTED_URLS.includes(formData.slug)) {
|
||||
setSlugError(false);
|
||||
await createWorkspace(formData)
|
||||
.then(async (workspaceResponse) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("workspace_creation.toast.success.title"),
|
||||
message: t("workspace_creation.toast.success.message"),
|
||||
});
|
||||
captureSuccess({
|
||||
eventName: WORKSPACE_TRACKER_EVENTS.create,
|
||||
payload: { slug: formData.slug },
|
||||
});
|
||||
await fetchWorkspaces();
|
||||
await completeStep(workspaceResponse.id);
|
||||
onComplete(formData.organization_size === "Just myself");
|
||||
})
|
||||
.catch(() => {
|
||||
captureError({
|
||||
eventName: WORKSPACE_TRACKER_EVENTS.create,
|
||||
payload: { slug: formData.slug },
|
||||
error: new Error("Error creating workspace"),
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("workspace_creation.toast.error.title"),
|
||||
message: t("workspace_creation.toast.error.message"),
|
||||
});
|
||||
});
|
||||
} else setSlugError(true);
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("workspace_creation.toast.error.title"),
|
||||
message: t("workspace_creation.toast.error.message"),
|
||||
})
|
||||
);
|
||||
try {
|
||||
const res = (await workspaceService.workspaceSlugCheck(formData.slug)) as { status: boolean };
|
||||
if (res.status === true && !RESTRICTED_URLS.includes(formData.slug)) {
|
||||
setSlugError(false);
|
||||
try {
|
||||
const workspaceResponse = await createWorkspace(formData);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("workspace_creation.toast.success.title"),
|
||||
message: t("workspace_creation.toast.success.message"),
|
||||
});
|
||||
captureSuccess({
|
||||
eventName: WORKSPACE_TRACKER_EVENTS.create,
|
||||
payload: { slug: formData.slug },
|
||||
});
|
||||
await fetchWorkspaces();
|
||||
await completeStep(workspaceResponse.id);
|
||||
onComplete(formData.organization_size === "Just myself");
|
||||
} catch {
|
||||
captureError({
|
||||
eventName: WORKSPACE_TRACKER_EVENTS.create,
|
||||
payload: { slug: formData.slug },
|
||||
error: new Error("Error creating workspace"),
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("workspace_creation.toast.error.title"),
|
||||
message: t("workspace_creation.toast.error.message"),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setSlugError(true);
|
||||
}
|
||||
} catch {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("workspace_creation.toast.error.title"),
|
||||
message: t("workspace_creation.toast.error.message"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const completeStep = async (workspaceId: string) => {
|
||||
@@ -136,7 +135,12 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<form className="flex flex-col gap-10" onSubmit={handleSubmit(handleCreateWorkspace)}>
|
||||
<form
|
||||
className="flex flex-col gap-10"
|
||||
onSubmit={(e) => {
|
||||
void handleSubmit(handleCreateWorkspace)(e);
|
||||
}}
|
||||
>
|
||||
<CommonOnboardingHeader title="Create your workspace" description="All your work — unified." />
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -181,6 +185,7 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({
|
||||
"border-red-500": errors.name,
|
||||
}
|
||||
)}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -71,13 +71,12 @@ export const PageOptionsDropdown = observer(function PageOptionsDropdown(props:
|
||||
key: "copy-markdown",
|
||||
action: () => {
|
||||
if (!editorRef) return;
|
||||
copyTextToClipboard(editorRef.getMarkDown()).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Markdown copied to clipboard.",
|
||||
})
|
||||
);
|
||||
editorRef.copyMarkdownToClipboard();
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Markdown copied to clipboard.",
|
||||
});
|
||||
},
|
||||
title: "Copy markdown",
|
||||
icon: Clipboard,
|
||||
|
||||
@@ -67,47 +67,46 @@ export const CreateWorkspaceForm = observer(function CreateWorkspaceForm(props:
|
||||
} = useForm<IWorkspace>({ defaultValues, mode: "onChange" });
|
||||
|
||||
const handleCreateWorkspace = async (formData: IWorkspace) => {
|
||||
await workspaceService
|
||||
.workspaceSlugCheck(formData.slug)
|
||||
.then(async (res) => {
|
||||
if (res.status === true && !RESTRICTED_URLS.includes(formData.slug)) {
|
||||
setSlugError(false);
|
||||
try {
|
||||
const res = (await workspaceService.workspaceSlugCheck(formData.slug)) as { status: boolean };
|
||||
if (res.status === true && !RESTRICTED_URLS.includes(formData.slug)) {
|
||||
setSlugError(false);
|
||||
|
||||
await createWorkspace(formData)
|
||||
.then(async (res) => {
|
||||
captureSuccess({
|
||||
eventName: WORKSPACE_TRACKER_EVENTS.create,
|
||||
payload: { slug: formData.slug },
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("workspace_creation.toast.success.title"),
|
||||
message: t("workspace_creation.toast.success.message"),
|
||||
});
|
||||
try {
|
||||
const workspaceResponse = await createWorkspace(formData);
|
||||
captureSuccess({
|
||||
eventName: WORKSPACE_TRACKER_EVENTS.create,
|
||||
payload: { slug: formData.slug },
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("workspace_creation.toast.success.title"),
|
||||
message: t("workspace_creation.toast.success.message"),
|
||||
});
|
||||
|
||||
if (onSubmit) await onSubmit(res);
|
||||
})
|
||||
.catch(() => {
|
||||
captureError({
|
||||
eventName: WORKSPACE_TRACKER_EVENTS.create,
|
||||
payload: { slug: formData.slug },
|
||||
error: new Error("Error creating workspace"),
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("workspace_creation.toast.error.title"),
|
||||
message: t("workspace_creation.toast.error.message"),
|
||||
});
|
||||
});
|
||||
} else setSlugError(true);
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("workspace_creation.toast.error.title"),
|
||||
message: t("workspace_creation.toast.error.message"),
|
||||
});
|
||||
if (onSubmit) await onSubmit(workspaceResponse);
|
||||
} catch {
|
||||
captureError({
|
||||
eventName: WORKSPACE_TRACKER_EVENTS.create,
|
||||
payload: { slug: formData.slug },
|
||||
error: new Error("Error creating workspace"),
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("workspace_creation.toast.error.title"),
|
||||
message: t("workspace_creation.toast.error.message"),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setSlugError(true);
|
||||
}
|
||||
} catch {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("workspace_creation.toast.error.title"),
|
||||
message: t("workspace_creation.toast.error.message"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(
|
||||
@@ -119,7 +118,12 @@ export const CreateWorkspaceForm = observer(function CreateWorkspaceForm(props:
|
||||
);
|
||||
|
||||
return (
|
||||
<form className="space-y-6 sm:space-y-9" onSubmit={handleSubmit(handleCreateWorkspace)}>
|
||||
<form
|
||||
className="space-y-6 sm:space-y-9"
|
||||
onSubmit={(e) => {
|
||||
void handleSubmit(handleCreateWorkspace)(e);
|
||||
}}
|
||||
>
|
||||
<div className="space-y-6 sm:space-y-7">
|
||||
<div className="space-y-1 text-sm">
|
||||
<label htmlFor="workspaceName">
|
||||
|
||||
@@ -19,6 +19,7 @@ import { copyUrlToClipboard, getFileURL } from "@plane/utils";
|
||||
// components
|
||||
import { WorkspaceImageUploadModal } from "@/components/core/modals/workspace-image-upload-modal";
|
||||
// helpers
|
||||
import { TimezoneSelect } from "@/components/global/timezone-select";
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
@@ -30,6 +31,7 @@ const defaultValues: Partial<IWorkspace> = {
|
||||
url: "",
|
||||
organization_size: "2-10",
|
||||
logo_url: null,
|
||||
timezone: "UTC",
|
||||
};
|
||||
|
||||
export const WorkspaceDetails = observer(function WorkspaceDetails() {
|
||||
@@ -62,64 +64,69 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
|
||||
const payload: Partial<IWorkspace> = {
|
||||
name: formData.name,
|
||||
organization_size: formData.organization_size,
|
||||
timezone: formData.timezone,
|
||||
};
|
||||
|
||||
await updateWorkspace(currentWorkspace.slug, payload)
|
||||
.then(() => {
|
||||
captureSuccess({
|
||||
eventName: WORKSPACE_TRACKER_EVENTS.update,
|
||||
payload: { slug: currentWorkspace.slug },
|
||||
});
|
||||
setToast({
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Workspace updated successfully",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
captureError({
|
||||
eventName: WORKSPACE_TRACKER_EVENTS.update,
|
||||
payload: { slug: currentWorkspace.slug },
|
||||
error: err,
|
||||
});
|
||||
console.error(err);
|
||||
try {
|
||||
await updateWorkspace(currentWorkspace.slug, payload);
|
||||
captureSuccess({
|
||||
eventName: WORKSPACE_TRACKER_EVENTS.update,
|
||||
payload: { slug: currentWorkspace.slug },
|
||||
});
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 300);
|
||||
setToast({
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Workspace updated successfully",
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
captureError({
|
||||
eventName: WORKSPACE_TRACKER_EVENTS.update,
|
||||
payload: { slug: currentWorkspace.slug },
|
||||
error: err instanceof Error ? err : new Error(String(err)),
|
||||
});
|
||||
console.error(err);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveLogo = async () => {
|
||||
if (!currentWorkspace) return;
|
||||
|
||||
await updateWorkspace(currentWorkspace.slug, {
|
||||
logo_url: "",
|
||||
})
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Workspace picture removed successfully.",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "There was some error in deleting your profile picture. Please try again.",
|
||||
});
|
||||
try {
|
||||
await updateWorkspace(currentWorkspace.slug, {
|
||||
logo_url: "",
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Workspace picture removed successfully.",
|
||||
});
|
||||
} catch {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "There was some error in deleting your profile picture. Please try again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyUrl = () => {
|
||||
if (!currentWorkspace) return;
|
||||
|
||||
copyUrlToClipboard(`${currentWorkspace.slug}`).then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Workspace URL copied to the clipboard.",
|
||||
void copyUrlToClipboard(`${currentWorkspace.slug}`)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Workspace URL copied to the clipboard.",
|
||||
});
|
||||
return undefined;
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently handle clipboard errors
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -264,12 +271,30 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.url)}
|
||||
className="w-full"
|
||||
className="w-full cursor-not-allowed rounded-md !bg-custom-background-90"
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">{t("workspace_settings.settings.general.workspace_timezone")}</h4>
|
||||
<Controller
|
||||
name="timezone"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<TimezoneSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
buttonClassName="border-none"
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
@@ -277,7 +302,9 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
|
||||
<Button
|
||||
data-ph-element={WORKSPACE_TRACKER_ELEMENTS.UPDATE_WORKSPACE_BUTTON}
|
||||
variant="primary"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
onClick={(e) => {
|
||||
void handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
loading={isLoading}
|
||||
>
|
||||
{isLoading ? t("updating") : t("workspace_settings.settings.general.update_workspace")}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "plane",
|
||||
"description": "Open-source project management that unlocks customer value",
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/codemods",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
|
||||
@@ -56,16 +56,24 @@ export function CustomImageNodeView(props: CustomImageNodeViewProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
setResolvedSrc(undefined);
|
||||
setResolvedDownloadSrc(undefined);
|
||||
setFailedToLoadImage(false);
|
||||
|
||||
const getImageSource = async () => {
|
||||
const url = await extension.options.getImageSource?.(imgNodeSrc);
|
||||
setResolvedSrc(url);
|
||||
const downloadUrl = await extension.options.getImageDownloadSource?.(imgNodeSrc);
|
||||
setResolvedDownloadSrc(downloadUrl);
|
||||
try {
|
||||
const url = await extension.options.getImageSource?.(imgNodeSrc);
|
||||
setResolvedSrc(url);
|
||||
const downloadUrl = await extension.options.getImageDownloadSource?.(imgNodeSrc);
|
||||
setResolvedDownloadSrc(downloadUrl);
|
||||
} catch (error) {
|
||||
console.error("Error fetching image source:", error);
|
||||
setFailedToLoadImage(true);
|
||||
}
|
||||
};
|
||||
getImageSource();
|
||||
}, [imgNodeSrc, extension.options]);
|
||||
|
||||
// Handle image duplication when status is duplicating
|
||||
useEffect(() => {
|
||||
const handleDuplication = async () => {
|
||||
if (status !== ECustomImageStatus.DUPLICATING || !extension.options.duplicateImage || !imgNodeSrc) {
|
||||
@@ -87,11 +95,8 @@ export function CustomImageNodeView(props: CustomImageNodeViewProps) {
|
||||
throw new Error("Duplication returned invalid asset ID");
|
||||
}
|
||||
|
||||
// Update node with new source and success status
|
||||
updateAttributes({
|
||||
src: newAssetId,
|
||||
status: ECustomImageStatus.UPLOADED,
|
||||
});
|
||||
setFailedToLoadImage(false);
|
||||
updateAttributes({ src: newAssetId, status: ECustomImageStatus.UPLOADED });
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to duplicate image:", error);
|
||||
// Update status to failed
|
||||
@@ -115,11 +120,13 @@ export function CustomImageNodeView(props: CustomImageNodeViewProps) {
|
||||
useEffect(() => {
|
||||
if (status === ECustomImageStatus.UPLOADED) {
|
||||
hasRetriedOnMount.current = false;
|
||||
setFailedToLoadImage(false);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
const hasDuplicationFailed = hasImageDuplicationFailed(status);
|
||||
const shouldShowBlock = (isUploaded || imageFromFileSystem) && !failedToLoadImage;
|
||||
const hasValidImageSource = imageFromFileSystem || (isUploaded && resolvedSrc);
|
||||
const shouldShowBlock = hasValidImageSource && !failedToLoadImage && !hasDuplicationFailed;
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
|
||||
@@ -8,8 +8,6 @@ import { DropHandlerPlugin } from "@/plugins/drop";
|
||||
import { FilePlugins } from "@/plugins/file/root";
|
||||
import { NodeHighlightPlugin } from "@/plugins/highlight";
|
||||
import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
|
||||
// types
|
||||
import { PasteAssetPlugin } from "@/plugins/paste-asset";
|
||||
import type { IEditorProps, TEditorAsset, TFileHandler } from "@/types";
|
||||
import { codemark } from "./code-mark";
|
||||
|
||||
@@ -83,7 +81,6 @@ export const UtilityExtension = (props: Props) => {
|
||||
flaggedExtensions,
|
||||
editor: this.editor,
|
||||
}),
|
||||
PasteAssetPlugin(),
|
||||
NodeHighlightPlugin(),
|
||||
];
|
||||
},
|
||||
|
||||
@@ -96,6 +96,27 @@ export const getEditorRefHelpers = (args: TArgs): EditorRefApi => {
|
||||
});
|
||||
return markdown;
|
||||
},
|
||||
copyMarkdownToClipboard: () => {
|
||||
if (!editor) return;
|
||||
|
||||
const html = editor.getHTML();
|
||||
const metaData = getEditorMetaData(html);
|
||||
const markdown = convertHTMLToMarkdown({
|
||||
description_html: html,
|
||||
metaData,
|
||||
});
|
||||
|
||||
const copyHandler = (event: ClipboardEvent) => {
|
||||
event.preventDefault();
|
||||
event.clipboardData?.setData("text/plain", markdown);
|
||||
event.clipboardData?.setData("text/html", html);
|
||||
event.clipboardData?.setData("text/plane-editor-html", html);
|
||||
document.removeEventListener("copy", copyHandler);
|
||||
};
|
||||
|
||||
document.addEventListener("copy", copyHandler);
|
||||
document.execCommand("copy");
|
||||
},
|
||||
isAnyDropbarOpen: () => {
|
||||
if (!editor) return false;
|
||||
const utilityStorage = editor.storage.utility;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { assetDuplicationHandlers } from "@/plane-editor/helpers/asset-duplication";
|
||||
|
||||
// Utility function to process HTML content with all registered handlers
|
||||
export const processAssetDuplication = (htmlContent: string): { processedHtml: string } => {
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = htmlContent;
|
||||
|
||||
let processedHtml = htmlContent;
|
||||
|
||||
// Process each registered component type
|
||||
for (const [componentName, handler] of Object.entries(assetDuplicationHandlers)) {
|
||||
const elements = tempDiv.querySelectorAll(componentName);
|
||||
|
||||
if (elements.length > 0) {
|
||||
elements.forEach((element) => {
|
||||
const result = handler({ element, originalHtml: processedHtml });
|
||||
if (result.shouldProcess) {
|
||||
processedHtml = result.modifiedHtml;
|
||||
}
|
||||
});
|
||||
|
||||
// Update tempDiv with processed HTML for next iteration
|
||||
tempDiv.innerHTML = processedHtml;
|
||||
}
|
||||
}
|
||||
|
||||
return { processedHtml };
|
||||
};
|
||||
@@ -32,6 +32,7 @@ export const MarkdownClipboardPlugin = (args: TArgs): Plugin => {
|
||||
});
|
||||
event.clipboardData?.setData("text/plain", markdown);
|
||||
event.clipboardData?.setData("text/html", clipboardHTML);
|
||||
event.clipboardData?.setData("text/plane-editor-html", clipboardHTML);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to copy markdown content to clipboard:", error);
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { assetDuplicationHandlers } from "@/plane-editor/helpers/asset-duplication";
|
||||
|
||||
export const PasteAssetPlugin = (): Plugin =>
|
||||
new Plugin({
|
||||
key: new PluginKey("paste-asset-duplication"),
|
||||
props: {
|
||||
handlePaste: (view, event) => {
|
||||
if (!event.clipboardData) return false;
|
||||
|
||||
const htmlContent = event.clipboardData.getData("text/html");
|
||||
if (!htmlContent || htmlContent.includes('data-uploaded="true"')) return false;
|
||||
|
||||
// Process the HTML content using the registry
|
||||
const { processedHtml, hasChanges } = processAssetDuplication(htmlContent);
|
||||
|
||||
if (!hasChanges) return false;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Mark the content as already processed to avoid infinite loops
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = processedHtml;
|
||||
const metaTag = tempDiv.querySelector("meta[charset='utf-8']");
|
||||
if (metaTag) {
|
||||
metaTag.setAttribute("data-uploaded", "true");
|
||||
}
|
||||
const finalHtml = tempDiv.innerHTML;
|
||||
|
||||
const newDataTransfer = new DataTransfer();
|
||||
newDataTransfer.setData("text/html", finalHtml);
|
||||
if (event.clipboardData) {
|
||||
newDataTransfer.setData("text/plain", event.clipboardData.getData("text/plain"));
|
||||
}
|
||||
|
||||
const pasteEvent = new ClipboardEvent("paste", {
|
||||
clipboardData: newDataTransfer,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
view.dom.dispatchEvent(pasteEvent);
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Utility function to process HTML content with all registered handlers
|
||||
const processAssetDuplication = (htmlContent: string): { processedHtml: string; hasChanges: boolean } => {
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = htmlContent;
|
||||
|
||||
let processedHtml = htmlContent;
|
||||
let hasChanges = false;
|
||||
|
||||
// Process each registered component type
|
||||
for (const [componentName, handler] of Object.entries(assetDuplicationHandlers)) {
|
||||
const elements = tempDiv.querySelectorAll(componentName);
|
||||
|
||||
if (elements.length > 0) {
|
||||
elements.forEach((element) => {
|
||||
const result = handler({ element, originalHtml: processedHtml });
|
||||
if (result.shouldProcess) {
|
||||
processedHtml = result.modifiedHtml;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Update tempDiv with processed HTML for next iteration
|
||||
tempDiv.innerHTML = processedHtml;
|
||||
}
|
||||
}
|
||||
|
||||
return { processedHtml, hasChanges };
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { EditorProps } from "@tiptap/pm/view";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// helpers
|
||||
import { processAssetDuplication } from "@/helpers/paste-asset";
|
||||
|
||||
type TArgs = {
|
||||
editorClassName: string;
|
||||
@@ -46,6 +48,16 @@ export const CoreEditorProps = (props: TArgs): EditorProps => {
|
||||
}
|
||||
},
|
||||
},
|
||||
handlePaste: (view, event) => {
|
||||
if (!event.clipboardData) return false;
|
||||
|
||||
const htmlContent = event.clipboardData.getData("text/plane-editor-html");
|
||||
if (!htmlContent) return false;
|
||||
|
||||
const { processedHtml } = processAssetDuplication(htmlContent);
|
||||
view.pasteHTML(processedHtml);
|
||||
return true;
|
||||
},
|
||||
transformPastedHTML(html) {
|
||||
return stripCommentMarksFromHTML(html);
|
||||
},
|
||||
|
||||
@@ -134,6 +134,7 @@ export type CoreEditorRefApi = {
|
||||
getDocumentInfo: () => TDocumentInfo;
|
||||
getHeadings: () => IMarking[];
|
||||
getMarkDown: () => string;
|
||||
copyMarkdownToClipboard: () => void;
|
||||
getSelectedText: () => string | null;
|
||||
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
|
||||
isAnyDropbarOpen: () => boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/hooks",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "React hooks that are shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/i18n",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "I18n shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
||||
@@ -1580,6 +1580,7 @@ export default {
|
||||
name: "Název pracovního prostoru",
|
||||
company_size: "Velikost společnosti",
|
||||
url: "URL pracovního prostoru",
|
||||
workspace_timezone: "Časové pásmo pracovního prostoru",
|
||||
update_workspace: "Aktualizovat prostor",
|
||||
delete_workspace: "Smazat tento prostor",
|
||||
delete_workspace_description: "Smazáním prostoru odstraníte všechna data a zdroje. Akce je nevratná.",
|
||||
|
||||
@@ -1600,6 +1600,7 @@ export default {
|
||||
name: "Name des Arbeitsbereichs",
|
||||
company_size: "Unternehmensgröße",
|
||||
url: "URL des Arbeitsbereichs",
|
||||
workspace_timezone: "Zeitzone des Arbeitsbereichs",
|
||||
update_workspace: "Arbeitsbereich aktualisieren",
|
||||
delete_workspace: "Diesen Arbeitsbereich löschen",
|
||||
delete_workspace_description:
|
||||
|
||||
@@ -1442,6 +1442,7 @@ export default {
|
||||
name: "Workspace name",
|
||||
company_size: "Company size",
|
||||
url: "Workspace URL",
|
||||
workspace_timezone: "Workspace Timezone",
|
||||
update_workspace: "Update workspace",
|
||||
delete_workspace: "Delete this workspace",
|
||||
delete_workspace_description:
|
||||
|
||||
@@ -1603,6 +1603,7 @@ export default {
|
||||
name: "Nombre del espacio de trabajo",
|
||||
company_size: "Tamaño de la empresa",
|
||||
url: "URL del espacio de trabajo",
|
||||
workspace_timezone: "Zona horaria del espacio de trabajo",
|
||||
update_workspace: "Actualizar espacio de trabajo",
|
||||
delete_workspace: "Eliminar este espacio de trabajo",
|
||||
delete_workspace_description:
|
||||
|
||||
@@ -1601,6 +1601,7 @@ export default {
|
||||
name: "Nom de l’espace de travail",
|
||||
company_size: "Taille de l’entreprise",
|
||||
url: "URL de l’espace de travail",
|
||||
workspace_timezone: "Fuseau horaire de l’espace de travail",
|
||||
update_workspace: "Mettre à jour l’espace de travail",
|
||||
delete_workspace: "Supprimer cet espace de travail",
|
||||
delete_workspace_description:
|
||||
|
||||
@@ -1588,6 +1588,7 @@ export default {
|
||||
name: "Nama ruang kerja",
|
||||
company_size: "Ukuran perusahaan",
|
||||
url: "URL ruang kerja",
|
||||
workspace_timezone: "Zona waktu ruang kerja",
|
||||
update_workspace: "Perbarui ruang kerja",
|
||||
delete_workspace: "Hapus ruang kerja ini",
|
||||
delete_workspace_description:
|
||||
|
||||
@@ -1593,6 +1593,7 @@ export default {
|
||||
name: "Nome dello spazio di lavoro",
|
||||
company_size: "Dimensione aziendale",
|
||||
url: "URL dello spazio di lavoro",
|
||||
workspace_timezone: "Fuso orario dello spazio di lavoro",
|
||||
update_workspace: "Aggiorna spazio di lavoro",
|
||||
delete_workspace: "Elimina questo spazio di lavoro",
|
||||
delete_workspace_description:
|
||||
|
||||
@@ -1579,6 +1579,7 @@ export default {
|
||||
name: "ワークスペース名",
|
||||
company_size: "会社の規模",
|
||||
url: "ワークスペースURL",
|
||||
workspace_timezone: "ワークスペースのタイムゾーン",
|
||||
update_workspace: "ワークスペースを更新",
|
||||
delete_workspace: "このワークスペースを削除",
|
||||
delete_workspace_description:
|
||||
|
||||
@@ -1572,6 +1572,7 @@ export default {
|
||||
name: "작업 공간 이름",
|
||||
company_size: "회사 규모",
|
||||
url: "작업 공간 URL",
|
||||
workspace_timezone: "작업 공간 시간대",
|
||||
update_workspace: "작업 공간 업데이트",
|
||||
delete_workspace: "이 작업 공간 삭제",
|
||||
delete_workspace_description:
|
||||
|
||||
@@ -1583,6 +1583,7 @@ export default {
|
||||
name: "Nazwa przestrzeni roboczej",
|
||||
company_size: "Rozmiar firmy",
|
||||
url: "URL przestrzeni roboczej",
|
||||
workspace_timezone: "Strefa czasowa przestrzeni roboczej",
|
||||
update_workspace: "Zaktualizuj przestrzeń",
|
||||
delete_workspace: "Usuń tę przestrzeń",
|
||||
delete_workspace_description:
|
||||
|
||||
@@ -1601,6 +1601,7 @@ export default {
|
||||
name: "Nome do espaço de trabalho",
|
||||
company_size: "Tamanho da empresa",
|
||||
url: "URL do espaço de trabalho",
|
||||
workspace_timezone: "Fuso horário do espaço de trabalho",
|
||||
update_workspace: "Atualizar espaço de trabalho",
|
||||
delete_workspace: "Excluir este espaço de trabalho",
|
||||
delete_workspace_description:
|
||||
|
||||
@@ -1593,6 +1593,7 @@ export default {
|
||||
name: "Numele spațiului de lucru",
|
||||
company_size: "Dimensiunea companiei",
|
||||
url: "URL-ul spațiului de lucru",
|
||||
workspace_timezone: "Fusul orar al spațiului de lucru",
|
||||
update_workspace: "Actualizează spațiul de lucru",
|
||||
delete_workspace: "Șterge acest spațiu de lucru",
|
||||
delete_workspace_description:
|
||||
|
||||
@@ -1586,6 +1586,7 @@ export default {
|
||||
name: "Название пространства",
|
||||
company_size: "Размер компании",
|
||||
url: "URL пространства",
|
||||
workspace_timezone: "Часовой пояс рабочего пространства",
|
||||
update_workspace: "Обновить пространство",
|
||||
delete_workspace: "Удалить пространство",
|
||||
delete_workspace_description: "Все данные будут безвозвратно удалены.",
|
||||
|
||||
@@ -1583,6 +1583,7 @@ export default {
|
||||
name: "Názov pracovného priestoru",
|
||||
company_size: "Veľkosť spoločnosti",
|
||||
url: "URL pracovného priestoru",
|
||||
workspace_timezone: "Časové pásmo pracovného priestoru",
|
||||
update_workspace: "Aktualizovať priestor",
|
||||
delete_workspace: "Zmazať tento priestor",
|
||||
delete_workspace_description: "Zmazaním priestoru odstránite všetky dáta a zdroje. Akcia je nevratná.",
|
||||
|
||||
@@ -1588,6 +1588,7 @@ export default {
|
||||
name: "Çalışma Alanı Adı",
|
||||
company_size: "Şirket Büyüklüğü",
|
||||
url: "Çalışma Alanı URL'si",
|
||||
workspace_timezone: "Çalışma Alanı Saat Dilimi",
|
||||
update_workspace: "Çalışma Alanını Güncelle",
|
||||
delete_workspace: "Bu çalışma alanını sil",
|
||||
delete_workspace_description:
|
||||
|
||||
@@ -1587,6 +1587,7 @@ export default {
|
||||
name: "Назва робочого простору",
|
||||
company_size: "Розмір компанії",
|
||||
url: "URL робочого простору",
|
||||
workspace_timezone: "Часовий пояс робочого простору",
|
||||
update_workspace: "Оновити простір",
|
||||
delete_workspace: "Видалити цей простір",
|
||||
delete_workspace_description: "Видалення простору призведе до втрати всіх даних і ресурсів. Дія незворотна.",
|
||||
|
||||
@@ -1589,6 +1589,7 @@ export default {
|
||||
name: "Tên không gian làm việc",
|
||||
company_size: "Quy mô công ty",
|
||||
url: "URL không gian làm việc",
|
||||
workspace_timezone: "Múi giờ không gian làm việc",
|
||||
update_workspace: "Cập nhật không gian làm việc",
|
||||
delete_workspace: "Xóa không gian làm việc này",
|
||||
delete_workspace_description:
|
||||
|
||||
@@ -1562,6 +1562,7 @@ export default {
|
||||
name: "工作区名称",
|
||||
company_size: "公司规模",
|
||||
url: "工作区网址",
|
||||
workspace_timezone: "工作区时区",
|
||||
update_workspace: "更新工作区",
|
||||
delete_workspace: "删除此工作区",
|
||||
delete_workspace_description: "删除工作区时,该工作区内的所有数据和资源将被永久删除,且无法恢复。",
|
||||
|
||||
@@ -1563,6 +1563,7 @@ export default {
|
||||
name: "工作區名稱",
|
||||
company_size: "公司規模",
|
||||
url: "工作區網址",
|
||||
workspace_timezone: "工作區時區",
|
||||
update_workspace: "更新工作區",
|
||||
delete_workspace: "刪除此工作區",
|
||||
delete_workspace_description: "刪除工作區時,該工作區內的所有資料和資源都將被永久移除且無法復原。",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/logger",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Logger shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/propel",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/services",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/shared-state",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Shared state shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/tailwind-config",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "tailwind.config.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface IWorkspace {
|
||||
current_plan?: EProductSubscriptionEnum;
|
||||
is_on_trial?: boolean;
|
||||
role: number;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export interface IWorkspaceLite {
|
||||
@@ -234,7 +235,7 @@ export interface IWorkspaceProgressResponse {
|
||||
unstarted_issues: number;
|
||||
}
|
||||
export interface IWorkspaceAnalyticsResponse {
|
||||
completion_chart: any;
|
||||
completion_chart: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type TWorkspacePaginationInfo = TPaginationInfo & {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/typescript-config",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/utils",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Helper functions shared across multiple apps internally",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
|
||||
Reference in New Issue
Block a user