Compare commits

..
Author SHA1 Message Date
NarayanBavisetti 28612a975b chore: corrected the issue activity for drafts 2024-09-06 10:45:32 +05:30
213 changed files with 2167 additions and 2835 deletions
-1
View File
@@ -1 +0,0 @@
*.sh text eol=lf
+13 -48
View File
@@ -2,12 +2,6 @@ name: Branch Build
on:
workflow_dispatch:
inputs:
arm64:
description: "Build for ARM64 architecture"
required: false
default: false
type: boolean
push:
branches:
- master
@@ -17,8 +11,6 @@ on:
env:
TARGET_BRANCH: ${{ github.ref_name || github.event.release.target_commitish }}
ARM64_BUILD: ${{ github.event.inputs.arm64 }}
IS_PRERELEASE: ${{ github.event.release.prerelease }}
jobs:
branch_build_setup:
@@ -36,13 +28,12 @@ jobs:
build_space: ${{ steps.changed_files.outputs.space_any_changed }}
build_web: ${{ steps.changed_files.outputs.web_any_changed }}
build_live: ${{ steps.changed_files.outputs.live_any_changed }}
flat_branch_name: ${{ steps.set_env_variables.outputs.FLAT_BRANCH_NAME }}
steps:
- id: set_env_variables
name: Set Environment Variables
run: |
if [ "${{ env.TARGET_BRANCH }}" == "master" ] || [ "${{ env.ARM64_BUILD }}" == "true" ] || ([ "${{ github.event_name }}" == "release" ] && [ "${{ env.IS_PRERELEASE }}" != "true" ]); then
if [ "${{ env.TARGET_BRANCH }}" == "master" ] || [ "${{ github.event_name }}" == "release" ]; then
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
@@ -54,8 +45,6 @@ jobs:
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
fi
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
flat_branch_name=$(echo ${{ env.TARGET_BRANCH }} | sed 's/[^a-zA-Z0-9\._]/-/g')
echo "FLAT_BRANCH_NAME=${flat_branch_name}" >> $GITHUB_OUTPUT
- id: checkout_files
name: Checkout Files
@@ -101,11 +90,10 @@ jobs:
branch_build_push_web:
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Web Docker Image
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
FRONTEND_TAG: makeplane/plane-frontend:${{ needs.branch_build_setup.outputs.flat_branch_name }}
FRONTEND_TAG: makeplane/plane-frontend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
@@ -115,10 +103,7 @@ jobs:
- name: Set Frontend Docker Tag
run: |
if [ "${{ github.event_name }}" == "release" ]; then
TAG=makeplane/plane-frontend:${{ github.event.release.tag_name }}
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
TAG=${TAG},makeplane/plane-frontend:stable
fi
TAG=makeplane/plane-frontend:stable,makeplane/plane-frontend:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-frontend:latest
else
@@ -157,11 +142,10 @@ jobs:
branch_build_push_admin:
if: ${{ needs.branch_build_setup.outputs.build_admin== 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Admin Docker Image
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
ADMIN_TAG: makeplane/plane-admin:${{ needs.branch_build_setup.outputs.flat_branch_name }}
ADMIN_TAG: makeplane/plane-admin:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
@@ -171,10 +155,7 @@ jobs:
- name: Set Admin Docker Tag
run: |
if [ "${{ github.event_name }}" == "release" ]; then
TAG=makeplane/plane-admin:${{ github.event.release.tag_name }}
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
TAG=${TAG},makeplane/plane-admin:stable
fi
TAG=makeplane/plane-admin:stable,makeplane/plane-admin:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-admin:latest
else
@@ -213,11 +194,10 @@ jobs:
branch_build_push_space:
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Space Docker Image
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
SPACE_TAG: makeplane/plane-space:${{ needs.branch_build_setup.outputs.flat_branch_name }}
SPACE_TAG: makeplane/plane-space:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
@@ -227,10 +207,7 @@ jobs:
- name: Set Space Docker Tag
run: |
if [ "${{ github.event_name }}" == "release" ]; then
TAG=makeplane/plane-space:${{ github.event.release.tag_name }}
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
TAG=${TAG},makeplane/plane-space:stable
fi
TAG=makeplane/plane-space:stable,makeplane/plane-space:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-space:latest
else
@@ -269,11 +246,10 @@ jobs:
branch_build_push_apiserver:
if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push API Server Docker Image
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
BACKEND_TAG: makeplane/plane-backend:${{ needs.branch_build_setup.outputs.flat_branch_name }}
BACKEND_TAG: makeplane/plane-backend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
@@ -283,10 +259,7 @@ jobs:
- name: Set Backend Docker Tag
run: |
if [ "${{ github.event_name }}" == "release" ]; then
TAG=makeplane/plane-backend:${{ github.event.release.tag_name }}
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
TAG=${TAG},makeplane/plane-backend:stable
fi
TAG=makeplane/plane-backend:stable,makeplane/plane-backend:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-backend:latest
else
@@ -325,11 +298,10 @@ jobs:
branch_build_push_live:
if: ${{ needs.branch_build_setup.outputs.build_live == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Live Collaboration Docker Image
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
LIVE_TAG: makeplane/plane-live:${{ needs.branch_build_setup.outputs.flat_branch_name }}
LIVE_TAG: makeplane/plane-live:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
@@ -339,10 +311,7 @@ jobs:
- name: Set Live Docker Tag
run: |
if [ "${{ github.event_name }}" == "release" ]; then
TAG=makeplane/plane-live:${{ github.event.release.tag_name }}
if [ "${{ github.event.release.prerelease }}" != "true" ]; then
TAG=${TAG},makeplane/plane-live:stable
fi
TAG=makeplane/plane-live:stable,makeplane/plane-live:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-live:latest
else
@@ -381,11 +350,10 @@ jobs:
branch_build_push_proxy:
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Proxy Docker Image
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
PROXY_TAG: makeplane/plane-proxy:${{ needs.branch_build_setup.outputs.flat_branch_name }}
PROXY_TAG: makeplane/plane-proxy:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
@@ -395,10 +363,7 @@ jobs:
- name: Set Proxy Docker Tag
run: |
if [ "${{ github.event_name }}" == "release" ]; then
TAG=makeplane/plane-proxy:${{ github.event.release.tag_name }}
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
TAG=${TAG},makeplane/plane-proxy:stable
fi
TAG=makeplane/plane-proxy:stable,makeplane/plane-proxy:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-proxy:latest
else
@@ -10,9 +10,8 @@ import {
// components
import { AuthenticationMethodCard } from "@/components/authentication";
// helpers
import { UpgradeButton } from "@/components/common/upgrade-button";
import { getBaseAuthenticationModes } from "@/helpers/authentication.helper";
// plane admin components
import { UpgradeButton } from "@/plane-admin/components/common";
// images
import OIDCLogo from "@/public/logos/oidc-logo.svg";
import SAMLLogo from "@/public/logos/saml-logo.svg";
@@ -28,24 +27,24 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
updateConfig,
resolvedTheme,
}) => [
...getBaseAuthenticationModes({ disabled, updateConfig, resolvedTheme }),
{
key: "oidc",
name: "OIDC",
description: "Authenticate your users via the OpenID Connect protocol.",
icon: <Image src={OIDCLogo} height={22} width={22} alt="OIDC Logo" />,
config: <UpgradeButton />,
unavailable: true,
},
{
key: "saml",
name: "SAML",
description: "Authenticate your users via the Security Assertion Markup Language protocol.",
icon: <Image src={SAMLLogo} height={22} width={22} alt="SAML Logo" className="pl-0.5" />,
config: <UpgradeButton />,
unavailable: true,
},
];
...getBaseAuthenticationModes({ disabled, updateConfig, resolvedTheme }),
{
key: "oidc",
name: "OIDC",
description: "Authenticate your users via the OpenID Connect protocol.",
icon: <Image src={OIDCLogo} height={22} width={22} alt="OIDC Logo" />,
config: <UpgradeButton />,
unavailable: true,
},
{
key: "saml",
name: "SAML",
description: "Authenticate your users via the Security Assertion Markup Language protocol.",
icon: <Image src={SAMLLogo} height={22} width={22} alt="SAML Logo" className="pl-0.5" />,
config: <UpgradeButton />,
unavailable: true,
},
];
export const AuthenticationModes: React.FC<TAuthenticationModeProps> = observer((props) => {
const { disabled, updateConfig } = props;
-1
View File
@@ -1 +0,0 @@
export * from "./upgrade-button";
-19
View File
@@ -1,19 +0,0 @@
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();
}
}
+1
View File
@@ -8,3 +8,4 @@ export * from "./empty-state";
export * from "./logo-spinner";
export * from "./page-header";
export * from "./code-block";
export * from "./upgrade-button";
-1
View File
@@ -18,7 +18,6 @@ export const AdminLayout: FC<TAdminLayout> = observer((props) => {
const { children } = props;
// router
const router = useRouter();
// store hooks
const { isUserLoggedIn } = useUser();
useEffect(() => {
+2 -2
View File
@@ -1,8 +1,8 @@
"use client";
import { ReactNode, createContext } from "react";
// plane admin store
import { RootStore } from "@/plane-admin/store/root.store";
// store
import { RootStore } from "@/store/root.store";
let rootStore = new RootStore();
+2 -2
View File
@@ -13,7 +13,7 @@ import { EInstanceStatus, TInstanceStatus } from "@/helpers/instance.helper";
// services
import { InstanceService } from "@/services/instance.service";
// root store
import { CoreRootStore } from "@/store/root.store";
import { RootStore } from "@/store/root.store";
export interface IInstanceStore {
// issues
@@ -46,7 +46,7 @@ export class InstanceStore implements IInstanceStore {
// service
instanceService;
constructor(private store: CoreRootStore) {
constructor(private store: RootStore) {
makeObservable(this, {
// observable
isLoading: observable.ref,
+1 -1
View File
@@ -6,7 +6,7 @@ import { IUserStore, UserStore } from "./user.store";
enableStaticRendering(typeof window === "undefined");
export abstract class CoreRootStore {
export class RootStore {
theme: IThemeStore;
instance: IInstanceStore;
user: IUserStore;
+2 -2
View File
@@ -1,6 +1,6 @@
import { action, observable, makeObservable } from "mobx";
// root store
import { CoreRootStore } from "@/store/root.store";
import { RootStore } from "@/store/root.store";
type TTheme = "dark" | "light";
export interface IThemeStore {
@@ -21,7 +21,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,
+2 -2
View File
@@ -6,7 +6,7 @@ import { EUserStatus, TUserStatus } from "@/helpers/user.helper";
import { AuthService } from "@/services/auth.service";
import { UserService } from "@/services/user.service";
// root store
import { CoreRootStore } from "@/store/root.store";
import { RootStore } from "@/store/root.store";
export interface IUserStore {
// observables
@@ -31,7 +31,7 @@ export class UserStore implements IUserStore {
userService;
authService;
constructor(private store: CoreRootStore) {
constructor(private store: RootStore) {
makeObservable(this, {
// observables
isLoading: observable.ref,
-1
View File
@@ -1 +0,0 @@
export * from "ce/components/common";
-1
View File
@@ -1 +0,0 @@
export * from "ce/store/root.store";
+4 -4
View File
@@ -1,3 +1,6 @@
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
# Django imports
from django.utils import timezone
from lxml import html
@@ -27,9 +30,6 @@ from .module import ModuleLiteSerializer, ModuleSerializer
from .state import StateLiteSerializer
from .user import UserLiteSerializer
# Django imports
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
class IssueSerializer(BaseSerializer):
assignees = serializers.ListField(
@@ -315,7 +315,7 @@ class IssueLinkSerializer(BaseSerializer):
"created_at",
"updated_at",
]
def validate_url(self, value):
# Check URL format
validate_url = URLValidator()
-23
View File
@@ -71,16 +71,6 @@ class ModuleSerializer(BaseSerializer):
project_id = self.context["project_id"]
workspace_id = self.context["workspace_id"]
module_name = validated_data.get("name")
if module_name:
# Lookup for the module name in the module table for that project
if Module.objects.filter(
name=module_name, project_id=project_id
).exists():
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
module = Module.objects.create(**validated_data, project_id=project_id)
if members is not None:
ModuleMember.objects.bulk_create(
@@ -103,19 +93,6 @@ class ModuleSerializer(BaseSerializer):
def update(self, instance, validated_data):
members = validated_data.pop("members", None)
module_name = validated_data.get("name")
if module_name:
# Lookup for the module name in the module table for that project
if (
Module.objects.filter(
name=module_name, project=instance.project
)
.exclude(id=instance.id)
.exists()
):
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
if members is not None:
ModuleMember.objects.filter(module=instance).delete()
+10 -20
View File
@@ -437,21 +437,17 @@ class IssueLinkSerializer(BaseSerializer):
"issue",
]
def to_internal_value(self, data):
# Modify the URL before validation by appending http:// if missing
url = data.get("url", "")
if url and not url.startswith(("http://", "https://")):
data["url"] = "http://" + url
return super().to_internal_value(data)
def validate_url(self, value):
# Use Django's built-in URLValidator for validation
url_validator = URLValidator()
# Check URL format
validate_url = URLValidator()
try:
url_validator(value)
validate_url(value)
except ValidationError:
raise serializers.ValidationError({"error": "Invalid URL format."})
raise serializers.ValidationError("Invalid URL format.")
# Check URL scheme
if not value.startswith(("http://", "https://")):
raise serializers.ValidationError("Invalid URL scheme.")
return value
@@ -537,7 +533,7 @@ class IssueReactionSerializer(BaseSerializer):
"project",
"issue",
"actor",
"deleted_at",
"deleted_at"
]
@@ -556,13 +552,7 @@ class CommentReactionSerializer(BaseSerializer):
class Meta:
model = CommentReaction
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"comment",
"actor",
"deleted_at",
]
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at"]
class IssueVoteSerializer(BaseSerializer):
+3 -69
View File
@@ -5,10 +5,6 @@ from rest_framework import serializers
from .base import BaseSerializer, DynamicBaseSerializer
from .project import ProjectLiteSerializer
# Django imports
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
from plane.db.models import (
User,
Module,
@@ -68,16 +64,6 @@ class ModuleWriteSerializer(BaseSerializer):
members = validated_data.pop("member_ids", None)
project = self.context["project"]
module_name = validated_data.get("name")
if module_name:
# Lookup for the module name in the module table for that project
if Module.objects.filter(
name=module_name, project=project
).exists():
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
module = Module.objects.create(**validated_data, project=project)
if members is not None:
ModuleMember.objects.bulk_create(
@@ -100,19 +86,6 @@ class ModuleWriteSerializer(BaseSerializer):
def update(self, instance, validated_data):
members = validated_data.pop("member_ids", None)
module_name = validated_data.get("name")
if module_name:
# Lookup for the module name in the module table for that project
if (
Module.objects.filter(
name=module_name, project=instance.project
)
.exclude(id=instance.id)
.exists()
):
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
if members is not None:
ModuleMember.objects.filter(module=instance).delete()
@@ -182,48 +155,16 @@ class ModuleLinkSerializer(BaseSerializer):
"module",
]
def to_internal_value(self, data):
# Modify the URL before validation by appending http:// if missing
url = data.get("url", "")
if url and not url.startswith(("http://", "https://")):
data["url"] = "http://" + url
return super().to_internal_value(data)
def validate_url(self, value):
# Use Django's built-in URLValidator for validation
url_validator = URLValidator()
try:
url_validator(value)
except ValidationError:
raise serializers.ValidationError({"error": "Invalid URL format."})
return value
# Validation if url already exists
def create(self, validated_data):
validated_data["url"] = self.validate_url(validated_data.get("url"))
if ModuleLink.objects.filter(
url=validated_data.get("url"),
module_id=validated_data.get("module_id"),
).exists():
raise serializers.ValidationError({"error": "URL already exists."})
return super().create(validated_data)
def update(self, instance, validated_data):
validated_data["url"] = self.validate_url(validated_data.get("url"))
if (
ModuleLink.objects.filter(
url=validated_data.get("url"),
module_id=instance.module_id,
)
.exclude(pk=instance.id)
.exists()
):
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
return super().update(instance, validated_data)
return ModuleLink.objects.create(**validated_data)
class ModuleSerializer(DynamicBaseSerializer):
@@ -288,14 +229,7 @@ class ModuleDetailSerializer(ModuleSerializer):
cancelled_estimate_points = serializers.FloatField(read_only=True)
class Meta(ModuleSerializer.Meta):
fields = ModuleSerializer.Meta.fields + [
"link_module",
"sub_issues",
"backlog_estimate_points",
"unstarted_estimate_points",
"started_estimate_points",
"cancelled_estimate_points",
]
fields = ModuleSerializer.Meta.fields + ["link_module", "sub_issues", "backlog_estimate_points", "unstarted_estimate_points", "started_estimate_points", "cancelled_estimate_points"]
class ModuleUserPropertiesSerializer(BaseSerializer):
+2 -2
View File
@@ -40,7 +40,7 @@ class WebhookSerializer(DynamicBaseSerializer):
for addr in ip_addresses:
ip = ipaddress.ip_address(addr[4][0])
if ip.is_loopback:
if ip.is_private or ip.is_loopback:
raise serializers.ValidationError(
{"url": "URL resolves to a blocked IP address."}
)
@@ -92,7 +92,7 @@ class WebhookSerializer(DynamicBaseSerializer):
for addr in ip_addresses:
ip = ipaddress.ip_address(addr[4][0])
if ip.is_loopback:
if ip.is_private or ip.is_loopback:
raise serializers.ValidationError(
{"url": "URL resolves to a blocked IP address."}
)
+1 -1
View File
@@ -42,7 +42,7 @@ class IssueActivityEndpoint(BaseAPIView):
issue_activities = (
IssueActivity.objects.filter(issue_id=issue_id)
.filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]),
~Q(field__in=["comment", "vote", "reaction"]),
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
@@ -1530,19 +1530,7 @@ def update_draft_issue_activity(
workspace_id=workspace_id,
comment="created the issue",
verb="updated",
actor_id=actor_id,
epoch=epoch,
)
)
else:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project_id=project_id,
workspace_id=workspace_id,
comment="updated the draft issue",
field="draft",
verb="updated",
actor_id=actor_id,
epoch=epoch,
)
+2 -2
View File
@@ -442,10 +442,10 @@ function backupData() {
local BACKUP_FOLDER=$PLANE_INSTALL_DIR/backup/$datetime
mkdir -p "$BACKUP_FOLDER"
volumes=$(docker volume ls -f "name=$SERVICE_FOLDER" --format "{{.Name}}" | grep -E "_pgdata|_redisdata|_uploads")
volumes=$(docker volume ls -f "name=plane-app" --format "{{.Name}}" | grep -E "_pgdata|_redisdata|_uploads")
# Check if there are any matching volumes
if [ -z "$volumes" ]; then
echo "No volumes found starting with '$SERVICE_FOLDER'"
echo "No volumes found starting with 'plane-app'"
exit 1
fi
+5 -6
View File
@@ -12,7 +12,6 @@ DEBUG=0
SENTRY_DSN=
SENTRY_ENVIRONMENT=production
CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN}
API_BASE_URL=http://api:8000
#DB SETTINGS
PGHOST=plane-db
@@ -30,11 +29,11 @@ REDIS_PORT=6379
REDIS_URL=
# RabbitMQ Settings
RABBITMQ_HOST=plane-mq
RABBITMQ_PORT=5672
RABBITMQ_USER=plane
RABBITMQ_PASSWORD=plane
RABBITMQ_VHOST=plane
RABBITMQ_HOST="plane-mq"
RABBITMQ_PORT="5672"
RABBITMQ_USER="plane"
RABBITMQ_PASSWORD="plane"
RABBITMQ_VHOST="plane"
AMQP_URL=
# Secret Key
+1 -4
View File
@@ -24,12 +24,10 @@
"@tiptap/core": "^2.4.0",
"@tiptap/html": "^2.3.0",
"axios": "^1.7.2",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.20.0",
"express": "^4.19.2",
"express-ws": "^5.0.2",
"helmet": "^7.1.0",
"ioredis": "^5.4.1",
"lodash": "^4.17.21",
"morgan": "^1.10.0",
@@ -44,7 +42,6 @@
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@babel/preset-typescript": "^7.24.7",
"@types/compression": "^1.7.5",
"@types/cors": "^2.8.17",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.21",
+25 -33
View File
@@ -24,7 +24,7 @@ import { TDocumentTypes } from "@/core/types/common.js";
import { fetchDocument } from "@/plane-live/lib/fetch-document.js";
import { updateDocument } from "@/plane-live/lib/update-document.js";
export const getExtensions: () => Promise<Extension[]> = async () => {
export const getExtensions: () => Extension[] = () => {
const extensions: Extension[] = [
new Logger({
onChange: false,
@@ -65,7 +65,7 @@ export const getExtensions: () => Promise<Extension[]> = async () => {
}
resolve(fetchedData);
} catch (error) {
manualLogger.error("Error in fetching document", error);
console.error("Error in fetching document", error);
}
});
},
@@ -97,7 +97,7 @@ export const getExtensions: () => Promise<Extension[]> = async () => {
});
}
} catch (error) {
manualLogger.error("Error in updating document:", error);
console.error("Error in updating document", error);
}
});
},
@@ -106,42 +106,34 @@ export const getExtensions: () => Promise<Extension[]> = async () => {
const redisUrl = getRedisUrl();
// Add the Redis extension only if configured
if (redisUrl) {
try {
const redisClient = new Redis(redisUrl);
await new Promise<void>((resolve, reject) => {
redisClient.on("error", (error: any) => {
if (
error?.code === "ENOTFOUND" ||
error.message.includes("WRONGPASS") ||
error.message.includes("NOAUTH")
) {
redisClient.disconnect();
}
manualLogger.warn(
`Redis Client wasn't able to connect, continuing without Redis (you won't be able to sync data between multiple plane live servers)`,
error,
);
reject(error);
});
redisClient.on("ready", () => {
extensions.push(new HocusPocusRedis({ redis: redisClient }));
manualLogger.info("Redis Client connected ✅");
resolve();
});
redisClient.on("error", (error: any) => {
// if auth fails or the server is down, disconnect redis
if (
error?.code === "ENOTFOUND" ||
error.message.includes("WRONGPASS") ||
error.message.includes("NOAUTH")
) {
redisClient.disconnect();
}
manualLogger.error(
`Redis Client wasn't able to connect, continuing without Redis (you won't be able to sync data betwen multiple plane live servers)`,
);
manualLogger.error(error);
});
redisClient.on("ready", () => {
manualLogger.info("Redis Client connected");
});
if (!redisClient) {
throw new Error("Redis client is not defined");
}
extensions.push(new HocusPocusRedis({ redis: redisClient }));
} catch (error) {
manualLogger.warn(
`Redis Client wasn't able to connect, continuing without Redis (you won't be able to sync data between multiple plane live servers)`,
error,
);
manualLogger.error("Failed to connect to Redis:", error);
}
} else {
manualLogger.warn(
"Redis URL is not set, continuing without Redis (you won't be able to sync data between multiple plane live servers)",
);
}
return extensions;
-23
View File
@@ -7,32 +7,9 @@ const transport = {
},
};
const hooks = {
logMethod(inputArgs: any, method: any): any {
if (inputArgs.length >= 2) {
const arg1 = inputArgs.shift();
const arg2 = inputArgs.shift();
return method.apply(this, [arg2, arg1, ...inputArgs]);
}
return method.apply(this, inputArgs);
},
};
export const logger = pinoHttp({
level: "info",
transport: transport,
hooks: hooks,
serializers: {
req(req) {
return `${req.method} ${req.url}`;
},
res(res) {
return `${res.statusCode} ${res?.statusMessage || ""}`;
},
responseTime(time) {
return `${time}ms`;
},
},
});
export const manualLogger = logger.logger;
+28 -31
View File
@@ -3,36 +3,33 @@ import { Server } from "@hocuspocus/server";
import { handleAuthentication } from "@/core/lib/authentication.js";
import { getExtensions } from "@/core/extensions/index.js";
export const getHocusPocusServer = async () => {
const extensions = await getExtensions();
return Server.configure({
onAuthenticate: async ({
requestHeaders,
requestParameters,
connection,
// user id used as token for authentication
token,
}) => {
// request headers
const cookie = requestHeaders.cookie?.toString();
// params
const params = requestParameters;
export const HocusPocusServer = Server.configure({
onAuthenticate: async ({
requestHeaders,
requestParameters,
connection,
// user id used as token for authentication
token,
}) => {
// request headers
const cookie = requestHeaders.cookie?.toString();
// params
const params = requestParameters;
if (!cookie) {
throw Error("Credentials not provided");
}
if (!cookie) {
throw Error("Credentials not provided");
}
try {
await handleAuthentication({
connection,
cookie,
params,
token,
});
} catch (error) {
throw Error("Authentication unsuccessful!");
}
},
extensions,
});
};
try {
await handleAuthentication({
connection,
cookie,
params,
token,
});
} catch (error) {
throw Error("Authentication unsuccessful!");
}
},
extensions: getExtensions(),
});
+11 -18
View File
@@ -5,8 +5,6 @@ import { UserService } from "@/core/services/user.service.js";
import { TDocumentTypes } from "@/core/types/common.js";
// plane live lib
import { authenticateUser } from "@/plane-live/lib/authentication.js";
// core helpers
import { manualLogger } from "@/core/helpers/logger.js";
const userService = new UserService();
@@ -28,7 +26,7 @@ export const handleAuthentication = async (props: Props) => {
try {
response = await userService.currentUser(cookie);
} catch (error) {
manualLogger.error("Failed to fetch current user:", error);
console.error("Failed to fetch current user:", error);
throw error;
}
if (response.id !== token) {
@@ -45,27 +43,22 @@ export const handleAuthentication = async (props: Props) => {
);
}
// fetch current user's project membership info
try {
const projectMembershipInfo = await userService.getUserProjectMembership(
workspaceSlug,
projectId,
cookie
);
const projectRole = projectMembershipInfo.role;
// make the connection read only for roles lower than a member
if (projectRole < 15) {
connection.readOnly = true;
}
} catch (error) {
manualLogger.error("Failed to fetch project membership info:", error);
throw error;
const projectMembershipInfo = await userService.getUserProjectMembership(
workspaceSlug,
projectId,
cookie
);
const projectRole = projectMembershipInfo.role;
// make the connection read only for roles lower than a member
if (projectRole < 15) {
connection.readOnly = true;
}
} else {
await authenticateUser({
connection,
cookie,
documentType,
params,
params
});
}
+18 -24
View File
@@ -1,22 +1,18 @@
// helpers
import {
getAllDocumentFormatsFromBinaryData,
getBinaryDataFromHTMLString,
} from "@/core/helpers/page.js";
import { getAllDocumentFormatsFromBinaryData, getBinaryDataFromHTMLString } from "@/core/helpers/page.js";
// services
import { PageService } from "@/core/services/page.service.js";
import { manualLogger } from "../helpers/logger.js";
const pageService = new PageService();
export const updatePageDescription = async (
params: URLSearchParams,
pageId: string,
updatedDescription: Uint8Array,
cookie: string | undefined,
cookie: string | undefined
) => {
if (!(updatedDescription instanceof Uint8Array)) {
throw new Error(
"Invalid updatedDescription: must be an instance of Uint8Array",
"Invalid updatedDescription: must be an instance of Uint8Array"
);
}
@@ -24,8 +20,11 @@ export const updatePageDescription = async (
const projectId = params.get("projectId")?.toString();
if (!workspaceSlug || !projectId || !cookie) return;
const { contentBinaryEncoded, contentHTML, contentJSON } =
getAllDocumentFormatsFromBinaryData(updatedDescription);
const {
contentBinaryEncoded,
contentHTML,
contentJSON
} = getAllDocumentFormatsFromBinaryData(updatedDescription);
try {
const payload = {
description_binary: contentBinaryEncoded,
@@ -38,10 +37,10 @@ export const updatePageDescription = async (
projectId,
pageId,
payload,
cookie,
cookie
);
} catch (error) {
manualLogger.error("Update error:", error);
console.error("Update error:", error);
throw error;
}
};
@@ -50,7 +49,7 @@ const fetchDescriptionHTMLAndTransform = async (
workspaceSlug: string,
projectId: string,
pageId: string,
cookie: string,
cookie: string
) => {
if (!workspaceSlug || !projectId || !cookie) return;
@@ -59,17 +58,12 @@ const fetchDescriptionHTMLAndTransform = async (
workspaceSlug,
projectId,
pageId,
cookie,
);
const { contentBinary } = getBinaryDataFromHTMLString(
pageDetails.description_html ?? "<p></p>",
cookie
);
const { contentBinary } = getBinaryDataFromHTMLString(pageDetails.description_html ?? "<p></p>")
return contentBinary;
} catch (error) {
manualLogger.error(
"Error while transforming from HTML to Uint8Array",
error,
);
console.error("Error while transforming from HTML to Uint8Array", error);
throw error;
}
};
@@ -77,7 +71,7 @@ const fetchDescriptionHTMLAndTransform = async (
export const fetchPageDescriptionBinary = async (
params: URLSearchParams,
pageId: string,
cookie: string | undefined,
cookie: string | undefined
) => {
const workspaceSlug = params.get("workspaceSlug")?.toString();
const projectId = params.get("projectId")?.toString();
@@ -88,7 +82,7 @@ export const fetchPageDescriptionBinary = async (
workspaceSlug,
projectId,
pageId,
cookie,
cookie
);
const binaryData = new Uint8Array(response);
@@ -97,7 +91,7 @@ export const fetchPageDescriptionBinary = async (
workspaceSlug,
projectId,
pageId,
cookie,
cookie
);
if (binary) {
return binary;
@@ -106,7 +100,7 @@ export const fetchPageDescriptionBinary = async (
return binaryData;
} catch (error) {
manualLogger.error("Fetch error:", error);
console.error("Fetch error:", error);
throw error;
}
};
+4 -68
View File
@@ -3,14 +3,12 @@ import "@/core/config/sentry-config.js";
import express from "express";
import expressWs from "express-ws";
import * as Sentry from "@sentry/node";
import compression from "compression";
import helmet from "helmet";
// cors
import cors from "cors";
// core hocuspocus server
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
import { HocusPocusServer } from "@/core/hocuspocus-server.js";
// helpers
import { logger, manualLogger } from "@/core/helpers/logger.js";
@@ -21,17 +19,6 @@ expressWs(app);
app.set("port", process.env.PORT || 3000);
// Security middleware
app.use(helmet());
// Middleware for response compression
app.use(
compression({
level: 6,
threshold: 5 * 1000,
}),
);
// Logging middleware
app.use(logger);
@@ -44,27 +31,17 @@ app.use(cors());
const router = express.Router();
const HocusPocusServer = await getHocusPocusServer().catch((err) => {
manualLogger.error("Failed to initialize HocusPocusServer:", err);
process.exit(1);
});
router.get("/health", (_req, res) => {
res.status(200).json({ status: "OK" });
});
router.ws("/collaboration", (ws, req) => {
try {
HocusPocusServer.handleConnection(ws, req);
} catch (err) {
manualLogger.error("WebSocket connection error:", err);
ws.close();
}
HocusPocusServer.handleConnection(ws, req);
});
app.use(process.env.LIVE_BASE_PATH || "/live", router);
app.use((_req, res) => {
app.use((_req, res, _next) => {
res.status(404).send("Not Found");
});
@@ -72,47 +49,6 @@ Sentry.setupExpressErrorHandler(app);
app.use(errorHandler);
const liveServer = app.listen(app.get("port"), () => {
app.listen(app.get("port"), () => {
manualLogger.info(`Plane Live server has started at port ${app.get("port")}`);
});
const gracefulShutdown = async () => {
manualLogger.info("Starting graceful shutdown...");
try {
// Close the HocusPocus server WebSocket connections
await HocusPocusServer.destroy();
manualLogger.info(
"HocusPocus server WebSocket connections closed gracefully.",
);
// Close the Express server
liveServer.close(() => {
manualLogger.info("Express server closed gracefully.");
process.exit(1);
});
} catch (err) {
manualLogger.error("Error during shutdown:", err);
process.exit(1);
}
// Forcefully shut down after 10 seconds if not closed
setTimeout(() => {
manualLogger.error("Forcing shutdown...");
process.exit(1);
}, 10000);
};
// Graceful shutdown on unhandled rejection
process.on("unhandledRejection", (err: any) => {
manualLogger.info("Unhandled Rejection: ", err);
manualLogger.info(`UNHANDLED REJECTION! 💥 Shutting down...`);
gracefulShutdown();
});
// Graceful shutdown on uncaught exception
process.on("uncaughtException", (err: any) => {
manualLogger.info("Uncaught Exception: ", err);
manualLogger.info(`UNCAUGHT EXCEPTION! 💥 Shutting down...`);
gracefulShutdown();
});
+1 -1
View File
@@ -35,7 +35,7 @@
"prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4",
"tailwindcss": "^3.3.3",
"turbo": "^2.1.1"
"turbo": "^2.0.14"
},
"resolutions": {
"@types/react": "18.2.48"
@@ -3,14 +3,14 @@ import React from "react";
import { PageRenderer } from "@/components/editors";
// constants
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
// extensions
import { IssueWidget } from "@/extensions";
// helpers
import { getEditorClassNames } from "@/helpers/common";
// hooks
import { useCollaborativeEditor } from "@/hooks/use-collaborative-editor";
// plane editor types
import { TEmbedConfig } from "@/plane-editor/types";
// types
import { EditorRefApi, ICollaborativeDocumentEditor } from "@/types";
import { useCollaborativeEditor } from "@/hooks/use-collaborative-editor";
import { IssueWidget } from "@/extensions";
const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
const {
@@ -18,9 +18,9 @@ export function clickHandler(options: ClickHandlerOptions): Plugin {
let a = event.target as HTMLElement;
const els = [];
while (a?.nodeName !== "DIV") {
while (a.nodeName !== "DIV") {
els.push(a);
a = a?.parentNode as HTMLElement;
a = a.parentNode as HTMLElement;
}
if (!els.find((value) => value.nodeName === "A")) {
@@ -1,4 +1,3 @@
import CharacterCount from "@tiptap/extension-character-count";
import TaskItem from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list";
import TextStyle from "@tiptap/extension-text-style";
@@ -105,5 +104,4 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
mentionHighlights: mentionConfig.mentionHighlights,
readonly: true,
}),
CharacterCount,
];
@@ -63,7 +63,7 @@ const Command = Extension.create<SlashCommandOptions>({
const { selection } = editor.state;
const parentNode = selection.$from.node(selection.$from.depth);
const blockType = parentNode?.type?.name;
const blockType = parentNode.type.name;
if (blockType === "codeBlock") {
return false;
@@ -146,7 +146,7 @@ export const insertImageCommand = (
if (range) editor.chain().focus().deleteRange(range).run();
const input = document.createElement("input");
input.type = "file";
input.accept = ".jpeg, .jpg, .png, .webp";
input.accept = ".jpeg, .jpg, .png, .webp, .svg";
input.onchange = async () => {
if (input.files?.length) {
const file = input.files[0];
+4 -6
View File
@@ -230,12 +230,10 @@ export const useEditor = (props: CustomEditorProps) => {
editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
}
},
getDocumentInfo: () => {
return {
characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editorRef?.current?.state),
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
};
documentInfo: {
characters: editorRef.current?.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editorRef.current?.state),
words: editorRef.current?.storage?.characterCount?.words?.() ?? 0,
},
}),
[editorRef, savedSelection, fileHandler.upload]
@@ -82,12 +82,10 @@ export const useReadOnlyEditor = ({
if (!editorRef.current) return;
scrollSummary(editorRef.current, marking);
},
getDocumentInfo: () => {
return {
characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editorRef?.current?.state),
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
};
documentInfo: {
characters: editorRef.current?.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editorRef.current?.state),
words: editorRef.current?.storage?.characterCount?.words?.() ?? 0,
},
}));
@@ -4,9 +4,9 @@ export function isFileValid(file: File): boolean {
return false;
}
const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/webp"];
const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/svg+xml"];
if (!allowedTypes.includes(file.type)) {
alert("Invalid file type. Please select a JPEG, JPG, PNG, or WEBP image file.");
alert("Invalid file type. Please select a JPEG, JPG, PNG, WEBP, or SVG image file.");
return false;
}
+1 -1
View File
@@ -20,7 +20,7 @@ export type EditorReadOnlyRefApi = {
clearEditor: (emitUpdate?: boolean) => void;
setEditorValue: (content: string) => void;
scrollSummary: (marking: IMarking) => void;
getDocumentInfo: () => {
documentInfo: {
characters: number;
paragraphs: number;
words: number;
+3 -3
View File
@@ -170,13 +170,13 @@ ul[data-type="taskList"] li > label input[type="checkbox"]:hover {
background-color: rgba(var(--color-background-80)) !important;
}
ul[data-type="taskList"] li > label input[type="checkbox"][checked] {
ul[data-type="taskList"] li > label input[type="checkbox"]:checked {
background-color: rgba(var(--color-primary-100)) !important;
border-color: rgba(var(--color-primary-100)) !important;
color: white !important;
}
ul[data-type="taskList"] li > label input[type="checkbox"][checked]:hover {
ul[data-type="taskList"] li > label input[type="checkbox"]:checked:hover {
background-color: rgba(var(--color-primary-300)) !important;
border-color: rgba(var(--color-primary-300)) !important;
}
@@ -229,7 +229,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
&[checked]::before {
&:checked::before {
transform: scale(1) translate(-50%, -50%);
}
}
@@ -18,8 +18,8 @@ module.exports = {
"./pages/**/*.tsx",
"./app/**/*.tsx",
"./ui/**/*.tsx",
"../packages/ui/src/**/*.{js,ts,jsx,tsx}",
"../packages/editor/src/**/*.{js,ts,jsx,tsx}",
"../packages/ui/**/*.{js,ts,jsx,tsx}",
"../packages/editor/**/src/**/*.{js,ts,jsx,tsx}",
"!../packages/ui/**/*.stories{js,ts,jsx,tsx}",
],
},
@@ -334,7 +334,7 @@ module.exports = {
80: "18rem",
96: "21.6rem",
"page-x": "1.35rem",
"page-y": "1.35rem",
"page-y": "1.35rem"
},
margin: {
0: "0",
@@ -436,26 +436,23 @@ module.exports = {
custom: ["Inter", "sans-serif"],
},
},
plugins: [
require("tailwindcss-animate"),
require("@tailwindcss/typography"),
function ({ addUtilities }) {
const newUtilities = {
// Mobile screens
".px-page-x": {
paddingLeft: "1.25rem",
paddingRight: "1.25rem",
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography"), function({ addUtilities }) {
const newUtilities = {
// Mobile screens
'.px-page-x': {
paddingLeft: '1.25rem',
paddingRight: '1.25rem',
},
// Medium screens (768px and up)
'@media (min-width: 768px)': {
'.px-page-x': {
paddingLeft: '1.35rem',
paddingRight: '1.35rem',
},
// Medium screens (768px and up)
"@media (min-width: 768px)": {
".px-page-x": {
paddingLeft: "1.35rem",
paddingRight: "1.35rem",
},
},
};
}
};
addUtilities(newUtilities, ["responsive"]);
},
],
addUtilities(newUtilities, ['responsive']);
},
],
};
-1
View File
@@ -109,7 +109,6 @@ export type TWidgetIssue = TIssue & {
project_id: string;
relation_type: TIssueRelationTypes;
sequence_id: number;
type_id: string | null;
}[];
};
@@ -14,12 +14,10 @@ interface IInputSearch {
inputContainerClassName?: string;
inputClassName?: string;
inputPlaceholder?: string;
isMobile: boolean;
}
export const InputSearch: FC<IInputSearch> = (props) => {
const { isOpen, query, updateQuery, inputIcon, inputContainerClassName, inputClassName, inputPlaceholder, isMobile } =
props;
const { isOpen, query, updateQuery, inputIcon, inputContainerClassName, inputClassName, inputPlaceholder } = props;
const inputRef = useRef<HTMLInputElement | null>(null);
@@ -31,10 +29,10 @@ export const InputSearch: FC<IInputSearch> = (props) => {
};
useEffect(() => {
if (isOpen && !isMobile) {
if (isOpen) {
inputRef.current && inputRef.current.focus();
}
}, [isOpen, isMobile]);
}, [isOpen]);
return (
<div
className={cn(
@@ -26,7 +26,6 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
value,
renderItem,
loader,
isMobile = false,
} = props;
return (
<>
@@ -39,7 +38,6 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
inputPlaceholder={inputPlaceholder}
inputClassName={inputClassName}
inputContainerClassName={inputContainerClassName}
isMobile={isMobile}
/>
)}
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
-1
View File
@@ -85,7 +85,6 @@ export interface IDropdownOptions {
renderItem: (({ value, selected }: { value: string; selected: boolean }) => React.ReactNode) | undefined;
options: TDropdownOption[] | undefined;
loader?: React.ReactNode;
isMobile?: boolean;
}
export interface IMultiSelectDropdownOptions extends IDropdownOptions {
+10 -27
View File
@@ -11,7 +11,6 @@ export interface HeaderProps {
showOnMobile?: boolean;
}
const HeaderContext = React.createContext<THeaderVariant | null>(null);
const Header = (props: HeaderProps) => {
const {
variant = EHeaderVariant.PRIMARY,
@@ -24,15 +23,13 @@ const Header = (props: HeaderProps) => {
const style = getHeaderStyle(variant, setHeight, showOnMobile);
return (
<HeaderContext.Provider value={variant}>
<Row
variant={variant === EHeaderVariant.PRIMARY ? ERowVariant.HUGGING : ERowVariant.REGULAR}
className={cn(style, className)}
{...rest}
>
{children}
</Row>
</HeaderContext.Provider>
<Row
variant={variant === EHeaderVariant.PRIMARY ? ERowVariant.HUGGING : ERowVariant.REGULAR}
className={cn(style, className)}
{...rest}
>
{children}
</Row>
);
};
@@ -43,23 +40,9 @@ const LeftItem = (props: HeaderProps) => (
{props.children}
</div>
);
const RightItem = (props: HeaderProps) => {
const variant = React.useContext(HeaderContext);
if (variant === undefined) throw new Error("RightItem must be used within Header");
return (
<div
className={cn(
"flex justify-end gap-3 w-auto items-start",
{
"items-baseline": variant === EHeaderVariant.TERNARY,
},
props.className
)}
>
{props.children}
</div>
);
};
const RightItem = (props: HeaderProps) => (
<div className={cn("flex justify-end gap-3 w-auto items-baseline", props.className)}>{props.children}</div>
);
Header.LeftItem = LeftItem;
Header.RightItem = RightItem;
@@ -1,6 +1,6 @@
"use client";
import { FC, FormEvent, useMemo, useRef, useState } from "react";
import { FC, FormEvent, useMemo, useState } from "react";
import { observer } from "mobx-react";
// icons
import { CircleAlert, XCircle } from "lucide-react";
@@ -22,6 +22,7 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
// states
const [isSubmitting, setIsSubmitting] = useState(false);
const [email, setEmail] = useState(defaultEmail);
const [isFocused, setFocused] = useState(false);
const emailError = useMemo(
() => (email && !checkEmailValidity(email) ? { email: "Email is invalid" } : undefined),
@@ -40,9 +41,6 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
const isButtonDisabled = email.length === 0 || Boolean(emailError?.email) || isSubmitting;
const [isFocused, setIsFocused] = useState(true)
const inputRef = useRef<HTMLInputElement>(null);
return (
<form onSubmit={handleFormSubmit} className="mt-5 space-y-4">
<div className="space-y-1">
@@ -54,9 +52,6 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
`relative flex items-center rounded-md bg-onboarding-background-200 border`,
!isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-onboarding-border-100`
)}
tabIndex={-1}
onFocus={() => {setIsFocused(true)}}
onBlur={() => {setIsFocused(false)}}
>
<Input
id="email"
@@ -66,17 +61,15 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
onChange={(e) => setEmail(e.target.value)}
placeholder="name@company.com"
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
autoComplete="on"
autoFocus
ref={inputRef}
/>
{email.length > 0 && (
{email.length > 0 && (
<XCircle
className="h-[46px] w-11 px-3 stroke-custom-text-400 hover:cursor-pointer text-xs"
onClick={() => {
setEmail("");
inputRef.current?.focus();
}}
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setEmail("")}
/>
)}
</div>
@@ -92,4 +85,4 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
</Button>
</form>
);
});
});
@@ -3,7 +3,7 @@ import Image from "next/image";
import SomethingWentWrongImage from "public/something-went-wrong.svg";
export const SomethingWentWrongError = () => (
<div className="grid min-h-screen w-full place-items-center p-6">
<div className="grid h-full w-full place-items-center p-6">
<div className="text-center">
<div className="mx-auto grid h-52 w-52 place-items-center rounded-full bg-custom-background-80">
<div className="grid h-32 w-32 place-items-center">
@@ -36,12 +36,12 @@ const AnalyticsPage = observer(() => {
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
<Header variant={EHeaderVariant.SECONDARY}>
<Tab.List as="div" className="flex space-x-2 h-full">
<Tab.List as="div" className="flex space-x-2 border-b h-[50px] border-custom-border-200">
{ANALYTICS_TABS.map((tab) => (
<Tab key={tab.key} as={Fragment}>
{({ selected }) => (
<button
className={`text-sm group relative flex items-center gap-1 h-full px-3 cursor-pointer transition-all font-medium outline-none ${
className={`text-sm group relative flex items-center gap-1 h-[50px] px-3 cursor-pointer transition-all font-medium outline-none ${
selected ? "text-custom-primary-100 " : "hover:text-custom-text-200"
}`}
>
@@ -48,63 +48,63 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
return (
<Header>
<Header.LeftItem>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label={breadcrumbLabel}
disableTooltip
icon={<UserActivityIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
</Breadcrumbs>
</Header.LeftItem>
<Header.RightItem>
<div className="hidden md:flex md:items-center">{showProfileIssuesFilter && <ProfileIssuesFilter />}</div>
<div className="flex gap-4 md:hidden">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-sm text-custom-text-200"
placement="bottom-start"
customButton={
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1.5">
<span className="flex flex-grow justify-center text-sm text-custom-text-200">{type}</span>
<ChevronDown className="h-4 w-4 text-custom-text-400" />
</div>
}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
<></>
{tabsList.map((tab) => (
<CustomMenu.MenuItem className="flex items-center gap-2" key={tab.route}>
<Link
key={tab.route}
href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}
className="w-full text-custom-text-300"
>
{tab.label}
</Link>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<button
className="block transition-all md:hidden"
onClick={() => {
toggleProfileSidebar();
}}
>
<PanelRight
className={cn(
"block h-4 w-4 md:hidden",
!profileSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200"
)}
<div className="flex w-full justify-between">
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label={breadcrumbLabel}
disableTooltip
icon={<UserActivityIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
</button>
</Breadcrumbs>
<div className="hidden md:flex md:items-center">{showProfileIssuesFilter && <ProfileIssuesFilter />}</div>
<div className="flex gap-4 md:hidden">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-sm text-custom-text-200"
placement="bottom-start"
customButton={
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1.5">
<span className="flex flex-grow justify-center text-sm text-custom-text-200">{type}</span>
<ChevronDown className="h-4 w-4 text-custom-text-400" />
</div>
}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
<></>
{tabsList.map((tab) => (
<CustomMenu.MenuItem className="flex items-center gap-2" key={tab.route}>
<Link
key={tab.route}
href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}
className="w-full text-custom-text-300"
>
{tab.label}
</Link>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<button
className="block transition-all md:hidden"
onClick={() => {
toggleProfileSidebar();
}}
>
<PanelRight
className={cn(
"block h-4 w-4 md:hidden",
!profileSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200"
)}
/>
</button>
</div>
</div>
</Header.RightItem>
</Header.LeftItem>
</Header>
);
});
@@ -8,7 +8,7 @@ import { ChevronDown } from "lucide-react";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
// ui
import { CustomMenu } from "@plane/ui";
import { CustomMenu, Row } from "@plane/ui";
// components
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
// constants
@@ -109,18 +109,18 @@ export const ProfileIssuesMobileHeader = observer(() => {
);
return (
<div className="flex justify-evenly border-b border-custom-border-200 py-2 md:hidden">
<div className="flex justify-start border-b border-custom-border-200 py-2 md:hidden">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-sm text-custom-text-200"
className="flex justify-center text-sm text-custom-text-200"
placement="bottom-start"
customButton={
<div className="flex flex-center text-sm text-custom-text-200">
<Row className="flex flex-start text-sm text-custom-text-200">
Layout
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
</div>
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={1} />
</Row>
}
customButtonClassName="flex flex-center text-custom-text-200 text-sm"
customButtonClassName="flex flex-start text-custom-text-200 text-sm"
closeOnSelect
>
{ISSUE_LAYOUTS.map((layout, index) => {
@@ -139,15 +139,15 @@ export const ProfileIssuesMobileHeader = observer(() => {
);
})}
</CustomMenu>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<div className="flex items-center justify-start border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
title="Filters"
placement="bottom-end"
menuButton={
<div className="flex flex-center text-sm text-custom-text-200">
<Row className="flex flex-start text-sm text-custom-text-200">
Filters
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
</div>
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={1} />
</Row>
}
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
@@ -165,15 +165,15 @@ export const ProfileIssuesMobileHeader = observer(() => {
/>
</FiltersDropdown>
</div>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<div className="flex items-center justify-start border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
title="Display"
placement="bottom-end"
menuButton={
<div className="flex flex-center text-sm text-custom-text-200">
<Row className="flex flex-start text-sm text-custom-text-200">
Display
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
</div>
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={1} />
</Row>
}
>
<DisplayFiltersSelection
@@ -291,7 +291,6 @@ export const CycleIssuesHeader: React.FC = observer(() => {
</Button>
{!isCompletedCycle && (
<Button
className="h-full self-start"
onClick={() => {
setTrackElement("Cycle issues page");
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
@@ -55,23 +55,25 @@ export const CyclesListHeader: FC = observer(() => {
/>
</Breadcrumbs>
</Header.LeftItem>
{canUserCreateCycle && currentProjectDetails ? (
<Header.RightItem>
<CyclesViewHeader projectId={currentProjectDetails.id} />
<Button
variant="primary"
size="sm"
onClick={() => {
setTrackElement("Cycles page");
toggleCreateCycleModal(true);
}}
>
<div className="hidden sm:block">Add</div> Cycle
</Button>
</Header.RightItem>
) : (
<></>
)}
<Header.RightItem>
{canUserCreateCycle && currentProjectDetails ? (
<div className="flex items-center gap-3">
<CyclesViewHeader projectId={currentProjectDetails.id} />
<Button
variant="primary"
size="sm"
onClick={() => {
setTrackElement("Cycles page");
toggleCreateCycleModal(true);
}}
>
<div className="hidden sm:block">Add</div> Cycle
</Button>
</div>
) : (
<></>
)}
</Header.RightItem>
</Header>
);
});
@@ -2,13 +2,16 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { PanelRight } from "lucide-react";
// ui
import { Breadcrumbs, LayersIcon, Header } from "@plane/ui";
// components
import { BreadcrumbLink, Logo } from "@/components/common";
import { IssueDetailQuickActions } from "@/components/issues";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useIssueDetail, useProject } from "@/hooks/store";
import { useAppTheme, useIssueDetail, useProject } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
export const ProjectIssueDetailsHeader = observer(() => {
@@ -17,11 +20,13 @@ export const ProjectIssueDetailsHeader = observer(() => {
const { workspaceSlug, projectId, issueId } = useParams();
// store hooks
const { currentProjectDetails, loader } = useProject();
const { issueDetailSidebarCollapsed, toggleIssueDetailSidebar } = useAppTheme();
const {
issue: { getIssueById },
} = useIssueDetail();
// derived values
const issueDetails = issueId ? getIssueById(issueId.toString()) : undefined;
const isSidebarCollapsed = issueDetailSidebarCollapsed;
return (
<Header>
@@ -77,6 +82,11 @@ export const ProjectIssueDetailsHeader = observer(() => {
projectId={projectId.toString()}
issueId={issueId.toString()}
/>
<button className="block md:hidden" onClick={() => toggleIssueDetailSidebar()}>
<PanelRight
className={cn("h-4 w-4 ", !isSidebarCollapsed ? "text-custom-primary-100" : " text-custom-text-200")}
/>
</button>
</Header.RightItem>
</Header>
);
@@ -8,7 +8,7 @@ import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
import { CustomMenu } from "@plane/ui";
import { CustomMenu, Row } from "@plane/ui";
// components
import { ProjectAnalyticsModal } from "@/components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts";
@@ -107,10 +107,10 @@ export const ProjectIssuesMobileHeader = observer(() => {
className="flex flex-grow justify-center text-sm text-custom-text-200"
placement="bottom-start"
customButton={
<div className="flex flex-start text-sm text-custom-text-200">
<Row className="flex flex-start text-sm text-custom-text-200">
Layout
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
</div>
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={1} />
</Row>
}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
@@ -17,7 +17,7 @@ export const ModulesListMobileHeader = observer(() => {
className="flex flex-grow justify-start text-custom-text-200 text-sm py-2 border-b border-custom-border-200 bg-custom-sidebar-background-100"
// placement="bottom-start"
customButton={
<Row className="flex flex-grow justify-center text-custom-text-200 text-sm gap-2">
<Row className="flex flex-grow justify-start text-custom-text-200 text-sm gap-2">
<span>Layout</span> <ChevronDown className="h-4 w-4 text-custom-text-200 my-auto" strokeWidth={1} />
</Row>
}
@@ -57,25 +57,27 @@ export const PagesListHeader = observer(() => {
</Breadcrumbs>
</div>
</Header.LeftItem>
{canUserCreatePage ? (
<Header.RightItem>
<Button
variant="primary"
size="sm"
onClick={() => {
setTrackElement("Project pages page");
toggleCreatePageModal({
isOpen: true,
pageAccess: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC,
});
}}
>
Add page
</Button>
</Header.RightItem>
) : (
<></>
)}
<Header.RightItem>
{canUserCreatePage ? (
<div className="flex items-center gap-2">
<Button
variant="primary"
size="sm"
onClick={() => {
setTrackElement("Project pages page");
toggleCreatePageModal({
isOpen: true,
pageAccess: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC,
});
}}
>
Add page
</Button>
</div>
) : (
<></>
)}
</Header.RightItem>
</Header>
);
});
@@ -45,9 +45,9 @@ const AutomationSettingsPage = observer(() => {
return (
<>
<PageHead title={pageTitle} />
<section className={`w-full overflow-y-auto ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
<div className="flex flex-col items-start border-b border-custom-border-100 pb-3.5">
<h3 className="text-xl font-medium leading-normal">Automations</h3>
<section className={`w-full overflow-y-auto py-8 pr-9 ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
<div className="flex items-center border-b border-custom-border-100 py-3.5">
<h3 className="text-xl font-medium">Automations</h3>
</div>
<AutoArchiveAutomation handleChange={handleChange} />
<AutoCloseAutomation handleChange={handleChange} />
@@ -30,7 +30,7 @@ const EstimatesSettingsPage = observer(() => {
<>
<PageHead title={pageTitle} />
<div
className={`w-full overflow-y-auto ${canPerformProjectAdminActions ? "" : "pointer-events-none opacity-60"}`}
className={`w-full overflow-y-auto py-8 pr-9 ${canPerformProjectAdminActions ? "" : "pointer-events-none opacity-60"}`}
>
<EstimateRoot
workspaceSlug={workspaceSlug?.toString()}
@@ -29,7 +29,7 @@ const FeaturesSettingsPage = observer(() => {
return (
<>
<PageHead title={pageTitle} />
<section className={`w-full overflow-y-auto ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
<section className={`w-full overflow-y-auto py-8 pr-9 ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
<ProjectFeaturesList
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
@@ -42,7 +42,7 @@ const LabelsSettingsPage = observer(() => {
return (
<>
<PageHead title={pageTitle} />
<div ref={scrollableContainerRef} className="h-full w-full gap-10 overflow-y-auto">
<div ref={scrollableContainerRef} className="h-full w-full gap-10 overflow-y-auto py-2 pr-9">
<ProjectSettingsLabelList />
</div>
</>
@@ -2,7 +2,7 @@
import { FC, ReactNode } from "react";
// components
import { AppHeader } from "@/components/core";
import { AppHeader, ContentWrapper } from "@/components/core";
// local components
import { ProjectSettingHeader } from "./header";
import { ProjectSettingsSidebar } from "./sidebar";
@@ -16,16 +16,16 @@ const ProjectSettingLayout: FC<IProjectSettingLayout> = (props) => {
return (
<>
<AppHeader header={<ProjectSettingHeader />} />
<div className="inset-y-0 flex flex-row vertical-scrollbar scrollbar-lg h-full w-full overflow-y-auto">
<div className="px-page-x !pr-0 py-page-y flex-shrink-0 overflow-y-hidden sm:hidden hidden md:block lg:block">
<ProjectSettingsSidebar />
</div>
<div className="flex flex-col relative w-full overflow-hidden">
<div className="w-full overflow-x-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-page-x md:px-9 py-page-y">
<ContentWrapper>
<div className="inset-y-0 z-20 flex flex-grow-0 h-full w-full">
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8 sm:hidden hidden md:block lg:block">
<ProjectSettingsSidebar />
</div>
<div className="w-full pl-10 sm:pl-10 md:pl-3 lg:pl-3 overflow-y-scroll vertical-scrollbar scrollbar-md">
{children}
</div>
</div>
</div>
</ContentWrapper>
</>
);
};
@@ -25,7 +25,7 @@ const MembersSettingsPage = observer(() => {
return (
<>
<PageHead title={pageTitle} />
<section className={`w-full overflow-y-auto`}>
<section className={`w-full overflow-y-auto py-8 pr-9`}>
<ProjectSettingsMemberDefaults />
<ProjectMemberList />
</section>
@@ -57,7 +57,7 @@ const GeneralSettingsPage = observer(() => {
</>
)}
<div className={`w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
<div className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
{currentProjectDetails && workspaceSlug && projectId && !isLoading ? (
<ProjectDetailsForm
project={currentProjectDetails}
@@ -27,7 +27,7 @@ export const ProjectSettingsSidebar = observer(() => {
if (!currentProjectRole) {
return (
<div className="flex w-[280px] flex-col gap-6">
<div className="flex w-80 flex-col gap-6 px-5">
<div className="flex flex-col gap-2">
<span className="text-xs font-semibold text-custom-sidebar-text-400">SETTINGS</span>
<Loader className="flex w-full flex-col gap-2">
@@ -41,7 +41,7 @@ export const ProjectSettingsSidebar = observer(() => {
}
return (
<div className="flex w-[280px] flex-col gap-6">
<div className="flex w-80 flex-col gap-6 px-5">
<div className="flex flex-col gap-2">
<span className="text-xs font-semibold text-custom-sidebar-text-400">SETTINGS</span>
<div className="flex w-full flex-col gap-1">
@@ -27,12 +27,14 @@ const StatesSettingsPage = observer(() => {
return (
<>
<PageHead title={pageTitle} />
<div className="flex items-center border-b border-custom-border-100">
<h3 className="text-xl font-medium">States</h3>
<div className="py-8 pr-9">
<div className="flex items-center border-b border-custom-border-100 py-3.5">
<h3 className="text-xl font-medium">States</h3>
</div>
{workspaceSlug && projectId && (
<ProjectStateRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
)}
</div>
{workspaceSlug && projectId && (
<ProjectStateRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
)}
</>
);
});
@@ -21,7 +21,7 @@ export const ViewMobileHeader = observer(() => {
return (
<>
<div className="md:hidden flex justify-evenly border-b border-custom-border-200 py-2 z-[13] bg-custom-background-100">
<Row className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<Row className="flex items-center justify-start border-l border-custom-border-200 text-sm text-custom-text-200">
<ViewOrderByDropdown
sortBy={filters.sortBy}
sortKey={filters.sortKey}
@@ -32,7 +32,7 @@ export const ViewMobileHeader = observer(() => {
isMobile
/>
</Row>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<div className="flex flex-grow items-center justify-start border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
icon={<ListFilter className="h-3 w-3" />}
title="Filters"
@@ -41,7 +41,7 @@ export const ViewMobileHeader = observer(() => {
menuButton={
<Row className="flex items-center text-sm text-custom-text-200">
Filters
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={1} />
</Row>
}
>
@@ -57,7 +57,7 @@ const ApiTokensPage = observer(() => {
<section className="w-full overflow-y-auto">
{tokens.length > 0 ? (
<>
<div className="flex items-center justify-between border-b border-custom-border-200 pb-3.5">
<div className="flex items-center justify-between border-b border-custom-border-200 py-3.5">
<h3 className="text-xl font-medium">API tokens</h3>
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
Add API token
@@ -2,6 +2,7 @@
import { ReactNode } from "react";
// components
import { ContentWrapper } from "@plane/ui";
import { AppHeader } from "@/components/core";
// local components
import { WorkspaceSettingHeader } from "./header";
@@ -19,16 +20,14 @@ export default function WorkspaceSettingLayout(props: IWorkspaceSettingLayout) {
<>
<AppHeader header={<WorkspaceSettingHeader />} />
<MobileWorkspaceSettingsTabs />
<div className="inset-y-0 flex flex-row vertical-scrollbar scrollbar-lg h-full w-full overflow-y-auto">
<div className="px-page-x !pr-0 py-page-y flex-shrink-0 overflow-y-hidden sm:hidden hidden md:block lg:block">
<ContentWrapper className="flex-row inset-y-0 gap-4">
<div className="w-80 flex-shrink-0 overflow-y-hidden sm:hidden hidden md:block lg:block">
<WorkspaceSettingsSidebar />
</div>
<div className="flex flex-col relative w-full overflow-hidden">
<div className="w-full h-full overflow-x-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-page-x md:px-9 py-page-y">
{children}
</div>
<div className="w-full overflow-x-hidden overflow-y-scroll vertical-scrollbar scrollbar-md">{children}</div>
</div>
</div>
</ContentWrapper>
</>
);
}
@@ -103,7 +103,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
"opacity-60": !canPerformWorkspaceMemberActions,
})}
>
<div className="flex justify-between gap-4 pb-3.5 items-start ">
<div className="flex items-center justify-between gap-4 pb-3.5">
<h4 className="text-xl font-medium">Members</h4>
<div className="ml-auto flex items-center gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5">
<Search className="h-3.5 w-3.5 text-custom-text-400" />
@@ -1,4 +1,3 @@
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
@@ -10,7 +9,7 @@ import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace";
// plane web helpers
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
export const MobileWorkspaceSettingsTabs = observer(() => {
export const MobileWorkspaceSettingsTabs = () => {
const router = useAppRouter();
const { workspaceSlug } = useParams();
const pathname = usePathname();
@@ -29,11 +28,10 @@ export const MobileWorkspaceSettingsTabs = observer(() => {
shouldRenderSettingLink(item.key) &&
workspaceMemberInfo >= item.access && (
<div
className={`${
item.highlight(pathname, `/${workspaceSlug}`)
? "text-custom-primary-100 text-sm py-2 px-3 whitespace-nowrap flex flex-grow cursor-pointer justify-around border-b border-custom-primary-200"
: "text-custom-text-200 flex flex-grow cursor-pointer justify-around border-b border-custom-border-200 text-sm py-2 px-3 whitespace-nowrap"
}`}
className={`${item.highlight(pathname, `/${workspaceSlug}`)
? "text-custom-primary-100 text-sm py-2 px-3 whitespace-nowrap flex flex-grow cursor-pointer justify-around border-b border-custom-primary-200"
: "text-custom-text-200 flex flex-grow cursor-pointer justify-around border-b border-custom-border-200 text-sm py-2 px-3 whitespace-nowrap"
}`}
key={index}
onClick={() => router.push(`/${workspaceSlug}${item.href}`)}
>
@@ -43,4 +41,4 @@ export const MobileWorkspaceSettingsTabs = observer(() => {
)}
</div>
);
});
};
@@ -28,7 +28,7 @@ export const WorkspaceSettingsSidebar = observer(() => {
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
return (
<div className="flex w-[280px] flex-col gap-6">
<div className="flex w-80 flex-col gap-6">
<div className="flex flex-col gap-2">
<span className="text-xs font-semibold text-custom-sidebar-text-400">SETTINGS</span>
<div className="flex w-full flex-col gap-1">
@@ -90,8 +90,8 @@ const WebhookDetailsPage = observer(() => {
<>
<PageHead title={pageTitle} />
<DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} />
<div className="w-full space-y-8 overflow-y-auto">
<div className="">
<div className="w-full space-y-8 overflow-y-auto md:py-8 py-4 md:pr-9 pr-4">
<div className="-m-5">
<WebhookForm onSubmit={async (data) => await handleUpdateWebhook(data)} data={currentWebhook} />
</div>
{currentWebhook && <WebhookDeleteSection openDeleteModal={() => setDeleteWebhookModal(true)} />}
@@ -100,4 +100,4 @@ const WebhookDetailsPage = observer(() => {
);
});
export default WebhookDetailsPage;
export default WebhookDetailsPage;
@@ -63,7 +63,7 @@ const WebhooksListPage = observer(() => {
/>
{Object.keys(webhooks).length > 0 ? (
<div className="flex h-full w-full flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 py-3.5">
<div className="text-xl font-medium">Webhooks</div>
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
Add webhook
@@ -39,6 +39,7 @@ export const AppSidebar: FC<IAppSidebar> = observer(() => {
useEffect(() => {
if (windowSize[0] < 768) !sidebarCollapsed && toggleSidebar();
else sidebarCollapsed && toggleSidebar();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [windowSize]);
+2 -9
View File
@@ -8,7 +8,7 @@ import "@/styles/react-day-picker.css";
// meta data info
import { SITE_NAME, SITE_DESCRIPTION } from "@/constants/meta";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper";
import { API_BASE_URL } from "@/helpers/common.helper";
// local
import { AppProvider } from "./provider";
@@ -75,14 +75,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<body>
<div id="context-menu-portal" />
<AppProvider>
<div
className={cn(
"h-screen w-full overflow-hidden bg-custom-background-100 relative flex flex-col",
"app-container"
)}
>
<div className="w-full h-full overflow-hidden relative">{children}</div>
</div>
<div className={`h-screen w-full overflow-hidden bg-custom-background-100`}>{children}</div>
</AppProvider>
</body>
{process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && (
@@ -81,7 +81,7 @@ export const WorkspaceActiveCyclesUpgrade = observer(() => {
<div className="grid h-full grid-cols-1 gap-5 pb-8 lg:grid-cols-2 xl:grid-cols-3">
{WORKSPACE_ACTIVE_CYCLES_DETAILS.map((item) => (
<div key={item.title} className="flex min-h-32 w-full flex-col gap-2 rounded-md bg-custom-background-80 p-4">
<div className="flex items-center gap-2 justify-between">
<div className="flex items-center gap-2">
<h3 className="font-medium">{item.title}</h3>
<item.icon className="h-4 w-4 text-blue-500" />
</div>
@@ -1,25 +1,11 @@
"use client";
import { FC } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { IProject } from "@plane/types";
// ui
import { CustomSelect } from "@plane/ui";
// components
import { MemberDropdown } from "@/components/dropdowns";
// constants
import { NETWORK_CHOICES } from "@/constants/project";
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getTabIndex } from "@/helpers/tab-indices.helper";
type Props = {
isMobile?: boolean;
};
const ProjectAttributes: FC<Props> = (props) => {
const { isMobile = false } = props;
const ProjectAttributes = () => {
const { control } = useFormContext<IProject>();
const { getIndex } = getTabIndex(ETabIndices.PROJECT_CREATE, isMobile);
return (
<div className="flex flex-wrap items-center gap-2">
<Controller
@@ -29,7 +15,7 @@ const ProjectAttributes: FC<Props> = (props) => {
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === value);
return (
<div className="flex-shrink-0 h-7" tabIndex={getIndex("network")}>
<div className="flex-shrink-0 h-7" tabIndex={4}>
<CustomSelect
value={value}
onChange={onChange}
@@ -49,7 +35,7 @@ const ProjectAttributes: FC<Props> = (props) => {
className="h-full"
buttonClassName="h-full"
noChevron
tabIndex={getIndex("network")}
tabIndex={4}
>
{NETWORK_CHOICES.map((network) => (
<CustomSelect.Option key={network.key} value={network.key}>
@@ -73,7 +59,7 @@ const ProjectAttributes: FC<Props> = (props) => {
render={({ field: { value, onChange } }) => {
if (value === undefined || value === null || typeof value === "string")
return (
<div className="flex-shrink-0 h-7" tabIndex={getIndex("lead")}>
<div className="flex-shrink-0 h-7" tabIndex={5}>
<MemberDropdown
value={value}
onChange={(lead) => onChange(lead === value ? null : lead)}
+2 -2
View File
@@ -120,7 +120,7 @@ export const CreateProjectForm: FC<Props> = observer((props) => {
return (
<FormProvider {...methods}>
<ProjectCreateHeader handleClose={handleClose} isMobile={isMobile} />
<ProjectCreateHeader handleClose={handleClose} />
<form onSubmit={handleSubmit(onSubmit)} className="px-3">
<div className="mt-9 space-y-6 pb-5">
@@ -130,7 +130,7 @@ export const CreateProjectForm: FC<Props> = observer((props) => {
isChangeInIdentifierRequired={isChangeInIdentifierRequired}
setIsChangeInIdentifierRequired={setIsChangeInIdentifierRequired}
/>
<ProjectAttributes isMobile={isMobile} />
<ProjectAttributes />
</div>
<ProjectCreateButtons handleClose={handleClose} />
</form>
+1 -1
View File
@@ -10,7 +10,7 @@ export const BillingRoot = () => (
<h3 className="text-xl font-medium">Billing and Plans</h3>
</div>
</div>
<div className="py-6">
<div className="px-4 py-6">
<div>
<h4 className="text-md mb-1 leading-6">Current plan</h4>
<p className="mb-3 text-sm text-custom-text-200">You are currently using the free plan</p>
@@ -1,6 +1,6 @@
"use client";
import { FC, FormEvent, useMemo, useRef, useState } from "react";
import { FC, FormEvent, useMemo, useState } from "react";
import { observer } from "mobx-react";
// icons
import { CircleAlert, XCircle } from "lucide-react";
@@ -22,6 +22,7 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
// states
const [isSubmitting, setIsSubmitting] = useState(false);
const [email, setEmail] = useState(defaultEmail);
const [isFocused, setFocused] = useState(false);
const emailError = useMemo(
() => (email && !checkEmailValidity(email) ? { email: "Email is invalid" } : undefined),
@@ -40,9 +41,6 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
const isButtonDisabled = email.length === 0 || Boolean(emailError?.email) || isSubmitting;
const [isFocused, setIsFocused] = useState(true)
const inputRef = useRef<HTMLInputElement>(null);
return (
<form onSubmit={handleFormSubmit} className="mt-5 space-y-4">
<div className="space-y-1">
@@ -54,9 +52,6 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
`relative flex items-center rounded-md bg-onboarding-background-200 border`,
!isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-onboarding-border-100`
)}
tabIndex={-1}
onFocus={() => {setIsFocused(true)}}
onBlur={() => {setIsFocused(false)}}
>
<Input
id="email"
@@ -66,17 +61,15 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
onChange={(e) => setEmail(e.target.value)}
placeholder="name@example.com"
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
autoComplete="on"
autoFocus
ref={inputRef}
/>
{email.length > 0 && (
{email.length > 0 && (
<XCircle
className="h-[46px] w-11 px-3 stroke-custom-text-400 hover:cursor-pointer text-xs"
onClick={() => {
setEmail("");
inputRef.current?.focus();
}}
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setEmail("")}
/>
)}
</div>
@@ -92,4 +85,4 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
</Button>
</form>
);
});
});
@@ -26,7 +26,7 @@ export const ApiTokenListItem: React.FC<Props> = (props) => {
return (
<>
<DeleteApiTokenModal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)} tokenId={token.id} />
<div className="group relative flex flex-col justify-center border-b border-custom-border-200 py-3">
<div className="group relative flex flex-col justify-center border-b border-custom-border-200 px-4 py-3">
<Tooltip tooltipContent="Delete token" isMobile={isMobile}>
<button
onClick={() => setDeleteModalOpen(true)}
@@ -41,7 +41,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
handleClose={() => setmonthModal(false)}
handleChange={handleChange}
/>
<div className="flex flex-col gap-4 border-b border-custom-border-100 py-6">
<div className="flex flex-col gap-4 border-b border-custom-border-100 px-4 py-6">
<div className="flex items-center justify-between">
<div className="flex items-start gap-3">
<div className="flex items-center justify-center rounded bg-custom-background-90 p-3">
@@ -68,7 +68,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
{currentProjectDetails ? (
currentProjectDetails.archive_in !== 0 && (
<div className="mx-6">
<div className="ml-12">
<div className="flex w-full items-center justify-between gap-2 rounded border border-custom-border-200 bg-custom-background-90 px-5 py-4">
<div className="w-1/2 text-sm font-medium">Auto-archive issues that are closed for</div>
<div className="w-1/2">
@@ -104,7 +104,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
</div>
)
) : (
<Loader className="mx-6">
<Loader className="ml-12">
<Loader.Item height="50px" />
</Loader>
)}
@@ -68,7 +68,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
handleClose={() => setmonthModal(false)}
handleChange={handleChange}
/>
<div className="flex flex-col gap-4 border-b border-custom-border-200 py-6">
<div className="flex flex-col gap-4 border-b border-custom-border-200 px-4 py-6">
<div className="flex items-center justify-between">
<div className="flex items-start gap-3">
<div className="flex items-center justify-center rounded bg-custom-background-90 p-3">
@@ -95,7 +95,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
{currentProjectDetails ? (
currentProjectDetails.close_in !== 0 && (
<div className="mx-6">
<div className="ml-12">
<div className="flex flex-col rounded border border-custom-border-200 bg-custom-background-90">
<div className="flex w-full items-center justify-between gap-2 px-5 py-4">
<div className="w-1/2 text-sm font-medium">Auto-close issues that are inactive for</div>
@@ -155,7 +155,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
)}
{selectedOption?.name
? selectedOption.name
: (currentDefaultState?.name ?? <span className="text-custom-text-200">State</span>)}
: currentDefaultState?.name ?? <span className="text-custom-text-200">State</span>}
</div>
}
onChange={(val: string) => {
@@ -171,7 +171,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
</div>
)
) : (
<Loader className="mx-6">
<Loader className="ml-12">
<Loader.Item height="50px" />
</Loader>
)}
@@ -28,8 +28,6 @@ import { EmptyState } from "@/components/empty-state";
import { EmptyStateType } from "@/constants/empty-state";
// fetch-keys
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
// helpers
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
@@ -82,8 +80,6 @@ export const CommandModal: React.FC = observer(() => {
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const { baseTabIndex } = getTabIndex(undefined, isMobile);
// TODO: update this to mobx store
const { data: issueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null,
@@ -242,7 +238,7 @@ export const CommandModal: React.FC = observer(() => {
value={searchTerm}
onValueChange={(e) => setSearchTerm(e)}
autoFocus
tabIndex={baseTabIndex}
tabIndex={1}
/>
</div>
+1 -1
View File
@@ -15,7 +15,7 @@ export const AppHeader = (props: AppHeaderProps) => {
return (
<div className="z-[15]">
<Row className="h-[3.75rem] z-10 flex gap-2 w-full items-center border-b border-custom-border-200 bg-custom-sidebar-background-100">
<Row className="h-[3.75rem] z-10 flex gap-2 w-full items-center border-b border-custom-border-200">
<div className="block bg-custom-sidebar-background-100 md:hidden">
<SidebarHamburgerToggle />
</div>
@@ -344,7 +344,7 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
</div>
)}
<input {...getInputProps()} />
<input {...getInputProps()} type="text" />
</div>
</div>
{fileRejections.length > 0 && (
@@ -7,8 +7,6 @@ import { Combobox, Dialog, Transition } from "@headlessui/react";
import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
// ui
import { Button, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import useDebounce from "@/hooks/use-debounce";
import { usePlatformOS } from "@/hooks/use-platform-os";
@@ -53,7 +51,6 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
const { isMobile } = usePlatformOS();
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
const { baseTabIndex } = getTabIndex(undefined, isMobile);
const handleClose = () => {
onClose();
@@ -143,7 +140,6 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
placeholder="Type to search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
tabIndex={baseTabIndex}
/>
</div>
+1
View File
@@ -1,6 +1,7 @@
export * from "./bulk-delete-issues-modal";
export * from "./existing-issues-list-modal";
export * from "./gpt-assistant-popover";
export * from "./link-modal";
export * from "./user-image-upload-modal";
export * from "./workspace-image-upload-modal";
export * from "./issue-search-modal-empty-state";
@@ -0,0 +1,175 @@
"use client";
import { FC, useEffect, Fragment } from "react";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
import type { IIssueLink, ILinkDetails, ModuleLink } from "@plane/types";
// ui
import { Button, Input } from "@plane/ui";
// types
type Props = {
isOpen: boolean;
handleClose: () => void;
data?: ILinkDetails | null;
status: boolean;
createIssueLink: (formData: IIssueLink | ModuleLink) => Promise<ILinkDetails> | Promise<void> | void;
updateIssueLink: (formData: IIssueLink | ModuleLink, linkId: string) => Promise<ILinkDetails> | Promise<void> | void;
};
const defaultValues: IIssueLink | ModuleLink = {
title: "",
url: "",
};
export const LinkModal: FC<Props> = (props) => {
const { isOpen, handleClose, createIssueLink, updateIssueLink, status, data } = props;
// form info
const {
formState: { errors, isSubmitting },
handleSubmit,
control,
reset,
} = useForm<IIssueLink | ModuleLink>({
defaultValues,
});
const onClose = () => {
handleClose();
const timeout = setTimeout(() => {
reset(defaultValues);
clearTimeout(timeout);
}, 500);
};
const handleFormSubmit = async (formData: IIssueLink | ModuleLink) => {
if (!data) await createIssueLink({ title: formData.title, url: formData.url });
else await updateIssueLink({ title: formData.title, url: formData.url }, data.id);
onClose();
};
const handleCreateUpdatePage = async (formData: IIssueLink | ModuleLink) => {
await handleFormSubmit(formData);
reset({
...defaultValues,
});
};
useEffect(() => {
reset({
...defaultValues,
...data,
});
}, [data, reset]);
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form onSubmit={handleSubmit(handleCreateUpdatePage)}>
<div>
<div className="space-y-5">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
{status ? "Update Link" : "Add Link"}
</Dialog.Title>
<div className="mt-2 space-y-3">
<div>
<label htmlFor="url" className="mb-2 text-custom-text-200">
URL
</label>
<Controller
control={control}
name="url"
rules={{
required: "URL is required",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="url"
name="url"
type="url"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.url)}
placeholder="https://..."
pattern="^(https?://).*"
className="w-full"
/>
)}
/>
</div>
<div>
<label htmlFor="title" className="mb-2 text-custom-text-200">
{`Title (optional)`}
</label>
<Controller
control={control}
name="title"
render={({ field: { value, onChange, ref } }) => (
<Input
id="title"
name="title"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.title)}
placeholder="Enter title"
className="w-full"
/>
)}
/>
</div>
</div>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<Button variant="neutral-primary" size="sm" onClick={onClose}>
Cancel
</Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
{status
? isSubmitting
? "Updating link..."
: "Update link"
: isSubmitting
? "Adding link..."
: "Add link"}
</Button>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};
@@ -144,7 +144,7 @@ export const UserImageUploadModal: React.FC<Props> = observer((props) => {
</div>
)}
<input {...getInputProps()} />
<input {...getInputProps()} type="text" />
</div>
</div>
{fileRejections.length > 0 && (
@@ -150,7 +150,7 @@ export const WorkspaceImageUploadModal: React.FC<Props> = observer((props) => {
</div>
)}
<input {...getInputProps()}/>
<input {...getInputProps()} type="text" />
</div>
</div>
{fileRejections.length > 0 && (
@@ -1,2 +1,3 @@
export * from "./links-list";
export * from "./single-progress-stats";
export * from "./sidebar-menu-hamburger-toggle";
@@ -0,0 +1,118 @@
"use client";
import { observer } from "mobx-react";
// icons
import { Pencil, Trash2, LinkIcon, ExternalLink } from "lucide-react";
import { ILinkDetails, UserAuth } from "@plane/types";
// ui
import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { calculateTimeAgo } from "@/helpers/date-time.helper";
// hooks
import { useMember, useModule } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// types
type Props = {
moduleId: string;
handleDeleteLink: (linkId: string) => void;
handleEditLink: (link: ILinkDetails) => void;
userAuth: UserAuth;
disabled?: boolean;
};
export const LinksList: React.FC<Props> = observer((props) => {
const { moduleId, handleDeleteLink, handleEditLink, userAuth, disabled } = props;
// hooks
const { getUserDetails } = useMember();
const { isMobile } = usePlatformOS();
const { getModuleById } = useModule();
// derived values
const currentModule = getModuleById(moduleId);
const moduleLinks = currentModule?.link_module || undefined;
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Copied to clipboard",
message: "The URL has been successfully copied to your clipboard",
});
};
if (!moduleLinks) return <></>;
return (
<>
{moduleLinks.map((link) => {
const createdByDetails = getUserDetails(link.created_by);
return (
<div key={link.id} className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
<div className="flex w-full items-start justify-between gap-2">
<div className="flex items-start gap-2 truncate">
<span className="py-1">
<LinkIcon className="h-3 w-3 flex-shrink-0" />
</span>
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url} isMobile={isMobile}>
<span
className="cursor-pointer truncate text-xs"
onClick={() => copyToClipboard(link.title && link.title !== "" ? link.title : link.url)}
>
{link.title && link.title !== "" ? link.title : link.url}
</span>
</Tooltip>
</div>
{!isNotAllowed && (
<div className="z-[1] flex flex-shrink-0 items-center gap-2">
<button
type="button"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleEditLink(link);
}}
>
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
</button>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
>
<ExternalLink className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
</a>
<button
type="button"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleDeleteLink(link.id);
}}
>
<Trash2 className="h-3 w-3" />
</button>
</div>
)}
</div>
<div className="px-5">
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
Added {calculateTimeAgo(link.created_at)}
<br />
{createdByDetails && (
<>
by{" "}
{createdByDetails?.is_bot ? createdByDetails?.first_name + " Bot" : createdByDetails?.display_name}
</>
)}
</p>
</div>
</div>
);
})}
</>
);
});
@@ -4,7 +4,6 @@ import { Search, X } from "lucide-react";
import { TCycleFilters, TCycleGroups } from "@plane/types";
// components
import { FilterEndDate, FilterStartDate, FilterStatus } from "@/components/cycles";
import { usePlatformOS } from "@/hooks/use-platform-os";
// types
type Props = {
@@ -17,8 +16,6 @@ export const CycleFiltersSelection: React.FC<Props> = observer((props) => {
const { filters, handleFiltersUpdate, isArchived = false } = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
// hooks
const { isMobile } = usePlatformOS();
return (
<div className="flex h-full w-full flex-col overflow-hidden">
@@ -31,7 +28,7 @@ export const CycleFiltersSelection: React.FC<Props> = observer((props) => {
placeholder="Search"
value={filtersSearchQuery}
onChange={(e) => setFiltersSearchQuery(e.target.value)}
autoFocus={!isMobile}
autoFocus
/>
{filtersSearchQuery !== "" && (
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>
+7 -13
View File
@@ -8,12 +8,9 @@ import { ICycle } from "@plane/types";
import { Button, Input, TextArea } from "@plane/ui";
// components
import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { shouldRenderProject } from "@/helpers/project.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
type Props = {
handleFormSubmit: (values: Partial<ICycle>, dirtyFields: any) => Promise<void>;
@@ -22,7 +19,6 @@ type Props = {
projectId: string;
setActiveProject: (projectId: string) => void;
data?: ICycle | null;
isMobile?: boolean;
};
const defaultValues: Partial<ICycle> = {
@@ -33,7 +29,7 @@ const defaultValues: Partial<ICycle> = {
};
export const CycleForm: React.FC<Props> = (props) => {
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data, isMobile = false } = props;
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props;
// form data
const {
formState: { errors, isSubmitting, dirtyFields },
@@ -50,8 +46,6 @@ export const CycleForm: React.FC<Props> = (props) => {
},
});
const { getIndex } = getTabIndex(ETabIndices.PROJECT_CYCLE, isMobile);
useEffect(() => {
reset({
...defaultValues,
@@ -77,7 +71,7 @@ export const CycleForm: React.FC<Props> = (props) => {
}}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={getIndex("cover_image")}
tabIndex={7}
/>
</div>
)}
@@ -107,7 +101,7 @@ export const CycleForm: React.FC<Props> = (props) => {
inputSize="md"
onChange={onChange}
hasError={Boolean(errors?.name)}
tabIndex={getIndex("description")}
tabIndex={1}
autoFocus
/>
)}
@@ -126,7 +120,7 @@ export const CycleForm: React.FC<Props> = (props) => {
hasError={Boolean(errors?.description)}
value={value}
onChange={onChange}
tabIndex={getIndex("description")}
tabIndex={2}
/>
)}
/>
@@ -159,7 +153,7 @@ export const CycleForm: React.FC<Props> = (props) => {
hideIcon={{
to: true,
}}
tabIndex={getIndex("date_range")}
tabIndex={3}
/>
)}
/>
@@ -169,10 +163,10 @@ export const CycleForm: React.FC<Props> = (props) => {
</div>
</div>
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={getIndex("cancel")}>
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={4}>
Cancel
</Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={getIndex("submit")}>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={5}>
{data ? (isSubmitting ? "Updating" : "Update Cycle") : isSubmitting ? "Creating" : "Create Cycle"}
</Button>
</div>
+1 -1
View File
@@ -19,7 +19,7 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
const { completedCycleIds, upcomingCycleIds, cycleIds, workspaceSlug, projectId, isArchived = false } = props;
return (
<ContentWrapper variant={ERowVariant.HUGGING} className="flex-row">
<ContentWrapper variant={ERowVariant.HUGGING}>
<ListLayout>
{isArchived ? (
<>
-3
View File
@@ -13,7 +13,6 @@ import { CYCLE_CREATED, CYCLE_UPDATED } from "@/constants/event-tracker";
// hooks
import { useEventTracker, useCycle, useProject } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os";
// services
import { CycleService } from "@/services/cycle.service";
@@ -36,7 +35,6 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
const { captureCycleEvent } = useEventTracker();
const { workspaceProjectIds } = useProject();
const { createCycle, updateCycleDetails } = useCycle();
const { isMobile } = usePlatformOS();
const { setValue: setCycleTab } = useLocalStorage<TCycleTabOptions>("cycle_tab", "active");
@@ -188,7 +186,6 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
data={data}
isMobile={isMobile}
/>
</ModalCore>
);

Some files were not shown because too many files have changed in this diff Show More