mirror of
https://github.com/makeplane/plane.git
synced 2026-02-24 04:00:14 +01:00
Merge branch 'preview' into fix-outline_scroll
This commit is contained in:
@@ -11,7 +11,7 @@ import { Outlet } from "react-router";
|
||||
// components
|
||||
import { AdminHeader } from "@/components/common/header";
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { NewUserPopup } from "@/components/new-user-popup";
|
||||
import { NewUserPopup } from "@/components/common/new-user-popup";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
// local components
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Input, Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { Banner } from "@/components/common/banner";
|
||||
// local components
|
||||
import { FormHeader } from "../../../core/components/instance/form-header";
|
||||
import { FormHeader } from "@/components/instance/form-header";
|
||||
import { AuthBanner } from "./auth-banner";
|
||||
import { AuthHeader } from "./auth-header";
|
||||
import { authErrorHandler } from "./auth-helpers";
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { enableStaticRendering } from "mobx-react";
|
||||
// stores
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
export class RootStore extends CoreRootStore {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
hydrate(initialData: any) {
|
||||
super.hydrate(initialData);
|
||||
}
|
||||
|
||||
resetOnSignOut() {
|
||||
super.resetOnSignOut();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { Menu, Settings } from "lucide-react";
|
||||
// icons
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
import { BreadcrumbLink } from "../breadcrumb-link";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
// local imports
|
||||
@@ -16,8 +16,8 @@ import { Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
|
||||
import { getPasswordStrength } from "@plane/utils";
|
||||
// components
|
||||
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
|
||||
import { Banner } from "@/components/common/banner";
|
||||
import { FormHeader } from "@/components/instance/form-header";
|
||||
import { Banner } from "../common/banner";
|
||||
import { FormHeader } from "./form-header";
|
||||
|
||||
// service initialization
|
||||
const authService = new AuthService();
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "ce/store/root.store";
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { createContext } from "react";
|
||||
// plane admin store
|
||||
import { RootStore } from "@/plane-admin/store/root.store";
|
||||
import { RootStore } from "../store/root.store";
|
||||
|
||||
let rootStore = new RootStore();
|
||||
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
IInstanceConfig,
|
||||
} from "@plane/types";
|
||||
// root store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
import type { RootStore } from "@/store/root.store";
|
||||
|
||||
export interface IInstanceStore {
|
||||
// issues
|
||||
@@ -53,7 +53,7 @@ export class InstanceStore implements IInstanceStore {
|
||||
// service
|
||||
instanceService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
isLoading: observable.ref,
|
||||
@@ -17,7 +17,7 @@ import { WorkspaceStore } from "./workspace.store";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
export abstract class CoreRootStore {
|
||||
export class RootStore {
|
||||
theme: IThemeStore;
|
||||
instance: IInstanceStore;
|
||||
user: IUserStore;
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { action, observable, makeObservable } from "mobx";
|
||||
// root store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
import type { RootStore } from "./root.store";
|
||||
|
||||
type TTheme = "dark" | "light";
|
||||
export interface IThemeStore {
|
||||
@@ -27,7 +27,7 @@ export class ThemeStore implements IThemeStore {
|
||||
isSidebarCollapsed: boolean | undefined = undefined;
|
||||
theme: string | undefined = undefined;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isNewUserPopup: observable.ref,
|
||||
@@ -11,7 +11,7 @@ import { EUserStatus } from "@plane/constants";
|
||||
import { AuthService, UserService } from "@plane/services";
|
||||
import type { IUser } from "@plane/types";
|
||||
// root store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
import type { RootStore } from "@/store/root.store";
|
||||
|
||||
export interface IUserStore {
|
||||
// observables
|
||||
@@ -36,7 +36,7 @@ export class UserStore implements IUserStore {
|
||||
userService;
|
||||
authService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isLoading: observable.ref,
|
||||
@@ -10,7 +10,7 @@ import { action, observable, runInAction, makeObservable, computed } from "mobx"
|
||||
import { InstanceWorkspaceService } from "@plane/services";
|
||||
import type { IWorkspace, TLoader, TPaginationInfo } from "@plane/types";
|
||||
// root store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
import type { RootStore } from "@/store/root.store";
|
||||
|
||||
export interface IWorkspaceStore {
|
||||
// observables
|
||||
@@ -37,7 +37,7 @@ export class WorkspaceStore implements IWorkspaceStore {
|
||||
// services
|
||||
instanceWorkspaceService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
loader: observable,
|
||||
@@ -9,12 +9,7 @@
|
||||
"types": ["vite/client"],
|
||||
"paths": {
|
||||
"package.json": ["./package.json"],
|
||||
"ce/*": ["./ce/*"],
|
||||
"@/app/*": ["./app/*"],
|
||||
"@/*": ["./core/*"],
|
||||
"@/plane-admin/*": ["./ce/*"],
|
||||
"@/ce/*": ["./ce/*"],
|
||||
"@/styles/*": ["./styles/*"]
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
|
||||
|
||||
@@ -19,6 +19,9 @@ class APITokenSerializer(BaseSerializer):
|
||||
"updated_at",
|
||||
"workspace",
|
||||
"user",
|
||||
"is_active",
|
||||
"last_used",
|
||||
"user_type",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -50,11 +50,10 @@ class ProjectViewSet(BaseViewSet):
|
||||
use_read_replica = True
|
||||
|
||||
def get_queryset(self):
|
||||
sort_order = ProjectMember.objects.filter(
|
||||
member=self.request.user,
|
||||
sort_order = ProjectUserProperty.objects.filter(
|
||||
user=self.request.user,
|
||||
project_id=OuterRef("pk"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
is_active=True,
|
||||
).values("sort_order")
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
@@ -140,11 +139,10 @@ class ProjectViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def list(self, request, slug):
|
||||
sort_order = ProjectMember.objects.filter(
|
||||
member=self.request.user,
|
||||
sort_order = ProjectUserProperty.objects.filter(
|
||||
user=self.request.user,
|
||||
project_id=OuterRef("pk"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
is_active=True,
|
||||
).values("sort_order")
|
||||
|
||||
projects = (
|
||||
|
||||
@@ -49,6 +49,7 @@ from plane.bgtasks.workspace_seed_task import workspace_seed
|
||||
from plane.bgtasks.event_tracking_task import track_event
|
||||
from plane.utils.url import contains_url
|
||||
from plane.utils.analytics_events import WORKSPACE_CREATED, WORKSPACE_DELETED
|
||||
from plane.utils.csv_utils import sanitize_csv_row
|
||||
|
||||
|
||||
class WorkSpaceViewSet(BaseViewSet):
|
||||
@@ -81,12 +82,14 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
|
||||
def create(self, request):
|
||||
try:
|
||||
(DISABLE_WORKSPACE_CREATION,) = get_configuration_value([
|
||||
{
|
||||
"key": "DISABLE_WORKSPACE_CREATION",
|
||||
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
|
||||
}
|
||||
])
|
||||
(DISABLE_WORKSPACE_CREATION,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "DISABLE_WORKSPACE_CREATION",
|
||||
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
if DISABLE_WORKSPACE_CREATION == "1":
|
||||
return Response(
|
||||
@@ -369,7 +372,7 @@ class ExportWorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
"""Generate CSV buffer from rows."""
|
||||
csv_buffer = io.StringIO()
|
||||
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
|
||||
[writer.writerow(row) for row in rows]
|
||||
[writer.writerow(sanitize_csv_row(row)) for row in rows]
|
||||
csv_buffer.seek(0)
|
||||
return csv_buffer
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from plane.license.utils.instance_value import get_email_configuration
|
||||
from plane.utils.analytics_plot import build_graph_plot
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.csv_utils import sanitize_csv_row
|
||||
|
||||
row_mapping = {
|
||||
"state__name": "State",
|
||||
@@ -180,7 +181,7 @@ def generate_csv_from_rows(rows):
|
||||
"""Generate CSV buffer from rows."""
|
||||
csv_buffer = io.StringIO()
|
||||
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
|
||||
[writer.writerow(row) for row in rows]
|
||||
[writer.writerow(sanitize_csv_row(row)) for row in rows]
|
||||
return csv_buffer
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
# Python imports
|
||||
import logging
|
||||
import socket
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
@@ -26,7 +27,7 @@ DEFAULT_FAVICON = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoP
|
||||
def validate_url_ip(url: str) -> None:
|
||||
"""
|
||||
Validate that a URL doesn't point to a private/internal IP address.
|
||||
Only checks if the hostname is a direct IP address.
|
||||
Resolves hostnames to IPs before checking.
|
||||
|
||||
Args:
|
||||
url: The URL to validate
|
||||
@@ -38,17 +39,31 @@ def validate_url_ip(url: str) -> None:
|
||||
hostname = parsed.hostname
|
||||
|
||||
if not hostname:
|
||||
return
|
||||
raise ValueError("Invalid URL: No hostname found")
|
||||
|
||||
# Only allow HTTP and HTTPS to prevent file://, gopher://, etc.
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
raise ValueError("Invalid URL scheme. Only HTTP and HTTPS are allowed")
|
||||
|
||||
# Resolve hostname to IP addresses — this catches domain names that
|
||||
# point to internal IPs (e.g. attacker.com -> 169.254.169.254)
|
||||
|
||||
try:
|
||||
ip = ipaddress.ip_address(hostname)
|
||||
except ValueError:
|
||||
# Not an IP address (it's a domain name), nothing to check here
|
||||
return
|
||||
addr_info = socket.getaddrinfo(hostname, None)
|
||||
except socket.gaierror:
|
||||
raise ValueError("Hostname could not be resolved")
|
||||
|
||||
# It IS an IP address - check if it's private/internal
|
||||
if ip.is_private or ip.is_loopback or ip.is_reserved:
|
||||
raise ValueError("Access to private/internal networks is not allowed")
|
||||
if not addr_info:
|
||||
raise ValueError("No IP addresses found for the hostname")
|
||||
|
||||
# Check every resolved IP against blocked ranges to prevent SSRF
|
||||
for addr in addr_info:
|
||||
ip = ipaddress.ip_address(addr[4][0])
|
||||
if ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local:
|
||||
raise ValueError("Access to private/internal networks is not allowed")
|
||||
|
||||
|
||||
MAX_REDIRECTS = 5
|
||||
|
||||
|
||||
def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
@@ -74,11 +89,23 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
validate_url_ip(final_url)
|
||||
|
||||
try:
|
||||
response = requests.get(final_url, headers=headers, timeout=1)
|
||||
final_url = response.url # Get the final URL after any redirects
|
||||
# Manually follow redirects to validate each URL before requesting
|
||||
redirect_count = 0
|
||||
response = requests.get(final_url, headers=headers, timeout=1, allow_redirects=False)
|
||||
|
||||
# check for redirected url also
|
||||
validate_url_ip(final_url)
|
||||
while response.is_redirect and redirect_count < MAX_REDIRECTS:
|
||||
redirect_url = response.headers.get("Location")
|
||||
if not redirect_url:
|
||||
break
|
||||
# Resolve relative redirects against current URL
|
||||
final_url = urljoin(final_url, redirect_url)
|
||||
# Validate the redirect target BEFORE making the request
|
||||
validate_url_ip(final_url)
|
||||
redirect_count += 1
|
||||
response = requests.get(final_url, headers=headers, timeout=1, allow_redirects=False)
|
||||
|
||||
if redirect_count >= MAX_REDIRECTS:
|
||||
logger.warning(f"Too many redirects for URL: {url}")
|
||||
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
title_tag = soup.find("title")
|
||||
@@ -134,7 +161,9 @@ def find_favicon_url(soup: Optional[BeautifulSoup], base_url: str) -> Optional[s
|
||||
for selector in favicon_selectors:
|
||||
favicon_tag = soup.select_one(selector)
|
||||
if favicon_tag and favicon_tag.get("href"):
|
||||
return urljoin(base_url, favicon_tag["href"])
|
||||
favicon_href = urljoin(base_url, favicon_tag["href"])
|
||||
validate_url_ip(favicon_href)
|
||||
return favicon_href
|
||||
|
||||
# Fallback to /favicon.ico
|
||||
parsed_url = urlparse(base_url)
|
||||
@@ -142,7 +171,9 @@ def find_favicon_url(soup: Optional[BeautifulSoup], base_url: str) -> Optional[s
|
||||
|
||||
# Check if fallback exists
|
||||
try:
|
||||
response = requests.head(fallback_url, timeout=2)
|
||||
validate_url_ip(fallback_url)
|
||||
response = requests.head(fallback_url, timeout=2, allow_redirects=False)
|
||||
|
||||
if response.status_code == 200:
|
||||
return fallback_url
|
||||
except requests.RequestException as e:
|
||||
@@ -173,6 +204,8 @@ def fetch_and_encode_favicon(
|
||||
"favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}",
|
||||
}
|
||||
|
||||
validate_url_ip(favicon_url)
|
||||
|
||||
response = requests.get(favicon_url, headers=headers, timeout=1)
|
||||
|
||||
# Get content type
|
||||
|
||||
19
apps/api/plane/db/migrations/0119_alter_estimatepoint_key.py
Normal file
19
apps/api/plane/db/migrations/0119_alter_estimatepoint_key.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.27 on 2026-02-09 09:37
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0118_remove_workspaceuserproperties_product_tour_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='estimatepoint',
|
||||
name='key',
|
||||
field=models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
]
|
||||
@@ -38,7 +38,7 @@ class Estimate(ProjectBaseModel):
|
||||
|
||||
class EstimatePoint(ProjectBaseModel):
|
||||
estimate = models.ForeignKey("db.Estimate", on_delete=models.CASCADE, related_name="points")
|
||||
key = models.IntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(12)])
|
||||
key = models.IntegerField(default=0, validators=[MinValueValidator(0)])
|
||||
description = models.TextField(blank=True)
|
||||
value = models.CharField(max_length=255)
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ class TestApiTokenEndpoint:
|
||||
"""Test retrieving a specific API token"""
|
||||
# Arrange
|
||||
session_client.force_authenticate(user=create_user)
|
||||
url = reverse("api-tokens", kwargs={"pk": create_api_token_for_user.pk})
|
||||
url = reverse("api-tokens-details", kwargs={"pk": create_api_token_for_user.pk})
|
||||
|
||||
# Act
|
||||
response = session_client.get(url)
|
||||
@@ -159,7 +159,7 @@ class TestApiTokenEndpoint:
|
||||
# Arrange
|
||||
session_client.force_authenticate(user=create_user)
|
||||
fake_pk = uuid4()
|
||||
url = reverse("api-tokens", kwargs={"pk": fake_pk})
|
||||
url = reverse("api-tokens-details", kwargs={"pk": fake_pk})
|
||||
|
||||
# Act
|
||||
response = session_client.get(url)
|
||||
@@ -178,7 +178,7 @@ class TestApiTokenEndpoint:
|
||||
other_user = User.objects.create(email=unique_email, username=unique_username)
|
||||
other_token = APIToken.objects.create(label="Other Token", user=other_user, user_type=0)
|
||||
session_client.force_authenticate(user=create_user)
|
||||
url = reverse("api-tokens", kwargs={"pk": other_token.pk})
|
||||
url = reverse("api-tokens-details", kwargs={"pk": other_token.pk})
|
||||
|
||||
# Act
|
||||
response = session_client.get(url)
|
||||
@@ -192,7 +192,7 @@ class TestApiTokenEndpoint:
|
||||
"""Test successful API token deletion"""
|
||||
# Arrange
|
||||
session_client.force_authenticate(user=create_user)
|
||||
url = reverse("api-tokens", kwargs={"pk": create_api_token_for_user.pk})
|
||||
url = reverse("api-tokens-details", kwargs={"pk": create_api_token_for_user.pk})
|
||||
|
||||
# Act
|
||||
response = session_client.delete(url)
|
||||
@@ -207,7 +207,7 @@ class TestApiTokenEndpoint:
|
||||
# Arrange
|
||||
session_client.force_authenticate(user=create_user)
|
||||
fake_pk = uuid4()
|
||||
url = reverse("api-tokens", kwargs={"pk": fake_pk})
|
||||
url = reverse("api-tokens-details", kwargs={"pk": fake_pk})
|
||||
|
||||
# Act
|
||||
response = session_client.delete(url)
|
||||
@@ -226,7 +226,7 @@ class TestApiTokenEndpoint:
|
||||
other_user = User.objects.create(email=unique_email, username=unique_username)
|
||||
other_token = APIToken.objects.create(label="Other Token", user=other_user, user_type=0)
|
||||
session_client.force_authenticate(user=create_user)
|
||||
url = reverse("api-tokens", kwargs={"pk": other_token.pk})
|
||||
url = reverse("api-tokens-details", kwargs={"pk": other_token.pk})
|
||||
|
||||
# Act
|
||||
response = session_client.delete(url)
|
||||
@@ -242,7 +242,7 @@ class TestApiTokenEndpoint:
|
||||
# Arrange
|
||||
service_token = APIToken.objects.create(label="Service Token", user=create_user, user_type=0, is_service=True)
|
||||
session_client.force_authenticate(user=create_user)
|
||||
url = reverse("api-tokens", kwargs={"pk": service_token.pk})
|
||||
url = reverse("api-tokens-details", kwargs={"pk": service_token.pk})
|
||||
|
||||
# Act
|
||||
response = session_client.delete(url)
|
||||
@@ -258,7 +258,7 @@ class TestApiTokenEndpoint:
|
||||
"""Test successful API token update"""
|
||||
# Arrange
|
||||
session_client.force_authenticate(user=create_user)
|
||||
url = reverse("api-tokens", kwargs={"pk": create_api_token_for_user.pk})
|
||||
url = reverse("api-tokens-details", kwargs={"pk": create_api_token_for_user.pk})
|
||||
update_data = {
|
||||
"label": "Updated Token Label",
|
||||
"description": "Updated description",
|
||||
@@ -282,7 +282,7 @@ class TestApiTokenEndpoint:
|
||||
"""Test partial API token update"""
|
||||
# Arrange
|
||||
session_client.force_authenticate(user=create_user)
|
||||
url = reverse("api-tokens", kwargs={"pk": create_api_token_for_user.pk})
|
||||
url = reverse("api-tokens-details", kwargs={"pk": create_api_token_for_user.pk})
|
||||
original_description = create_api_token_for_user.description
|
||||
update_data = {"label": "Only Label Updated"}
|
||||
|
||||
@@ -300,7 +300,7 @@ class TestApiTokenEndpoint:
|
||||
# Arrange
|
||||
session_client.force_authenticate(user=create_user)
|
||||
fake_pk = uuid4()
|
||||
url = reverse("api-tokens", kwargs={"pk": fake_pk})
|
||||
url = reverse("api-tokens-details", kwargs={"pk": fake_pk})
|
||||
update_data = {"label": "New Label"}
|
||||
|
||||
# Act
|
||||
@@ -320,7 +320,7 @@ class TestApiTokenEndpoint:
|
||||
other_user = User.objects.create(email=unique_email, username=unique_username)
|
||||
other_token = APIToken.objects.create(label="Other Token", user=other_user, user_type=0)
|
||||
session_client.force_authenticate(user=create_user)
|
||||
url = reverse("api-tokens", kwargs={"pk": other_token.pk})
|
||||
url = reverse("api-tokens-details", kwargs={"pk": other_token.pk})
|
||||
update_data = {"label": "Hacked Label"}
|
||||
|
||||
# Act
|
||||
@@ -333,6 +333,56 @@ class TestApiTokenEndpoint:
|
||||
other_token.refresh_from_db()
|
||||
assert other_token.label == "Other Token"
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_patch_cannot_modify_token(self, session_client, create_user, create_api_token_for_user):
|
||||
"""Test that token value cannot be modified via PATCH"""
|
||||
# Arrange
|
||||
session_client.force_authenticate(user=create_user)
|
||||
url = reverse("api-tokens-details", kwargs={"pk": create_api_token_for_user.pk})
|
||||
original_token = create_api_token_for_user.token
|
||||
update_data = {"token": "plane_api_malicious_token_value"}
|
||||
|
||||
# Act
|
||||
response = session_client.patch(url, update_data, format="json")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
create_api_token_for_user.refresh_from_db()
|
||||
assert create_api_token_for_user.token == original_token
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_patch_cannot_modify_user_type(self, session_client, create_user, create_api_token_for_user):
|
||||
"""Test that user_type cannot be modified via PATCH"""
|
||||
# Arrange
|
||||
session_client.force_authenticate(user=create_user)
|
||||
url = reverse("api-tokens-details", kwargs={"pk": create_api_token_for_user.pk})
|
||||
update_data = {"user_type": 1}
|
||||
|
||||
# Act
|
||||
response = session_client.patch(url, update_data, format="json")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
create_api_token_for_user.refresh_from_db()
|
||||
assert create_api_token_for_user.user_type == 0
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_patch_cannot_modify_service_token(self, session_client, create_user):
|
||||
"""Test that service tokens cannot be modified through user token endpoint"""
|
||||
# Arrange
|
||||
service_token = APIToken.objects.create(label="Service Token", user=create_user, user_type=0, is_service=True)
|
||||
session_client.force_authenticate(user=create_user)
|
||||
url = reverse("api-tokens-details", kwargs={"pk": service_token.pk})
|
||||
update_data = {"label": "Hacked Service Token"}
|
||||
|
||||
# Act
|
||||
response = session_client.patch(url, update_data, format="json")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
service_token.refresh_from_db()
|
||||
assert service_token.label == "Service Token"
|
||||
|
||||
# Authentication tests
|
||||
@pytest.mark.django_db
|
||||
def test_all_endpoints_require_authentication(self, api_client):
|
||||
@@ -341,9 +391,9 @@ class TestApiTokenEndpoint:
|
||||
endpoints = [
|
||||
(reverse("api-tokens"), "get"),
|
||||
(reverse("api-tokens"), "post"),
|
||||
(reverse("api-tokens", kwargs={"pk": uuid4()}), "get"),
|
||||
(reverse("api-tokens", kwargs={"pk": uuid4()}), "patch"),
|
||||
(reverse("api-tokens", kwargs={"pk": uuid4()}), "delete"),
|
||||
(reverse("api-tokens-details", kwargs={"pk": uuid4()}), "get"),
|
||||
(reverse("api-tokens-details", kwargs={"pk": uuid4()}), "patch"),
|
||||
(reverse("api-tokens-details", kwargs={"pk": uuid4()}), "delete"),
|
||||
]
|
||||
|
||||
# Act & Assert
|
||||
|
||||
26
apps/api/plane/utils/csv_utils.py
Normal file
26
apps/api/plane/utils/csv_utils.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# CSV utility functions for safe export
|
||||
# Characters that trigger formula evaluation in spreadsheet applications
|
||||
_CSV_FORMULA_TRIGGERS = frozenset(("=", "+", "-", "@", "\t", "\r", "\n"))
|
||||
|
||||
|
||||
def sanitize_csv_value(value):
|
||||
"""Sanitize a value for CSV export to prevent formula injection.
|
||||
|
||||
Prefixes string values starting with formula-triggering characters
|
||||
with a single quote so spreadsheet applications treat them as text
|
||||
instead of evaluating them as formulas.
|
||||
|
||||
See: https://owasp.org/www-community/attacks/CSV_Injection
|
||||
"""
|
||||
if isinstance(value, str) and value and value[0] in _CSV_FORMULA_TRIGGERS:
|
||||
return "'" + value
|
||||
return value
|
||||
|
||||
|
||||
def sanitize_csv_row(row):
|
||||
"""Sanitize all values in a CSV row."""
|
||||
return [sanitize_csv_value(v) for v in row]
|
||||
@@ -9,6 +9,9 @@ from typing import Any, Dict, List, Type
|
||||
|
||||
from openpyxl import Workbook
|
||||
|
||||
# Module imports
|
||||
from plane.utils.csv_utils import sanitize_csv_row
|
||||
|
||||
|
||||
class BaseFormatter:
|
||||
"""Base class for export formatters."""
|
||||
@@ -84,7 +87,7 @@ class CSVFormatter(BaseFormatter):
|
||||
buf = io.StringIO()
|
||||
writer = csv.writer(buf, delimiter=",", quoting=csv.QUOTE_ALL)
|
||||
for row in data:
|
||||
writer.writerow(row)
|
||||
writer.writerow(sanitize_csv_row(row))
|
||||
buf.seek(0)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ from typing import Any, Dict, List, Union
|
||||
from openpyxl import Workbook, load_workbook
|
||||
|
||||
|
||||
# Module imports
|
||||
from plane.utils.csv_utils import sanitize_csv_row, sanitize_csv_value
|
||||
|
||||
|
||||
class BaseFormatter(ABC):
|
||||
@abstractmethod
|
||||
def encode(self, data: List[Dict]) -> Union[str, bytes]:
|
||||
@@ -128,11 +132,12 @@ class CSVFormatter(BaseFormatter):
|
||||
|
||||
# Write data rows in the same field order
|
||||
for row in data:
|
||||
writer.writerow([row.get(key, "") for key in fieldnames])
|
||||
writer.writerow(sanitize_csv_row([row.get(key, "") for key in fieldnames]))
|
||||
else:
|
||||
writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=self.delimiter)
|
||||
writer.writeheader()
|
||||
writer.writerows(data)
|
||||
for row in data:
|
||||
writer.writerow({k: sanitize_csv_value(row.get(k, "")) for k in fieldnames})
|
||||
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# base requirements
|
||||
|
||||
# django
|
||||
Django==4.2.27
|
||||
Django==4.2.28
|
||||
# rest framework
|
||||
djangorestframework==3.15.2
|
||||
# postgres
|
||||
@@ -51,7 +51,7 @@ beautifulsoup4==4.12.3
|
||||
# analytics
|
||||
posthog==3.5.0
|
||||
# crypto
|
||||
cryptography==44.0.1
|
||||
cryptography==46.0.5
|
||||
# html validator
|
||||
lxml==6.0.0
|
||||
# s3
|
||||
|
||||
@@ -358,7 +358,6 @@ export const nodeRenderers: NodeRendererRegistry = {
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
type InternalRenderContext = {
|
||||
|
||||
@@ -5,17 +5,11 @@
|
||||
*/
|
||||
|
||||
import { logger } from "@plane/logger";
|
||||
import type { TPage } from "@plane/types";
|
||||
import type { TDocumentPayload, TPage } from "@plane/types";
|
||||
// services
|
||||
import { AppError } from "@/lib/errors";
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
export type TPageDescriptionPayload = {
|
||||
description_binary: string;
|
||||
description_html: string;
|
||||
description: object;
|
||||
};
|
||||
|
||||
export type TUserMention = {
|
||||
id: string;
|
||||
display_name: string;
|
||||
@@ -121,7 +115,7 @@ export abstract class PageCoreService extends APIService {
|
||||
}
|
||||
}
|
||||
|
||||
async updateDescriptionBinary(pageId: string, data: TPageDescriptionPayload): Promise<any> {
|
||||
async updateDescriptionBinary(pageId: string, data: TDocumentPayload): Promise<any> {
|
||||
try {
|
||||
const response = await this.patch(`${this.basePath}/pages/${pageId}/description/`, data, {
|
||||
headers: this.getHeader(),
|
||||
|
||||
@@ -728,6 +728,5 @@ describe("PDF Rendering Integration", () => {
|
||||
|
||||
expect(text).toContain("Text after image");
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,9 +24,10 @@ import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// layouts
|
||||
import { ProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper";
|
||||
// plane web imports
|
||||
import { useWorkItemProperties } from "@/plane-web/hooks/use-issue-properties";
|
||||
import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper";
|
||||
import { WorkItemDetailRoot } from "@/plane-web/components/browse/workItem-detail";
|
||||
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
@@ -15,7 +15,8 @@ import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-butt
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import { useProjectNavigationPreferences } from "@/hooks/use-navigation-preferences";
|
||||
import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper";
|
||||
// layouts
|
||||
import { ProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper";
|
||||
// local imports
|
||||
import type { Route } from "./+types/layout";
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web hooks
|
||||
import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
// services
|
||||
import { ProjectPageService, ProjectPageVersionService } from "@/services/page";
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
@@ -10,8 +10,8 @@ import { Outlet } from "react-router";
|
||||
// components
|
||||
import { getProjectActivePath } from "@/components/settings/helper";
|
||||
import { SettingsMobileNav } from "@/components/settings/mobile/nav";
|
||||
// plane web imports
|
||||
import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper";
|
||||
// layouts
|
||||
import { ProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper";
|
||||
// types
|
||||
import type { Route } from "./+types/layout";
|
||||
import { ProjectSettingsSidebarRoot } from "@/components/settings/project/sidebar";
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
// plane web imports
|
||||
import { ProjectTeamspaceList } from "@/plane-web/components/projects/teamspaces/teamspace-list";
|
||||
import { getProjectSettingsPageLabelI18nKey } from "@/plane-web/helpers/project-settings";
|
||||
// local imports
|
||||
import type { Route } from "./+types/page";
|
||||
import { MembersProjectSettingsHeader } from "./header";
|
||||
@@ -49,7 +48,7 @@ function MembersSettingsPage({ params }: Route.ComponentProps) {
|
||||
return (
|
||||
<SettingsContentWrapper header={<MembersProjectSettingsHeader />} hugging>
|
||||
<PageHead title={pageTitle} />
|
||||
<SettingsHeading title={t(getProjectSettingsPageLabelI18nKey("members", "common.members"))} />
|
||||
<SettingsHeading title={t("common.members")} />
|
||||
<ProjectSettingsMemberDefaults projectId={projectId} workspaceSlug={workspaceSlug} />
|
||||
<ProjectTeamspaceList projectId={projectId} workspaceSlug={workspaceSlug} />
|
||||
<ProjectMemberList projectId={projectId} workspaceSlug={workspaceSlug} />
|
||||
|
||||
@@ -9,7 +9,7 @@ import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper";
|
||||
import { WorkspaceContentWrapper } from "@/plane-web/components/workspace/content-wrapper";
|
||||
import { AppRailVisibilityProvider } from "@/plane-web/hooks/app-rail";
|
||||
import { GlobalModals } from "@/plane-web/components/common/modal/global";
|
||||
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
|
||||
import { WorkspaceAuthWrapper } from "@/layouts/auth-layout/workspace-wrapper";
|
||||
import type { Route } from "./+types/layout";
|
||||
|
||||
export default function WorkspaceLayout(props: Route.ComponentProps) {
|
||||
|
||||
@@ -18,17 +18,17 @@ import WorkspaceCreationDisabled from "@/app/assets/workspace/workspace-creation
|
||||
import { CreateWorkspaceForm } from "@/components/workspace/create-workspace-form";
|
||||
// hooks
|
||||
import { useUser, useUserProfile } from "@/hooks/store/user";
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// wrappers
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper";
|
||||
// plane web helpers
|
||||
import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper";
|
||||
|
||||
const CreateWorkspacePage = observer(function CreateWorkspacePage() {
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// store hooks
|
||||
const { config } = useInstance();
|
||||
const { data: currentUser } = useUser();
|
||||
const { updateUserProfile } = useUserProfile();
|
||||
// states
|
||||
@@ -38,7 +38,7 @@ const CreateWorkspacePage = observer(function CreateWorkspacePage() {
|
||||
organization_size: "",
|
||||
});
|
||||
// derived values
|
||||
const isWorkspaceCreationDisabled = getIsWorkspaceCreationDisabled();
|
||||
const isWorkspaceCreationDisabled = config?.is_workspace_creation_disabled ?? false;
|
||||
|
||||
// methods
|
||||
const getMailtoHref = () => {
|
||||
|
||||
@@ -32,7 +32,7 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// services
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useUser } from "@/hooks/store/user";
|
||||
// wrappers
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useUser } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// wrappers
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
// services
|
||||
|
||||
// service initialization
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
import { RichTextEditor } from "@/components/editor/rich-text";
|
||||
// plane web constants
|
||||
import { AI_EDITOR_TASKS, LOADING_TEXTS } from "@/plane-web/constants/ai";
|
||||
import { AI_EDITOR_TASKS, LOADING_TEXTS } from "@/constants/ai";
|
||||
// plane web services
|
||||
import type { TTaskPayload } from "@/services/ai.service";
|
||||
import { AIService } from "@/services/ai.service";
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./settings";
|
||||
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
// plane imports
|
||||
import { CycleIcon, IntakeIcon, ModuleIcon, PageIcon, ViewsIcon } from "@plane/propel/icons";
|
||||
import type { IProject } from "@plane/types";
|
||||
|
||||
export type TProperties = {
|
||||
key: string;
|
||||
property: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
isPro: boolean;
|
||||
isEnabled: boolean;
|
||||
renderChildren?: (currentProjectDetails: IProject, workspaceSlug: string) => ReactNode;
|
||||
href?: string;
|
||||
};
|
||||
|
||||
type TProjectBaseFeatureKeys = "cycles" | "modules" | "views" | "pages" | "inbox";
|
||||
|
||||
type TBaseFeatureList = {
|
||||
[key in TProjectBaseFeatureKeys]: TProperties;
|
||||
};
|
||||
|
||||
export const PROJECT_BASE_FEATURES_LIST: TBaseFeatureList = {
|
||||
cycles: {
|
||||
key: "cycles",
|
||||
property: "cycle_view",
|
||||
title: "Cycles",
|
||||
description: "Timebox work as you see fit per project and change frequency from one period to the next.",
|
||||
icon: <CycleIcon className="h-5 w-5 flex-shrink-0 rotate-180 text-tertiary" />,
|
||||
isPro: false,
|
||||
isEnabled: true,
|
||||
},
|
||||
modules: {
|
||||
key: "modules",
|
||||
property: "module_view",
|
||||
title: "Modules",
|
||||
description: "Group work into sub-project-like set-ups with their own leads and assignees.",
|
||||
icon: <ModuleIcon width={20} height={20} className="flex-shrink-0 text-tertiary" />,
|
||||
isPro: false,
|
||||
isEnabled: true,
|
||||
},
|
||||
views: {
|
||||
key: "views",
|
||||
property: "issue_views_view",
|
||||
title: "Views",
|
||||
description: "Save sorts, filters, and display options for later or share them.",
|
||||
icon: <ViewsIcon className="h-5 w-5 flex-shrink-0 text-tertiary" />,
|
||||
isPro: false,
|
||||
isEnabled: true,
|
||||
},
|
||||
pages: {
|
||||
key: "pages",
|
||||
property: "page_view",
|
||||
title: "Pages",
|
||||
description: "Write anything like you write anything.",
|
||||
icon: <PageIcon className="h-5 w-5 flex-shrink-0 text-tertiary" />,
|
||||
isPro: false,
|
||||
isEnabled: true,
|
||||
},
|
||||
inbox: {
|
||||
key: "intake",
|
||||
property: "inbox_view",
|
||||
title: "Intake",
|
||||
description: "Consider and discuss work items before you add them to your project.",
|
||||
icon: <IntakeIcon className="h-5 w-5 flex-shrink-0 text-tertiary" />,
|
||||
isPro: false,
|
||||
isEnabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
type TProjectFeatures = {
|
||||
project_features: {
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
featureList: TBaseFeatureList;
|
||||
};
|
||||
};
|
||||
|
||||
export const PROJECT_FEATURES_LIST: TProjectFeatures = {
|
||||
project_features: {
|
||||
key: "projects_and_issues",
|
||||
title: "Projects and work items",
|
||||
description: "Toggle these on or off this project.",
|
||||
featureList: PROJECT_BASE_FEATURES_LIST,
|
||||
},
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./features";
|
||||
@@ -1,112 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { TCommandPaletteActionList, TCommandPaletteShortcut, TCommandPaletteShortcutList } from "@plane/types";
|
||||
// store
|
||||
import { store } from "@/lib/store-context";
|
||||
|
||||
export const getGlobalShortcutsList: () => TCommandPaletteActionList = () => {
|
||||
const { toggleCreateIssueModal } = store.commandPalette;
|
||||
|
||||
return {
|
||||
c: {
|
||||
title: "Create a new work item",
|
||||
description: "Create a new work item in the current project",
|
||||
action: () => {
|
||||
toggleCreateIssueModal(true);
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getWorkspaceShortcutsList: () => TCommandPaletteActionList = () => {
|
||||
const { toggleCreateProjectModal } = store.commandPalette;
|
||||
|
||||
return {
|
||||
p: {
|
||||
title: "Create a new project",
|
||||
description: "Create a new project in the current workspace",
|
||||
action: () => {
|
||||
toggleCreateProjectModal(true);
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getProjectShortcutsList: () => TCommandPaletteActionList = () => {
|
||||
const {
|
||||
toggleCreatePageModal,
|
||||
toggleCreateModuleModal,
|
||||
toggleCreateCycleModal,
|
||||
toggleCreateViewModal,
|
||||
toggleBulkDeleteIssueModal,
|
||||
} = store.commandPalette;
|
||||
|
||||
return {
|
||||
d: {
|
||||
title: "Create a new page",
|
||||
description: "Create a new page in the current project",
|
||||
action: () => {
|
||||
toggleCreatePageModal({ isOpen: true });
|
||||
},
|
||||
},
|
||||
m: {
|
||||
title: "Create a new module",
|
||||
description: "Create a new module in the current project",
|
||||
action: () => {
|
||||
toggleCreateModuleModal(true);
|
||||
},
|
||||
},
|
||||
q: {
|
||||
title: "Create a new cycle",
|
||||
description: "Create a new cycle in the current project",
|
||||
action: () => {
|
||||
toggleCreateCycleModal(true);
|
||||
},
|
||||
},
|
||||
v: {
|
||||
title: "Create a new view",
|
||||
description: "Create a new view in the current project",
|
||||
action: () => {
|
||||
toggleCreateViewModal(true);
|
||||
},
|
||||
},
|
||||
backspace: {
|
||||
title: "Bulk delete work items",
|
||||
description: "Bulk delete work items in the current project",
|
||||
action: () => toggleBulkDeleteIssueModal(true),
|
||||
},
|
||||
delete: {
|
||||
title: "Bulk delete work items",
|
||||
description: "Bulk delete work items in the current project",
|
||||
action: () => toggleBulkDeleteIssueModal(true),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const handleAdditionalKeyDownEvents = (e: KeyboardEvent) => null;
|
||||
|
||||
export const getNavigationShortcutsList = (): TCommandPaletteShortcut[] => [
|
||||
{ keys: "Ctrl,K", description: "Open command menu" },
|
||||
];
|
||||
|
||||
export const getCommonShortcutsList = (platform: string): TCommandPaletteShortcut[] => [
|
||||
{ keys: "P", description: "Create project" },
|
||||
{ keys: "C", description: "Create work item" },
|
||||
{ keys: "Q", description: "Create cycle" },
|
||||
{ keys: "M", description: "Create module" },
|
||||
{ keys: "V", description: "Create view" },
|
||||
{ keys: "D", description: "Create page" },
|
||||
{ keys: "Delete", description: "Bulk delete work items" },
|
||||
{ keys: "Shift,/", description: "Open shortcuts guide" },
|
||||
{
|
||||
keys: platform === "MacOS" ? "Ctrl,control,C" : "Ctrl,Alt,C",
|
||||
description: "Copy work item URL from the work item details page",
|
||||
},
|
||||
];
|
||||
|
||||
export const getAdditionalShortcutsList = (): TCommandPaletteShortcutList[] => [];
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { TEpicAnalyticsGroup } from "@plane/types";
|
||||
|
||||
export const updateEpicAnalytics = () => {
|
||||
const updateAnalytics = (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
epicId: string,
|
||||
data: {
|
||||
incrementStateGroupCount?: TEpicAnalyticsGroup;
|
||||
decrementStateGroupCount?: TEpicAnalyticsGroup;
|
||||
}
|
||||
) => {};
|
||||
|
||||
return { updateAnalytics };
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { store } from "@/lib/store-context";
|
||||
|
||||
export const getIsWorkspaceCreationDisabled = () => {
|
||||
const instanceConfig = store.instance.config;
|
||||
|
||||
return instanceConfig?.is_workspace_creation_disabled;
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { IssueActions } from "@/hooks/use-issues-actions";
|
||||
|
||||
export const useTeamIssueActions: () => IssueActions = () => ({
|
||||
fetchIssues: () => Promise.resolve(undefined),
|
||||
fetchNextIssues: () => Promise.resolve(undefined),
|
||||
removeIssue: () => Promise.resolve(undefined),
|
||||
updateFilters: () => Promise.resolve(undefined),
|
||||
});
|
||||
|
||||
export const useTeamViewIssueActions: () => IssueActions = () => ({
|
||||
fetchIssues: () => Promise.resolve(undefined),
|
||||
fetchNextIssues: () => Promise.resolve(undefined),
|
||||
removeIssue: () => Promise.resolve(undefined),
|
||||
updateFilters: () => Promise.resolve(undefined),
|
||||
});
|
||||
|
||||
export const useTeamProjectWorkItemsActions: () => IssueActions = () => ({
|
||||
fetchIssues: () => Promise.resolve(undefined),
|
||||
fetchNextIssues: () => Promise.resolve(undefined),
|
||||
removeIssue: () => Promise.resolve(undefined),
|
||||
updateFilters: () => Promise.resolve(undefined),
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export const hideFloatingBot = () => {};
|
||||
|
||||
export const showFloatingBot = () => {};
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @description Get the i18n key for the project settings page label
|
||||
* @param _settingsKey - The key of the project settings page
|
||||
* @param defaultLabelKey - The default i18n key for the project settings page label
|
||||
* @returns The i18n key for the project settings page label
|
||||
*/
|
||||
export const getProjectSettingsPageLabelI18nKey = (_settingsKey: string, defaultLabelKey: string) => defaultLabelKey;
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
// plane imports
|
||||
import type { EIssuesStoreType } from "@plane/types";
|
||||
// plane web imports
|
||||
import type { TWorkItemFiltersEntityProps } from "@/plane-web/hooks/work-item-filters/use-work-item-filters-config";
|
||||
|
||||
export type TGetAdditionalPropsForProjectLevelFiltersHOCParams = {
|
||||
entityType: EIssuesStoreType;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TGetAdditionalPropsForProjectLevelFiltersHOC = (
|
||||
params: TGetAdditionalPropsForProjectLevelFiltersHOCParams
|
||||
) => TWorkItemFiltersEntityProps;
|
||||
|
||||
export const getAdditionalProjectLevelFiltersHOCProps: TGetAdditionalPropsForProjectLevelFiltersHOC = ({
|
||||
workspaceSlug,
|
||||
}) => ({
|
||||
workspaceSlug,
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export type TRenderSettingsLink = (workspaceSlug: string, settingKey: string) => boolean;
|
||||
export const shouldRenderSettingLink: TRenderSettingsLink = (workspaceSlug, settingKey) => true;
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// layouts
|
||||
import { ProjectAuthWrapper as CoreProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper";
|
||||
|
||||
export type IProjectAuthWrapper = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IProjectAuthWrapper) {
|
||||
// props
|
||||
const { workspaceSlug, projectId, children } = props;
|
||||
|
||||
return (
|
||||
<CoreProjectAuthWrapper workspaceSlug={workspaceSlug} projectId={projectId}>
|
||||
{children}
|
||||
</CoreProjectAuthWrapper>
|
||||
);
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// layouts
|
||||
import { WorkspaceAuthWrapper as CoreWorkspaceAuthWrapper } from "@/layouts/auth-layout/workspace-wrapper";
|
||||
|
||||
export type IWorkspaceAuthWrapper = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const WorkspaceAuthWrapper = observer(function WorkspaceAuthWrapper(props: IWorkspaceAuthWrapper) {
|
||||
// props
|
||||
const { children } = props;
|
||||
|
||||
return <CoreWorkspaceAuthWrapper>{children}</CoreWorkspaceAuthWrapper>;
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./project";
|
||||
export * from "@/services/workspace.service";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user