mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
14 Commits
feat/flat-
...
feat/pages
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17c539658b | ||
|
|
6f190ea6ee | ||
|
|
4ccdd29b9e | ||
|
|
25868bf07d | ||
|
|
f277aa02b6 | ||
|
|
58bf056ddb | ||
|
|
692f570258 | ||
|
|
11cd9f57d7 | ||
|
|
3a66986785 | ||
|
|
d8ab3e0087 | ||
|
|
16fc90930a | ||
|
|
7dada8a7a0 | ||
|
|
a9b45ed2ad | ||
|
|
c009a45887 |
12
admin/Dockerfile.dev
Normal file
12
admin/Dockerfile.dev
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM node:18-alpine
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
COPY . .
|
||||
RUN yarn global add turbo
|
||||
RUN yarn install
|
||||
EXPOSE 3000
|
||||
VOLUME [ "/app/node_modules", "/app/admin/node_modules" ]
|
||||
CMD ["yarn", "dev", "--filter=admin"]
|
||||
@@ -15,7 +15,7 @@ interface RootLayoutProps {
|
||||
}
|
||||
|
||||
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => {
|
||||
const prefix = parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0") === 0 ? "/" : "/god-mode/";
|
||||
const prefix = "/god-mode/";
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
// lib
|
||||
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
|
||||
// helpers
|
||||
import { EAuthenticationPageType, EInstancePageType } from "@/helpers";
|
||||
|
||||
interface LoginLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const LoginLayout = ({ children }: LoginLayoutProps) => (
|
||||
<InstanceWrapper pageType={EInstancePageType.POST_SETUP}>
|
||||
<AuthWrapper authType={EAuthenticationPageType.NOT_AUTHENTICATED}>{children}</AuthWrapper>
|
||||
</InstanceWrapper>
|
||||
);
|
||||
|
||||
export default LoginLayout;
|
||||
@@ -1,18 +0,0 @@
|
||||
"use client";
|
||||
|
||||
// layouts
|
||||
import { DefaultLayout } from "@/layouts";
|
||||
// components
|
||||
import { PageHeader } from "@/components/core";
|
||||
import { InstanceSignInForm } from "./components";
|
||||
|
||||
const LoginPage = () => (
|
||||
<>
|
||||
<PageHeader title="Setup - God Mode" />
|
||||
<DefaultLayout>
|
||||
<InstanceSignInForm />
|
||||
</DefaultLayout>
|
||||
</>
|
||||
);
|
||||
|
||||
export default LoginPage;
|
||||
@@ -1,20 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
// layouts
|
||||
import { DefaultLayout } from "@/layouts";
|
||||
// components
|
||||
import { PageHeader } from "@/components/core";
|
||||
import { InstanceSignInForm } from "@/components/login";
|
||||
// lib
|
||||
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
|
||||
// helpers
|
||||
import { EAuthenticationPageType, EInstancePageType } from "@/helpers";
|
||||
|
||||
const RootPage = () => {
|
||||
const router = useRouter();
|
||||
const LoginPage = () => (
|
||||
<>
|
||||
<PageHeader title="Login - God Mode" />
|
||||
<InstanceWrapper pageType={EInstancePageType.POST_SETUP}>
|
||||
<AuthWrapper authType={EAuthenticationPageType.NOT_AUTHENTICATED}>
|
||||
<DefaultLayout>
|
||||
<InstanceSignInForm />
|
||||
</DefaultLayout>
|
||||
</AuthWrapper>
|
||||
</InstanceWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
useEffect(() => router.push("/login"), [router]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Plane - God Mode" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootPage;
|
||||
export default LoginPage;
|
||||
|
||||
@@ -36,7 +36,7 @@ export const HelpSection: FC = () => {
|
||||
// refs
|
||||
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `${process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? `/god-mode/` : `/`}`}`;
|
||||
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `/god-mode/`}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -19,7 +19,7 @@ export const NewUserPopup: React.FC = observer(() => {
|
||||
// theme
|
||||
const { resolvedTheme } = nextUseTheme();
|
||||
|
||||
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `${process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? `/god-mode/` : `/`}`}`;
|
||||
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `/god-mode/`}`;
|
||||
|
||||
if (!isNewUserPopup) return <></>;
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ReactElement, createContext } from "react";
|
||||
// mobx store
|
||||
import { RootStore } from "@/store/root-store";
|
||||
|
||||
let rootStore = new RootStore();
|
||||
export let rootStore = new RootStore();
|
||||
|
||||
export const StoreContext = createContext<RootStore>(rootStore);
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { FC, ReactNode } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// hooks
|
||||
import { useInstance, useUser } from "@/hooks";
|
||||
// helpers
|
||||
import { EAuthenticationPageType, EUserStatus } from "@/helpers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { EAuthenticationPageType } from "@/helpers";
|
||||
|
||||
export interface IAuthWrapper {
|
||||
children: ReactNode;
|
||||
@@ -16,41 +16,41 @@ export interface IAuthWrapper {
|
||||
}
|
||||
|
||||
export const AuthWrapper: FC<IAuthWrapper> = observer((props) => {
|
||||
const router = useRouter();
|
||||
// props
|
||||
const { children, authType = EAuthenticationPageType.AUTHENTICATED } = props;
|
||||
// hooks
|
||||
const { instance, fetchInstanceAdmins } = useInstance();
|
||||
const { isLoading, userStatus, currentUser, fetchCurrentUser } = useUser();
|
||||
const { instance } = useInstance();
|
||||
const { isLoading, currentUser, fetchCurrentUser } = useUser();
|
||||
|
||||
useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins(), {
|
||||
const { isLoading: isSWRLoading } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
|
||||
if (isLoading)
|
||||
if (isSWRLoading || isLoading)
|
||||
return (
|
||||
<div className="relative flex h-screen w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (userStatus && userStatus?.status === EUserStatus.ERROR)
|
||||
return (
|
||||
<div className="relative flex h-screen w-screen items-center justify-center">
|
||||
Something went wrong. please try again later
|
||||
</div>
|
||||
);
|
||||
if (authType === EAuthenticationPageType.NOT_AUTHENTICATED) {
|
||||
if (currentUser === undefined) return <>{children}</>;
|
||||
else {
|
||||
router.push("/general/");
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
if ([EAuthenticationPageType.AUTHENTICATED, EAuthenticationPageType.NOT_AUTHENTICATED].includes(authType)) {
|
||||
if (authType === EAuthenticationPageType.NOT_AUTHENTICATED) {
|
||||
if (currentUser === undefined) return <>{children}</>;
|
||||
else redirect("/general/");
|
||||
} else {
|
||||
if (currentUser) return <>{children}</>;
|
||||
else {
|
||||
if (instance?.instance?.is_setup_done) redirect("/login/");
|
||||
else redirect("/setup/");
|
||||
if (authType === EAuthenticationPageType.AUTHENTICATED) {
|
||||
if (currentUser) return <>{children}</>;
|
||||
else {
|
||||
if (instance && instance?.instance?.is_setup_done) {
|
||||
router.push("/");
|
||||
return <></>;
|
||||
} else {
|
||||
router.push("/setup/");
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { InstanceNotReady } from "@/components/instance";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks";
|
||||
// helpers
|
||||
import { EInstancePageType, EInstanceStatus } from "@/helpers";
|
||||
import { EInstancePageType } from "@/helpers";
|
||||
|
||||
type TInstanceWrapper = {
|
||||
children: ReactNode;
|
||||
@@ -24,26 +24,19 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
|
||||
const searchparams = useSearchParams();
|
||||
const authEnabled = searchparams.get("auth_enabled") || "1";
|
||||
// hooks
|
||||
const { isLoading, instanceStatus, instance, fetchInstanceInfo } = useInstance();
|
||||
const { isLoading, instance, fetchInstanceInfo } = useInstance();
|
||||
|
||||
useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
|
||||
const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
if (isLoading)
|
||||
if (isSWRLoading || isLoading)
|
||||
return (
|
||||
<div className="relative flex h-screen w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (instanceStatus && instanceStatus?.status === EInstanceStatus.ERROR)
|
||||
return (
|
||||
<div className="relative flex h-screen w-screen items-center justify-center">
|
||||
Something went wrong. please try again later
|
||||
</div>
|
||||
);
|
||||
|
||||
if (instance?.instance?.is_setup_done === false && authEnabled === "1")
|
||||
return (
|
||||
<DefaultLayout withoutBackground>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
// store
|
||||
import { rootStore } from "@/lib/store-context";
|
||||
|
||||
export abstract class APIService {
|
||||
protected baseURL: string;
|
||||
@@ -18,7 +20,8 @@ export abstract class APIService {
|
||||
this.axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response && error.response.status === 401) window.location.href = "/login";
|
||||
const store = rootStore;
|
||||
if (error.response && error.response.status === 401 && store.user.currentUser) store.user.reset();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -18,8 +18,10 @@ export class RootStore {
|
||||
}
|
||||
|
||||
resetOnSignOut() {
|
||||
this.theme = new ThemeStore(this);
|
||||
localStorage.setItem("theme", "system");
|
||||
|
||||
this.instance = new InstanceStore(this);
|
||||
this.user = new UserStore(this);
|
||||
this.theme = new ThemeStore(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ export interface IUserStore {
|
||||
currentUser: IUser | undefined;
|
||||
// fetch actions
|
||||
fetchCurrentUser: () => Promise<IUser>;
|
||||
signOut: () => Promise<void>;
|
||||
reset: () => void;
|
||||
signOut: () => void;
|
||||
}
|
||||
|
||||
export class UserStore implements IUserStore {
|
||||
@@ -28,8 +29,6 @@ export class UserStore implements IUserStore {
|
||||
// services
|
||||
userService;
|
||||
authService;
|
||||
// rootStore
|
||||
rootStore;
|
||||
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
@@ -40,10 +39,11 @@ export class UserStore implements IUserStore {
|
||||
currentUser: observable,
|
||||
// action
|
||||
fetchCurrentUser: action,
|
||||
reset: action,
|
||||
signOut: action,
|
||||
});
|
||||
this.userService = new UserService();
|
||||
this.authService = new AuthService();
|
||||
this.rootStore = store;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,11 +54,20 @@ export class UserStore implements IUserStore {
|
||||
try {
|
||||
if (this.currentUser === undefined) this.isLoading = true;
|
||||
const currentUser = await this.userService.currentUser();
|
||||
runInAction(() => {
|
||||
this.isUserLoggedIn = true;
|
||||
this.currentUser = currentUser;
|
||||
this.isLoading = false;
|
||||
});
|
||||
if (currentUser) {
|
||||
await this.store.instance.fetchInstanceAdmins();
|
||||
runInAction(() => {
|
||||
this.isUserLoggedIn = true;
|
||||
this.currentUser = currentUser;
|
||||
this.isLoading = false;
|
||||
});
|
||||
} else {
|
||||
runInAction(() => {
|
||||
this.isUserLoggedIn = false;
|
||||
this.currentUser = undefined;
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
return currentUser;
|
||||
} catch (error: any) {
|
||||
this.isLoading = false;
|
||||
@@ -77,7 +86,14 @@ export class UserStore implements IUserStore {
|
||||
}
|
||||
};
|
||||
|
||||
reset = async () => {
|
||||
this.isUserLoggedIn = false;
|
||||
this.currentUser = undefined;
|
||||
this.isLoading = false;
|
||||
this.userStatus = undefined;
|
||||
};
|
||||
|
||||
signOut = async () => {
|
||||
this.rootStore.resetOnSignOut();
|
||||
this.store.resetOnSignOut();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Backend
|
||||
# Debug value for api server use it as 0 for production use
|
||||
DEBUG=0
|
||||
CORS_ALLOWED_ORIGINS=""
|
||||
CORS_ALLOWED_ORIGINS="http://localhost"
|
||||
|
||||
# Error logs
|
||||
SENTRY_DSN=""
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# Python imports
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import zoneinfo
|
||||
|
||||
|
||||
@@ -105,9 +105,13 @@ class PageSerializer(BaseSerializer):
|
||||
|
||||
class PageDetailSerializer(PageSerializer):
|
||||
description_html = serializers.CharField()
|
||||
description_yjs = serializers.CharField()
|
||||
|
||||
class Meta(PageSerializer.Meta):
|
||||
fields = PageSerializer.Meta.fields + ["description_html"]
|
||||
fields = PageSerializer.Meta.fields + [
|
||||
"description_html",
|
||||
"description_yjs",
|
||||
]
|
||||
|
||||
|
||||
class SubPageSerializer(BaseSerializer):
|
||||
|
||||
@@ -6,6 +6,7 @@ from plane.app.views import (
|
||||
PageFavoriteViewSet,
|
||||
PageLogEndpoint,
|
||||
SubPagesEndpoint,
|
||||
PagesDescriptionViewSet,
|
||||
)
|
||||
|
||||
|
||||
@@ -79,4 +80,14 @@ urlpatterns = [
|
||||
SubPagesEndpoint.as_view(),
|
||||
name="sub-page",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/description/",
|
||||
PagesDescriptionViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
}
|
||||
),
|
||||
name="page-description",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -177,6 +177,7 @@ from .page.base import (
|
||||
PageFavoriteViewSet,
|
||||
PageLogEndpoint,
|
||||
SubPagesEndpoint,
|
||||
PagesDescriptionViewSet,
|
||||
)
|
||||
|
||||
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Python imports
|
||||
import json
|
||||
import y_py as Y
|
||||
import base64
|
||||
from datetime import datetime
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
@@ -8,6 +10,7 @@ from django.db import connection
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.http import StreamingHttpResponse
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -387,3 +390,63 @@ class SubPagesEndpoint(BaseAPIView):
|
||||
return Response(
|
||||
SubPageSerializer(pages, many=True).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
|
||||
class PagesDescriptionViewSet(BaseViewSet):
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
binary_data = page.description_yjs
|
||||
|
||||
def stream_data():
|
||||
if binary_data:
|
||||
yield binary_data
|
||||
else:
|
||||
yield b""
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
stream_data(), content_type="application/octet-stream"
|
||||
)
|
||||
response["Content-Disposition"] = (
|
||||
'attachment; filename="page_description.bin"'
|
||||
)
|
||||
return response
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
base64_data = request.data.get("description_yjs")
|
||||
|
||||
if base64_data:
|
||||
# Decode the base64 data to bytes
|
||||
new_binary_data = base64.b64decode(base64_data)
|
||||
|
||||
# Load the existing data into a YDoc
|
||||
existing_doc = Y.YDoc()
|
||||
if page.description_yjs:
|
||||
Y.apply_update(existing_doc, page.description_yjs)
|
||||
|
||||
# # Load the new data into a separate YDoc
|
||||
# new_doc = Y.YDoc()
|
||||
Y.apply_update(existing_doc, new_binary_data)
|
||||
|
||||
# # Merge the new data into the existing data
|
||||
# # This will automatically resolve any conflicts
|
||||
# new_state_vector = Y.encode_state_vector(new_doc)
|
||||
# diff = Y.encode_state_as_update(existing_doc, new_state_vector)
|
||||
# Y.apply_update(existing_doc, diff)
|
||||
|
||||
# # Encode the updated state as binary data
|
||||
updated_binary_data = Y.encode_state_as_update(existing_doc)
|
||||
|
||||
# Store the updated binary data
|
||||
page.description_yjs = updated_binary_data
|
||||
page.description_html = request.data.get("description_html")
|
||||
page.save()
|
||||
return Response({"message": "Updated successfully"})
|
||||
else:
|
||||
return Response({"error": "No binary data provided"})
|
||||
|
||||
@@ -602,11 +602,19 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
@cache_response(60 * 60 * 24, user=False)
|
||||
def get(self, request):
|
||||
files = []
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
)
|
||||
if settings.USE_MINIO:
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
)
|
||||
else:
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
)
|
||||
params = {
|
||||
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
||||
"Prefix": "static/project-cover/",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Django imports
|
||||
from django.db.models import Case, Count, IntegerField, Q, When
|
||||
from django.contrib.auth import logout
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -26,6 +28,7 @@ from plane.db.models import (
|
||||
from plane.license.models import Instance, InstanceAdmin
|
||||
from plane.utils.cache import cache_response, invalidate_cache
|
||||
from plane.utils.paginator import BasePaginator
|
||||
from plane.authentication.utils.host import user_ip
|
||||
|
||||
|
||||
class UserEndpoint(BaseViewSet):
|
||||
@@ -166,7 +169,14 @@ class UserEndpoint(BaseViewSet):
|
||||
"workspace_invite": False,
|
||||
}
|
||||
profile.save()
|
||||
|
||||
# User log out
|
||||
user.last_logout_ip = user_ip(request=request)
|
||||
user.last_logout_time = timezone.now()
|
||||
user.save()
|
||||
|
||||
# Logout the user
|
||||
logout(request)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
||||
@@ -7,12 +7,6 @@ def auth_exception_handler(exc, context):
|
||||
response = exception_handler(exc, context)
|
||||
# Check if an AuthenticationFailed exception is raised.
|
||||
if isinstance(exc, NotAuthenticated):
|
||||
# Return 403 if the users me api fails
|
||||
request = context["request"]
|
||||
if request.path == "/api/users/me/":
|
||||
response.status_code = 403
|
||||
# else return 401
|
||||
else:
|
||||
response.status_code = 401
|
||||
response.status_code = 401
|
||||
|
||||
return response
|
||||
|
||||
@@ -46,9 +46,7 @@ class GitHubOAuthProvider(OauthAdapter):
|
||||
client_id = GITHUB_CLIENT_ID
|
||||
client_secret = GITHUB_CLIENT_SECRET
|
||||
|
||||
redirect_uri = (
|
||||
f"{request.scheme}://{request.get_host()}/auth/github/callback/"
|
||||
)
|
||||
redirect_uri = f"""{"https" if request.is_secure() else "http"}://{request.get_host()}/auth/github/callback/"""
|
||||
url_params = {
|
||||
"client_id": client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
|
||||
@@ -43,9 +43,7 @@ class GoogleOAuthProvider(OauthAdapter):
|
||||
client_id = GOOGLE_CLIENT_ID
|
||||
client_secret = GOOGLE_CLIENT_SECRET
|
||||
|
||||
redirect_uri = (
|
||||
f"{request.scheme}://{request.get_host()}/auth/google/callback/"
|
||||
)
|
||||
redirect_uri = f"""{"https" if request.is_secure() else "http"}://{request.get_host()}/auth/google/callback/"""
|
||||
url_params = {
|
||||
"client_id": client_id,
|
||||
"scope": self.scope,
|
||||
|
||||
@@ -6,7 +6,7 @@ def base_host(request):
|
||||
return (
|
||||
request.META.get("HTTP_ORIGIN")
|
||||
or f"{urlsplit(request.META.get('HTTP_REFERER')).scheme}://{urlsplit(request.META.get('HTTP_REFERER')).netloc}"
|
||||
or f"{request.scheme}://{request.get_host()}"
|
||||
or f"""{"https" if request.is_secure() else "http"}://{request.get_host()}"""
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,10 +10,13 @@ def get_redirection_path(user):
|
||||
return "onboarding"
|
||||
|
||||
# Redirect to the last workspace if the user has last workspace
|
||||
if profile.last_workspace_id and Workspace.objects.filter(
|
||||
pk=profile.last_workspace_id,
|
||||
workspace_member__member_id=user.id,
|
||||
workspace_member__is_active=True,
|
||||
if (
|
||||
profile.last_workspace_id
|
||||
and Workspace.objects.filter(
|
||||
pk=profile.last_workspace_id,
|
||||
workspace_member__member_id=user.id,
|
||||
workspace_member__is_active=True,
|
||||
).exists()
|
||||
):
|
||||
workspace = Workspace.objects.filter(
|
||||
pk=profile.last_workspace_id,
|
||||
|
||||
@@ -2,7 +2,6 @@ import uuid
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django import
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import uuid
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django import
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ class ResetPasswordEndpoint(View):
|
||||
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode({"success", True}),
|
||||
"accounts/sign-in?" + urlencode({"success": True}),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
except DjangoUnicodeDecodeError:
|
||||
|
||||
@@ -3,7 +3,6 @@ import uuid
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django import
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django imports
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
18
apiserver/plane/db/migrations/0065_page_description_yjs.py
Normal file
18
apiserver/plane/db/migrations/0065_page_description_yjs.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.11 on 2024-05-08 09:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0064_auto_20240409_1134'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='page',
|
||||
name='description_yjs',
|
||||
field=models.BinaryField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -18,6 +18,7 @@ def get_view_props():
|
||||
class Page(ProjectBaseModel):
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.JSONField(default=dict, blank=True)
|
||||
description_yjs = models.BinaryField(null=True)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
description_stripped = models.TextField(blank=True, null=True)
|
||||
owned_by = models.ForeignKey(
|
||||
|
||||
@@ -31,6 +31,8 @@ MEDIA_URL = "/uploads/"
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "uploads") # noqa
|
||||
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
"http://localhost",
|
||||
"http://127.0.0.1",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://localhost:4000",
|
||||
|
||||
@@ -60,4 +60,6 @@ zxcvbn==4.4.28
|
||||
# timezone
|
||||
pytz==2024.1
|
||||
# jwt
|
||||
jwt==1.3.1
|
||||
PyJWT==2.8.0
|
||||
# real-time
|
||||
y-py==0.6.2
|
||||
@@ -73,6 +73,20 @@ services:
|
||||
- worker
|
||||
- web
|
||||
|
||||
admin:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./admin/Dockerfile.dev
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./admin:/app/admin
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
- web
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ./apiserver
|
||||
@@ -167,3 +181,4 @@ services:
|
||||
- web
|
||||
- api
|
||||
- space
|
||||
- admin
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
export dollar="$"
|
||||
export http_upgrade="http_upgrade"
|
||||
export scheme="scheme"
|
||||
envsubst < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
|
||||
exec nginx -g 'daemon off;'
|
||||
|
||||
@@ -15,6 +15,8 @@ http {
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Permissions-Policy "interest-cohort=()" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Forwarded-Proto "${dollar}scheme";
|
||||
add_header Host "${dollar}host";
|
||||
|
||||
location / {
|
||||
proxy_pass http://web:3000/;
|
||||
@@ -23,8 +25,8 @@ http {
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
location /god-mode {
|
||||
proxy_pass http://godmode:3000/;
|
||||
location /god-mode/ {
|
||||
proxy_pass http://admin:3000/god-mode/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
|
||||
@@ -15,6 +15,8 @@ http {
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Permissions-Policy "interest-cohort=()" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Forwarded-Proto "${dollar}scheme";
|
||||
add_header Host "${dollar}host";
|
||||
|
||||
location / {
|
||||
proxy_pass http://web:3000/;
|
||||
|
||||
@@ -12,18 +12,17 @@ import { insertContentAtSavedSelection } from "src/helpers/insert-content-at-cur
|
||||
import { EditorMenuItemNames, getEditorMenuItems } from "src/ui/menus/menu-items";
|
||||
import { EditorRefApi } from "src/types/editor-ref-api";
|
||||
import { IMarking, scrollSummary } from "src/helpers/scroll-to-node";
|
||||
|
||||
interface CustomEditorProps {
|
||||
export interface CustomEditorProps {
|
||||
id?: string;
|
||||
uploadFile: UploadImage;
|
||||
restoreFile: RestoreImage;
|
||||
deleteFile: DeleteImage;
|
||||
cancelUploadImage?: () => void;
|
||||
initialValue: string;
|
||||
initialValue?: string;
|
||||
editorClassName: string;
|
||||
// undefined when prop is not passed, null if intentionally passed to stop
|
||||
// swr syncing
|
||||
value: string | null | undefined;
|
||||
value?: string | null | undefined;
|
||||
onChange?: (json: object, html: string) => void;
|
||||
extensions?: any;
|
||||
editorProps?: EditorProps;
|
||||
|
||||
@@ -24,6 +24,7 @@ export * from "src/ui/menus/menu-items";
|
||||
export * from "src/lib/editor-commands";
|
||||
|
||||
// types
|
||||
export type { CustomEditorProps } from "src/hooks/use-editor";
|
||||
export type { DeleteImage } from "src/types/delete-image";
|
||||
export type { UploadImage } from "src/types/upload-image";
|
||||
export type { EditorRefApi, EditorReadOnlyRefApi } from "src/types/editor-ref-api";
|
||||
|
||||
@@ -34,12 +34,17 @@
|
||||
"@plane/ui": "*",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@tiptap/core": "^2.1.13",
|
||||
"@tiptap/extension-collaboration": "^2.3.2",
|
||||
"@tiptap/pm": "^2.1.13",
|
||||
"@tiptap/suggestion": "^2.1.13",
|
||||
"lucide-react": "^0.378.0",
|
||||
"react-popper": "^2.3.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^9.0.1",
|
||||
"y-indexeddb": "^9.0.12",
|
||||
"y-prosemirror": "^1.2.5",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yjs": "^13.6.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.15.3",
|
||||
|
||||
101
packages/editor/document-editor/src/hooks/use-document-editor.ts
Normal file
101
packages/editor/document-editor/src/hooks/use-document-editor.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useLayoutEffect, useMemo } from "react";
|
||||
import {
|
||||
DeleteImage,
|
||||
EditorRefApi,
|
||||
IMentionHighlight,
|
||||
IMentionSuggestion,
|
||||
RestoreImage,
|
||||
UploadImage,
|
||||
useEditor,
|
||||
} from "@plane/editor-core";
|
||||
import * as Y from "yjs";
|
||||
import { CollaborationProvider } from "src/providers/collaboration-provider";
|
||||
import { DocumentEditorExtensions } from "src/ui/extensions";
|
||||
import { IndexeddbPersistence } from "y-indexeddb";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
|
||||
type DocumentEditorProps = {
|
||||
id?: string;
|
||||
uploadFile: UploadImage;
|
||||
restoreFile: RestoreImage;
|
||||
deleteFile: DeleteImage;
|
||||
cancelUploadImage?: () => void;
|
||||
value: Uint8Array;
|
||||
editorClassName: string;
|
||||
onChange: (binaryString: string, html: string) => void;
|
||||
extensions?: any;
|
||||
editorProps?: EditorProps;
|
||||
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||
mentionHandler: {
|
||||
highlights: () => Promise<IMentionHighlight[]>;
|
||||
suggestions?: () => Promise<IMentionSuggestion[]>;
|
||||
};
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
setHideDragHandleFunction: (hideDragHandlerFromDragDrop: () => void) => void;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export const useDocumentEditor = ({
|
||||
uploadFile,
|
||||
id = "",
|
||||
deleteFile,
|
||||
cancelUploadImage,
|
||||
editorProps = {},
|
||||
value,
|
||||
editorClassName,
|
||||
onChange,
|
||||
forwardedRef,
|
||||
tabIndex,
|
||||
restoreFile,
|
||||
handleEditorReady,
|
||||
mentionHandler,
|
||||
placeholder,
|
||||
setHideDragHandleFunction,
|
||||
}: DocumentEditorProps) => {
|
||||
const provider = useMemo(
|
||||
() =>
|
||||
new CollaborationProvider({
|
||||
name: id,
|
||||
onChange,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[id]
|
||||
);
|
||||
|
||||
const yDoc = useMemo(() => {
|
||||
if (value.byteLength !== 0) Y.applyUpdate(provider.document, value);
|
||||
return provider.document;
|
||||
}, [value, provider.document]);
|
||||
console.log("yDoc", yDoc);
|
||||
|
||||
// indexedDB provider
|
||||
useLayoutEffect(() => {
|
||||
const localProvider = new IndexeddbPersistence(id, provider.document);
|
||||
return () => {
|
||||
localProvider?.destroy();
|
||||
};
|
||||
}, [provider, id]);
|
||||
|
||||
const editor = useEditor({
|
||||
id,
|
||||
editorProps,
|
||||
editorClassName,
|
||||
restoreFile,
|
||||
uploadFile,
|
||||
deleteFile,
|
||||
cancelUploadImage,
|
||||
handleEditorReady,
|
||||
forwardedRef,
|
||||
mentionHandler,
|
||||
extensions: DocumentEditorExtensions({
|
||||
uploadFile,
|
||||
setHideDragHandle: setHideDragHandleFunction,
|
||||
provider,
|
||||
}),
|
||||
placeholder,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
return editor;
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import * as Y from "yjs";
|
||||
|
||||
export interface CompleteCollaboratorProviderConfiguration {
|
||||
/**
|
||||
* The identifier/name of your document
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The actual Y.js document
|
||||
*/
|
||||
document: Y.Doc;
|
||||
/**
|
||||
* onChange callback
|
||||
*/
|
||||
onChange: (binaryString: string, html: string) => void;
|
||||
}
|
||||
|
||||
export type CollaborationProviderConfiguration = Required<Pick<CompleteCollaboratorProviderConfiguration, "name">> &
|
||||
Partial<CompleteCollaboratorProviderConfiguration>;
|
||||
|
||||
export class CollaborationProvider {
|
||||
public configuration: CompleteCollaboratorProviderConfiguration = {
|
||||
name: "",
|
||||
// @ts-expect-error cannot be undefined
|
||||
document: undefined,
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
intervals: any = {
|
||||
forceSync: null,
|
||||
};
|
||||
|
||||
timeoutId: any;
|
||||
|
||||
constructor(configuration: CollaborationProviderConfiguration) {
|
||||
this.setConfiguration(configuration);
|
||||
|
||||
this.timeoutId = null;
|
||||
|
||||
this.configuration.document = configuration.document ?? new Y.Doc();
|
||||
this.document.on("update", this.documentUpdateHandler.bind(this));
|
||||
}
|
||||
|
||||
public setConfiguration(configuration: Partial<CompleteCollaboratorProviderConfiguration> = {}): void {
|
||||
this.configuration = {
|
||||
...this.configuration,
|
||||
...configuration,
|
||||
};
|
||||
}
|
||||
|
||||
get document() {
|
||||
return this.configuration.document;
|
||||
}
|
||||
|
||||
documentUpdateHandler(update: Uint8Array, origin: any) {
|
||||
if (origin === this) return;
|
||||
|
||||
// debounce onChange call
|
||||
if (this.timeoutId !== null) clearTimeout(this.timeoutId);
|
||||
|
||||
this.timeoutId = setTimeout(() => {
|
||||
const docAsUint8Array = Y.encodeStateAsUpdate(this.document);
|
||||
const base64Doc = Buffer.from(docAsUint8Array).toString("base64");
|
||||
// const base64Doc = Buffer.from(update).toString("base64");
|
||||
|
||||
this.configuration.onChange?.(base64Doc, "<p></p>");
|
||||
this.timeoutId = null;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,20 @@ import { IssueWidgetPlaceholder } from "src/ui/extensions/widgets/issue-embed-wi
|
||||
|
||||
import { SlashCommand, DragAndDrop } from "@plane/editor-extensions";
|
||||
import { UploadImage } from "@plane/editor-core";
|
||||
import { CollaborationProvider } from "src/providers/collaboration-provider";
|
||||
import Collaboration from "@tiptap/extension-collaboration";
|
||||
|
||||
type TArguments = {
|
||||
uploadFile: UploadImage;
|
||||
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
|
||||
provider: CollaborationProvider;
|
||||
};
|
||||
|
||||
export const DocumentEditorExtensions = ({ uploadFile, setHideDragHandle }: TArguments) => [
|
||||
export const DocumentEditorExtensions = ({ uploadFile, setHideDragHandle, provider }: TArguments) => [
|
||||
SlashCommand(uploadFile),
|
||||
DragAndDrop(setHideDragHandle),
|
||||
IssueWidgetPlaceholder(),
|
||||
Collaboration.configure({
|
||||
document: provider.document,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -4,17 +4,16 @@ import {
|
||||
DeleteImage,
|
||||
RestoreImage,
|
||||
getEditorClassNames,
|
||||
useEditor,
|
||||
EditorRefApi,
|
||||
IMentionHighlight,
|
||||
IMentionSuggestion,
|
||||
} from "@plane/editor-core";
|
||||
import { DocumentEditorExtensions } from "src/ui/extensions";
|
||||
import { PageRenderer } from "src/ui/components/page-renderer";
|
||||
import { useDocumentEditor } from "src/hooks/use-document-editor";
|
||||
|
||||
interface IDocumentEditor {
|
||||
initialValue: string;
|
||||
value?: string;
|
||||
id: string;
|
||||
value: Uint8Array;
|
||||
fileHandler: {
|
||||
cancel: () => void;
|
||||
delete: DeleteImage;
|
||||
@@ -24,7 +23,7 @@ interface IDocumentEditor {
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
containerClassName?: string;
|
||||
editorClassName?: string;
|
||||
onChange: (json: object, html: string) => void;
|
||||
onChange: (binaryString: string, html: string) => void;
|
||||
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||
mentionHandler: {
|
||||
highlights: () => Promise<IMentionHighlight[]>;
|
||||
@@ -37,8 +36,9 @@ interface IDocumentEditor {
|
||||
const DocumentEditor = (props: IDocumentEditor) => {
|
||||
const {
|
||||
onChange,
|
||||
initialValue,
|
||||
id,
|
||||
value,
|
||||
// value,
|
||||
fileHandler,
|
||||
containerClassName,
|
||||
editorClassName = "",
|
||||
@@ -56,26 +56,22 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
||||
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
|
||||
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
|
||||
};
|
||||
// use editor
|
||||
const editor = useEditor({
|
||||
onChange(json, html) {
|
||||
onChange(json, html);
|
||||
},
|
||||
|
||||
// use document editor
|
||||
const editor = useDocumentEditor({
|
||||
id,
|
||||
editorClassName,
|
||||
restoreFile: fileHandler.restore,
|
||||
uploadFile: fileHandler.upload,
|
||||
deleteFile: fileHandler.delete,
|
||||
cancelUploadImage: fileHandler.cancel,
|
||||
initialValue,
|
||||
value,
|
||||
onChange,
|
||||
handleEditorReady,
|
||||
forwardedRef,
|
||||
mentionHandler,
|
||||
extensions: DocumentEditorExtensions({
|
||||
uploadFile: fileHandler.upload,
|
||||
setHideDragHandle: setHideDragHandleFunction,
|
||||
}),
|
||||
placeholder,
|
||||
setHideDragHandleFunction,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
|
||||
2
setup.sh
2
setup.sh
@@ -7,6 +7,8 @@ export LC_CTYPE=C
|
||||
|
||||
cp ./web/.env.example ./web/.env
|
||||
cp ./apiserver/.env.example ./apiserver/.env
|
||||
cp ./space/.env.example ./space/.env
|
||||
cp ./admin/.env.example ./admin/.env
|
||||
|
||||
# Generate the SECRET_KEY that will be used by django
|
||||
echo "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
|
||||
2
space/.env.example
Normal file
2
space/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
NEXT_PUBLIC_APP_URL=
|
||||
NEXT_PUBLIC_API_BASE_URL=
|
||||
@@ -52,7 +52,7 @@ export const checkEmailValidity = (email: string): boolean => {
|
||||
|
||||
export const isEmptyHtmlString = (htmlString: string) => {
|
||||
// Remove HTML tags using regex
|
||||
const cleanText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: [] });
|
||||
const cleanText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: ["img"] });
|
||||
// Trim the string and check if it's empty
|
||||
return cleanText.trim() === "";
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import { IUserStore } from "@/store/user/index.store";
|
||||
import { IUserStore } from "@/store/user";
|
||||
|
||||
export const useUser = (): IUserStore => {
|
||||
const context = useContext(StoreContext);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ReactElement, createContext } from "react";
|
||||
// mobx store
|
||||
import { RootStore } from "@/store/root.store";
|
||||
|
||||
let rootStore = new RootStore();
|
||||
export let rootStore = new RootStore();
|
||||
|
||||
export const StoreContext = createContext<RootStore>(rootStore);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { StoreProvider } from "@/lib/store-context";
|
||||
// wrappers
|
||||
import { InstanceWrapper } from "@/lib/wrappers";
|
||||
|
||||
const prefix = parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0") === 0 ? "/" : "/spaces/";
|
||||
const prefix = "/spaces/";
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
// store
|
||||
import { rootStore } from "@/lib/store-context";
|
||||
|
||||
abstract class APIService {
|
||||
protected baseURL: string;
|
||||
@@ -19,7 +21,8 @@ abstract class APIService {
|
||||
this.axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response && error.response.status === 401) window.location.href = "/";
|
||||
const store = rootStore;
|
||||
if (error.response && error.response.status === 401 && store.user.data) store.user.reset();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { enableStaticRendering } from "mobx-react-lite";
|
||||
// store imports
|
||||
import { IInstanceStore, InstanceStore } from "@/store/instance.store";
|
||||
import { IProjectStore, ProjectStore } from "@/store/project";
|
||||
import { IUserStore, UserStore } from "@/store/user/index.store";
|
||||
import { IUserStore, UserStore } from "@/store/user";
|
||||
import { IProfileStore, ProfileStore } from "@/store/user/profile.store";
|
||||
|
||||
import IssueStore, { IIssueStore } from "./issue";
|
||||
|
||||
@@ -2,8 +2,6 @@ import set from "lodash/set";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// types
|
||||
import { IUser } from "@plane/types";
|
||||
// helpers
|
||||
// import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// services
|
||||
import { AuthService } from "@/services/authentication.service";
|
||||
import { UserService } from "@/services/user.service";
|
||||
@@ -30,6 +28,7 @@ export interface IUserStore {
|
||||
// actions
|
||||
fetchCurrentUser: () => Promise<IUser | undefined>;
|
||||
updateCurrentUser: (data: Partial<IUser>) => Promise<IUser | undefined>;
|
||||
reset: () => void;
|
||||
signOut: () => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -65,6 +64,7 @@ export class UserStore implements IUserStore {
|
||||
// actions
|
||||
fetchCurrentUser: action,
|
||||
updateCurrentUser: action,
|
||||
reset: action,
|
||||
signOut: action,
|
||||
});
|
||||
}
|
||||
@@ -153,6 +153,20 @@ export class UserStore implements IUserStore {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description resets the user store
|
||||
* @returns {void}
|
||||
*/
|
||||
reset = (): void => {
|
||||
runInAction(() => {
|
||||
this.isAuthenticated = false;
|
||||
this.isLoading = false;
|
||||
this.error = undefined;
|
||||
this.data = undefined;
|
||||
this.userProfile = new ProfileStore(this.store);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description signs out the current user
|
||||
* @returns {Promise<void>}
|
||||
@@ -88,7 +88,10 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
|
||||
if (authMode === EAuthModes.SIGN_IN) {
|
||||
if (response.is_password_autoset) setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
else setAuthStep(EAuthSteps.PASSWORD);
|
||||
} else setAuthStep(EAuthSteps.PASSWORD);
|
||||
} else {
|
||||
if (instance && instance?.config?.is_smtp_configured) setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
else setAuthStep(EAuthSteps.PASSWORD);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorhandler = authErrorHandler(error?.error_code.toString(), data?.email || undefined);
|
||||
|
||||
@@ -40,15 +40,22 @@ const authService = new AuthService();
|
||||
|
||||
export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
const { email, handleStepChange, handleEmailClear, mode } = props;
|
||||
// states
|
||||
const [passwordFormData, setPasswordFormData] = useState<TPasswordFormValues>({ ...defaultValues, email });
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
// hooks
|
||||
const { instance } = useInstance();
|
||||
const { captureEvent } = useEventTracker();
|
||||
// states
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
const [passwordFormData, setPasswordFormData] = useState<TPasswordFormValues>({ ...defaultValues, email });
|
||||
const [showPassword, setShowPassword] = useState({
|
||||
password: false,
|
||||
retypePassword: false,
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
||||
|
||||
const handleShowPassword = (key: keyof typeof showPassword) =>
|
||||
setShowPassword((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||
|
||||
// derived values
|
||||
const isSmtpConfigured = instance?.config?.is_smtp_configured;
|
||||
|
||||
@@ -116,9 +123,9 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
type="email"
|
||||
value={passwordFormData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
// hasError={Boolean(errors.email)}
|
||||
placeholder="name@company.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
|
||||
disabled
|
||||
/>
|
||||
{passwordFormData.email.length > 0 && (
|
||||
<XCircle
|
||||
@@ -127,6 +134,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<input type="hidden" value={passwordFormData.email} name="email" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
|
||||
@@ -134,7 +142,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
type={showPassword?.password ? "text" : "password"}
|
||||
name="password"
|
||||
value={passwordFormData.password}
|
||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||
@@ -144,15 +152,15 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
onBlur={() => setIsPasswordInputFocused(false)}
|
||||
autoFocus
|
||||
/>
|
||||
{showPassword ? (
|
||||
{showPassword?.password ? (
|
||||
<EyeOff
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(false)}
|
||||
onClick={() => handleShowPassword("password")}
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(true)}
|
||||
onClick={() => handleShowPassword("password")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -165,22 +173,22 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
type={showPassword?.retypePassword ? "text" : "password"}
|
||||
name="confirm_password"
|
||||
value={passwordFormData.confirm_password}
|
||||
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
|
||||
placeholder="Confirm password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
/>
|
||||
{showPassword ? (
|
||||
{showPassword?.retypePassword ? (
|
||||
<EyeOff
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(false)}
|
||||
onClick={() => handleShowPassword("retypePassword")}
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setShowPassword(true)}
|
||||
onClick={() => handleShowPassword("retypePassword")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -111,10 +111,9 @@ export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
type="email"
|
||||
value={uniqueCodeFormData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
// FIXME:
|
||||
// hasError={Boolean(errors.email)}
|
||||
placeholder="name@company.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
|
||||
disabled
|
||||
/>
|
||||
{uniqueCodeFormData.email.length > 0 && (
|
||||
<XCircle
|
||||
@@ -122,6 +121,7 @@ export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
onClick={handleEmailClear}
|
||||
/>
|
||||
)}
|
||||
<input type="hidden" value={uniqueCodeFormData.email} name="email" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import { mutate } from "swr";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
@@ -15,17 +13,14 @@ type Props = {
|
||||
};
|
||||
|
||||
export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
||||
const router = useRouter();
|
||||
const { isOpen, onClose } = props;
|
||||
// hooks
|
||||
const { deactivateAccount, signOut } = useUser();
|
||||
|
||||
// states
|
||||
const [isDeactivating, setIsDeactivating] = useState(false);
|
||||
|
||||
const { deactivateAccount } = useUser();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsDeactivating(false);
|
||||
onClose();
|
||||
@@ -41,8 +36,7 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
||||
title: "Success!",
|
||||
message: "Account deactivated successfully.",
|
||||
});
|
||||
mutate("CURRENT_USER_DETAILS", null);
|
||||
setTheme("system");
|
||||
signOut();
|
||||
router.push("/");
|
||||
handleClose();
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { FC } from "react";
|
||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||
import { observer } from "mobx-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { TGroupedIssues } from "@plane/types";
|
||||
// components
|
||||
@@ -51,67 +50,59 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
|
||||
const groupedIssueIds = (issues.groupedIssueIds ?? {}) as TGroupedIssues;
|
||||
|
||||
const onDragEnd = async (result: DropResult) => {
|
||||
if (!result) return;
|
||||
const handleDragAndDrop = async (
|
||||
issueId: string | undefined,
|
||||
sourceDate: string | undefined,
|
||||
destinationDate: string | undefined
|
||||
) => {
|
||||
if (!issueId || !destinationDate || !sourceDate) return;
|
||||
|
||||
// return if not dropped on the correct place
|
||||
if (!result.destination) return;
|
||||
|
||||
// return if dropped on the same date
|
||||
if (result.destination.droppableId === result.source.droppableId) return;
|
||||
|
||||
if (handleDragDrop) {
|
||||
await handleDragDrop(
|
||||
result.source,
|
||||
result.destination,
|
||||
workspaceSlug?.toString(),
|
||||
projectId?.toString(),
|
||||
issueMap,
|
||||
groupedIssueIds,
|
||||
updateIssue
|
||||
).catch((err) => {
|
||||
setToast({
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: err?.detail ?? "Failed to perform this action",
|
||||
});
|
||||
await handleDragDrop(
|
||||
issueId,
|
||||
sourceDate,
|
||||
destinationDate,
|
||||
workspaceSlug?.toString(),
|
||||
projectId?.toString(),
|
||||
updateIssue
|
||||
).catch((err) => {
|
||||
setToast({
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: err?.detail ?? "Failed to perform this action",
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full w-full overflow-hidden bg-custom-background-100 pt-4">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<CalendarChart
|
||||
issuesFilterStore={issuesFilter}
|
||||
issues={issueMap}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
layout={displayFilters?.calendar?.layout}
|
||||
showWeekends={displayFilters?.calendar?.show_weekends ?? false}
|
||||
quickActions={({ issue, parentRef, customActionButton, placement }) => (
|
||||
<QuickActions
|
||||
parentRef={parentRef}
|
||||
customActionButton={customActionButton}
|
||||
issue={issue}
|
||||
handleDelete={async () => removeIssue(issue.project_id, issue.id)}
|
||||
handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)}
|
||||
handleRemoveFromView={async () =>
|
||||
removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)
|
||||
}
|
||||
handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)}
|
||||
handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)}
|
||||
readOnly={!isEditingAllowed || isCompletedCycle}
|
||||
placements={placement}
|
||||
/>
|
||||
)}
|
||||
addIssuesToView={addIssuesToView}
|
||||
quickAddCallback={issues.quickAddIssue}
|
||||
viewId={viewId}
|
||||
readOnly={!isEditingAllowed || isCompletedCycle}
|
||||
updateFilters={updateFilters}
|
||||
/>
|
||||
</DragDropContext>
|
||||
<CalendarChart
|
||||
issuesFilterStore={issuesFilter}
|
||||
issues={issueMap}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
layout={displayFilters?.calendar?.layout}
|
||||
showWeekends={displayFilters?.calendar?.show_weekends ?? false}
|
||||
quickActions={({ issue, parentRef, customActionButton, placement }) => (
|
||||
<QuickActions
|
||||
parentRef={parentRef}
|
||||
customActionButton={customActionButton}
|
||||
issue={issue}
|
||||
handleDelete={async () => removeIssue(issue.project_id, issue.id)}
|
||||
handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)}
|
||||
handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)}
|
||||
handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)}
|
||||
handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)}
|
||||
readOnly={!isEditingAllowed || isCompletedCycle}
|
||||
placements={placement}
|
||||
/>
|
||||
)}
|
||||
addIssuesToView={addIssuesToView}
|
||||
quickAddCallback={issues.quickAddIssue}
|
||||
viewId={viewId}
|
||||
readOnly={!isEditingAllowed || isCompletedCycle}
|
||||
updateFilters={updateFilters}
|
||||
handleDragAndDrop={handleDragAndDrop}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// types
|
||||
import type {
|
||||
@@ -40,6 +42,11 @@ type Props = {
|
||||
layout: "month" | "week" | undefined;
|
||||
showWeekends: boolean;
|
||||
quickActions: TRenderQuickActions;
|
||||
handleDragAndDrop: (
|
||||
issueId: string | undefined,
|
||||
sourceDate: string | undefined,
|
||||
destinationDate: string | undefined
|
||||
) => Promise<void>;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@@ -63,6 +70,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
groupedIssueIds,
|
||||
layout,
|
||||
showWeekends,
|
||||
handleDragAndDrop,
|
||||
quickActions,
|
||||
quickAddCallback,
|
||||
addIssuesToView,
|
||||
@@ -72,6 +80,8 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
} = props;
|
||||
// states
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
||||
//refs
|
||||
const scrollableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
// store hooks
|
||||
const {
|
||||
issues: { viewFlags },
|
||||
@@ -91,6 +101,19 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
|
||||
const formattedDatePayload = renderFormattedPayloadDate(selectedDate) ?? undefined;
|
||||
|
||||
// Enable Auto Scroll for calendar
|
||||
useEffect(() => {
|
||||
const element = scrollableContainerRef.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
return combine(
|
||||
autoScrollForElements({
|
||||
element,
|
||||
})
|
||||
);
|
||||
}, [scrollableContainerRef?.current]);
|
||||
|
||||
if (!calendarPayload || !formattedDatePayload)
|
||||
return (
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
@@ -112,6 +135,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
className={cn("flex w-full flex-col overflow-y-auto md:h-full", {
|
||||
"vertical-scrollbar scrollbar-lg": windowWidth > 768,
|
||||
})}
|
||||
ref={scrollableContainerRef}
|
||||
>
|
||||
<CalendarWeekHeader isLoading={!issues} showWeekends={showWeekends} />
|
||||
<div className="h-full w-full">
|
||||
@@ -123,6 +147,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
selectedDate={selectedDate}
|
||||
setSelectedDate={setSelectedDate}
|
||||
issuesFilterStore={issuesFilterStore}
|
||||
handleDragAndDrop={handleDragAndDrop}
|
||||
key={weekIndex}
|
||||
week={week}
|
||||
issues={issues}
|
||||
@@ -143,6 +168,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
selectedDate={selectedDate}
|
||||
setSelectedDate={setSelectedDate}
|
||||
issuesFilterStore={issuesFilterStore}
|
||||
handleDragAndDrop={handleDragAndDrop}
|
||||
week={issueCalendarView.allDaysOfActiveWeek}
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
@@ -175,6 +201,8 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={viewId}
|
||||
readOnly={readOnly}
|
||||
isMonthLayout={false}
|
||||
showAllIssues
|
||||
isDragDisabled
|
||||
isMobileView
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Droppable } from "@hello-pangea/dnd";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// types
|
||||
import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types";
|
||||
// components
|
||||
import { CalendarIssueBlocks, ICalendarDate } from "@/components/issues";
|
||||
// constants
|
||||
import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils";
|
||||
// helpers
|
||||
import { MONTHS_LIST } from "@/constants/calendar";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
@@ -24,6 +27,11 @@ type Props = {
|
||||
quickActions: TRenderQuickActions;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
handleDragAndDrop: (
|
||||
issueId: string | undefined,
|
||||
sourceDate: string | undefined,
|
||||
destinationDate: string | undefined
|
||||
) => Promise<void>;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@@ -51,12 +59,46 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
viewId,
|
||||
readOnly = false,
|
||||
selectedDate,
|
||||
handleDragAndDrop,
|
||||
setSelectedDate,
|
||||
} = props;
|
||||
|
||||
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||
|
||||
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
|
||||
|
||||
const formattedDatePayload = renderFormattedPayloadDate(date.date);
|
||||
|
||||
const dayTileRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const element = dayTileRef.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
return combine(
|
||||
dropTargetForElements({
|
||||
element,
|
||||
getData: () => ({ date: formattedDatePayload }),
|
||||
onDragEnter: () => {
|
||||
setIsDraggingOver(true);
|
||||
},
|
||||
onDragLeave: () => {
|
||||
setIsDraggingOver(false);
|
||||
},
|
||||
onDrop: ({ source, self }) => {
|
||||
setIsDraggingOver(false);
|
||||
const sourceData = source?.data as { id: string; date: string } | undefined;
|
||||
const destinationData = self?.data as { date: string } | undefined;
|
||||
handleDragAndDrop(sourceData?.id, sourceData?.date, destinationData?.date);
|
||||
setShowAllIssues(true);
|
||||
highlightIssueOnDrop(source?.element?.id, false);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [dayTileRef?.current, formattedDatePayload]);
|
||||
|
||||
if (!formattedDatePayload) return null;
|
||||
const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null;
|
||||
|
||||
@@ -65,13 +107,19 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
const isToday = date.date.toDateString() === new Date().toDateString();
|
||||
const isSelectedDate = date.date.toDateString() == selectedDate.toDateString();
|
||||
|
||||
const isWeekend = date.date.getDay() === 0 || date.date.getDay() === 6;
|
||||
const isMonthLayout = calendarLayout === "month";
|
||||
|
||||
const normalBackground = isWeekend ? "bg-custom-background-90" : "bg-custom-background-100";
|
||||
const draggingOverBackground = isWeekend ? "bg-custom-background-80" : "bg-custom-background-90";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="group relative flex h-full w-full flex-col bg-custom-background-90">
|
||||
<div ref={dayTileRef} className="group relative flex h-full w-full flex-col bg-custom-background-90">
|
||||
{/* header */}
|
||||
<div
|
||||
className={`hidden flex-shrink-0 items-center justify-end px-2 py-1.5 text-right text-xs md:flex ${
|
||||
calendarLayout === "month" // if month layout, highlight current month days
|
||||
isMonthLayout // if month layout, highlight current month days
|
||||
? date.is_current_month
|
||||
? "font-medium"
|
||||
: "text-custom-text-300"
|
||||
@@ -93,44 +141,38 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
|
||||
{/* content */}
|
||||
<div className="hidden h-full w-full md:block">
|
||||
<Droppable droppableId={formattedDatePayload} isDropDisabled={readOnly}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`h-full w-full select-none ${
|
||||
snapshot.isDraggingOver || date.date.getDay() === 0 || date.date.getDay() === 6
|
||||
? "bg-custom-background-90"
|
||||
: "bg-custom-background-100"
|
||||
} ${calendarLayout === "month" ? "min-h-[5rem]" : ""}`}
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<CalendarIssueBlocks
|
||||
date={date.date}
|
||||
issues={issues}
|
||||
issueIdList={issueIdList}
|
||||
quickActions={quickActions}
|
||||
isDragDisabled={readOnly}
|
||||
addIssuesToView={addIssuesToView}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
<div className="h-full w-full hidden md:block">
|
||||
<div
|
||||
className={`h-full w-full select-none ${
|
||||
isDraggingOver ? `${draggingOverBackground} opacity-70` : normalBackground
|
||||
} ${isMonthLayout ? "min-h-[5rem]" : ""}`}
|
||||
>
|
||||
<CalendarIssueBlocks
|
||||
date={date.date}
|
||||
issues={issues}
|
||||
issueIdList={issueIdList}
|
||||
showAllIssues={showAllIssues}
|
||||
setShowAllIssues={setShowAllIssues}
|
||||
quickActions={quickActions}
|
||||
isDragDisabled={readOnly}
|
||||
addIssuesToView={addIssuesToView}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
readOnly={readOnly}
|
||||
isMonthLayout={isMonthLayout}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile view content */}
|
||||
<div
|
||||
onClick={() => setSelectedDate(date.date)}
|
||||
className={cn(
|
||||
"mx-auto flex h-full w-full cursor-pointer flex-col items-center justify-start py-2.5 text-sm font-medium md:hidden",
|
||||
"text-sm py-2.5 h-full w-full font-medium mx-auto flex flex-col justify-start items-center md:hidden cursor-pointer opacity-80",
|
||||
{
|
||||
"bg-custom-background-100": date.date.getDay() !== 0 && date.date.getDay() !== 6,
|
||||
"bg-custom-background-100": !isWeekend,
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,23 +1,54 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||
// components
|
||||
import { TIssueMap } from "@plane/types";
|
||||
import { CalendarIssueBlock } from "@/components/issues";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
import { HIGHLIGHT_CLASS } from "../utils";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
issues: TIssueMap | undefined;
|
||||
issueId: string;
|
||||
quickActions: TRenderQuickActions;
|
||||
isDragging?: boolean;
|
||||
isDragDisabled: boolean;
|
||||
};
|
||||
|
||||
export const CalendarIssueBlockRoot: React.FC<Props> = (props) => {
|
||||
const { issues, issueId, quickActions, isDragging } = props;
|
||||
const { issues, issueId, quickActions, isDragDisabled } = props;
|
||||
|
||||
if (!issues?.[issueId]) return null;
|
||||
const issueRef = useRef<HTMLAnchorElement | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const issue = issues?.[issueId];
|
||||
|
||||
return <CalendarIssueBlock isDragging={isDragging} issue={issue} quickActions={quickActions} />;
|
||||
useEffect(() => {
|
||||
const element = issueRef.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
return combine(
|
||||
draggable({
|
||||
element,
|
||||
canDrag: () => !isDragDisabled,
|
||||
getInitialData: () => ({ id: issue?.id, date: issue?.target_date }),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDrop: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [issueRef?.current, issue]);
|
||||
|
||||
useOutsideClickDetector(issueRef, () => {
|
||||
issueRef?.current?.classList?.remove(HIGHLIGHT_CLASS);
|
||||
});
|
||||
|
||||
if (!issue) return null;
|
||||
|
||||
return <CalendarIssueBlock isDragging={isDragging} issue={issue} quickActions={quickActions} ref={issueRef} />;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef } from "react";
|
||||
/* eslint-disable react/display-name */
|
||||
import { useState, useRef, forwardRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
import { TIssue } from "@plane/types";
|
||||
@@ -19,106 +20,111 @@ type Props = {
|
||||
isDragging?: boolean;
|
||||
};
|
||||
|
||||
export const CalendarIssueBlock: React.FC<Props> = observer((props) => {
|
||||
const { issue, quickActions, isDragging = false } = props;
|
||||
// states
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
// refs
|
||||
const blockRef = useRef(null);
|
||||
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||
// hooks
|
||||
const { workspaceSlug, projectId } = useAppRouter();
|
||||
const { getProjectIdentifierById } = useProject();
|
||||
const { getProjectStates } = useProjectState();
|
||||
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
|
||||
const { isMobile } = usePlatformOS();
|
||||
export const CalendarIssueBlock = observer(
|
||||
forwardRef<HTMLAnchorElement, Props>((props, ref) => {
|
||||
const { issue, quickActions, isDragging = false } = props;
|
||||
// states
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
// refs
|
||||
const blockRef = useRef(null);
|
||||
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||
// hooks
|
||||
const { workspaceSlug, projectId } = useAppRouter();
|
||||
const { getProjectIdentifierById } = useProject();
|
||||
const { getProjectStates } = useProjectState();
|
||||
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const stateColor = getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || "";
|
||||
const stateColor = getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || "";
|
||||
|
||||
const handleIssuePeekOverview = (issue: TIssue) =>
|
||||
workspaceSlug &&
|
||||
issue &&
|
||||
issue.project_id &&
|
||||
issue.id &&
|
||||
!getIsIssuePeeked(issue.id) &&
|
||||
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
|
||||
const handleIssuePeekOverview = (issue: TIssue) =>
|
||||
workspaceSlug &&
|
||||
issue &&
|
||||
issue.project_id &&
|
||||
issue.id &&
|
||||
!getIsIssuePeeked(issue.id) &&
|
||||
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
|
||||
|
||||
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
||||
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
||||
|
||||
const customActionButton = (
|
||||
<div
|
||||
ref={menuActionRef}
|
||||
className={`w-full cursor-pointer rounded p-1 text-custom-sidebar-text-400 hover:bg-custom-background-80 ${
|
||||
isMenuActive ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
>
|
||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
);
|
||||
const customActionButton = (
|
||||
<div
|
||||
ref={menuActionRef}
|
||||
className={`w-full cursor-pointer rounded p-1 text-custom-sidebar-text-400 hover:bg-custom-background-80 ${
|
||||
isMenuActive ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
>
|
||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const isMenuActionRefAboveScreenBottom =
|
||||
menuActionRef?.current && menuActionRef?.current?.getBoundingClientRect().bottom < window.innerHeight - 220;
|
||||
const isMenuActionRefAboveScreenBottom =
|
||||
menuActionRef?.current && menuActionRef?.current?.getBoundingClientRect().bottom < window.innerHeight - 220;
|
||||
|
||||
const placement = isMenuActionRefAboveScreenBottom ? "bottom-end" : "top-end";
|
||||
const placement = isMenuActionRefAboveScreenBottom ? "bottom-end" : "top-end";
|
||||
|
||||
return (
|
||||
<ControlLink
|
||||
id={`issue-${issue.id}`}
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||
target="_blank"
|
||||
onClick={() => handleIssuePeekOverview(issue)}
|
||||
className="w-full cursor-pointer text-sm text-custom-text-100"
|
||||
disabled={!!issue?.tempId}
|
||||
>
|
||||
<>
|
||||
{issue?.tempId !== undefined && (
|
||||
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={blockRef}
|
||||
className={cn(
|
||||
"group/calendar-block flex h-10 w-full items-center justify-between gap-1.5 rounded border-b border-custom-border-200 px-4 py-1.5 hover:border-custom-border-400 md:h-8 md:border-[0.5px] md:px-1 ",
|
||||
{
|
||||
"bg-custom-background-90 shadow-custom-shadow-rg border-custom-primary-100": isDragging,
|
||||
"bg-custom-background-100 hover:bg-custom-background-90": !isDragging,
|
||||
"border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id),
|
||||
}
|
||||
return (
|
||||
<ControlLink
|
||||
id={`issue-${issue.id}`}
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||
target="_blank"
|
||||
onClick={() => handleIssuePeekOverview(issue)}
|
||||
className="block w-full text-sm text-custom-text-100 rounded border-b md:border-[1px] border-custom-border-200 hover:border-custom-border-400"
|
||||
disabled={!!issue?.tempId}
|
||||
ref={ref}
|
||||
>
|
||||
<>
|
||||
{issue?.tempId !== undefined && (
|
||||
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full items-center gap-1.5 truncate">
|
||||
<span
|
||||
className="h-full w-0.5 flex-shrink-0 rounded"
|
||||
style={{
|
||||
backgroundColor: stateColor,
|
||||
}}
|
||||
/>
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-300 md:text-xs">
|
||||
{getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
|
||||
</div>
|
||||
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
||||
<div className="truncate text-sm font-medium md:text-xs md:font-normal">{issue.name}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`h-5 w-5 flex-shrink-0 group-hover/calendar-block:block md:hidden ${
|
||||
isMenuActive ? "!block" : ""
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
ref={blockRef}
|
||||
className={cn(
|
||||
"group/calendar-block flex h-10 md:h-8 w-full items-center justify-between gap-1.5 rounded md:px-1 px-4 py-1.5 ",
|
||||
{
|
||||
"bg-custom-background-90 shadow-custom-shadow-rg border-custom-primary-100": isDragging,
|
||||
"bg-custom-background-100 hover:bg-custom-background-90": !isDragging,
|
||||
"border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id),
|
||||
}
|
||||
)}
|
||||
>
|
||||
{quickActions({
|
||||
issue,
|
||||
parentRef: blockRef,
|
||||
customActionButton,
|
||||
placement,
|
||||
})}
|
||||
<div className="flex h-full items-center gap-1.5 truncate">
|
||||
<span
|
||||
className="h-full w-0.5 flex-shrink-0 rounded"
|
||||
style={{
|
||||
backgroundColor: stateColor,
|
||||
}}
|
||||
/>
|
||||
<div className="flex-shrink-0 text-sm md:text-xs text-custom-text-300">
|
||||
{getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
|
||||
</div>
|
||||
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
||||
<div className="truncate text-sm font-medium md:font-normal md:text-xs">{issue.name}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className={`flex-shrink-0 md:hidden h-5 w-5 group-hover/calendar-block:block ${
|
||||
isMenuActive ? "!block" : ""
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{quickActions({
|
||||
issue,
|
||||
parentRef: blockRef,
|
||||
customActionButton,
|
||||
placement,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</ControlLink>
|
||||
);
|
||||
});
|
||||
</>
|
||||
</ControlLink>
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
CalendarIssueBlock.displayName = "CalendarIssueBlock";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { Draggable } from "@hello-pangea/dnd";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// types
|
||||
import { TIssue, TIssueMap } from "@plane/types";
|
||||
@@ -14,6 +13,9 @@ type Props = {
|
||||
date: Date;
|
||||
issues: TIssueMap | undefined;
|
||||
issueIdList: string[] | null;
|
||||
showAllIssues: boolean;
|
||||
setShowAllIssues?: Dispatch<SetStateAction<boolean>>;
|
||||
isMonthLayout: boolean;
|
||||
quickActions: TRenderQuickActions;
|
||||
isDragDisabled?: boolean;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
@@ -35,6 +37,8 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
date,
|
||||
issues,
|
||||
issueIdList,
|
||||
showAllIssues,
|
||||
setShowAllIssues,
|
||||
quickActions,
|
||||
isDragDisabled = false,
|
||||
enableQuickIssueCreate,
|
||||
@@ -43,10 +47,9 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
addIssuesToView,
|
||||
viewId,
|
||||
readOnly,
|
||||
isMonthLayout,
|
||||
isMobileView = false,
|
||||
} = props;
|
||||
// states
|
||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||
|
||||
const formattedDatePayload = renderFormattedPayloadDate(date);
|
||||
const totalIssues = issueIdList?.length ?? 0;
|
||||
@@ -55,30 +58,27 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueIdList?.slice(0, showAllIssues || isMobileView ? issueIdList.length : 4).map((issueId, index) =>
|
||||
!isMobileView ? (
|
||||
<Draggable key={issueId} draggableId={issueId} index={index} isDragDisabled={isDragDisabled}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className="relative cursor-pointer p-1 px-2"
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<CalendarIssueBlockRoot
|
||||
issues={issues}
|
||||
issueId={issueId}
|
||||
quickActions={quickActions}
|
||||
isDragging={snapshot.isDragging}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
) : (
|
||||
<CalendarIssueBlockRoot key={issueId} issues={issues} issueId={issueId} quickActions={quickActions} />
|
||||
)
|
||||
{issueIdList?.slice(0, showAllIssues || !isMonthLayout ? issueIdList.length : 4).map((issueId) => (
|
||||
<div key={issueId} className="relative cursor-pointer p-1 px-2">
|
||||
<CalendarIssueBlockRoot
|
||||
issues={issues}
|
||||
issueId={issueId}
|
||||
quickActions={quickActions}
|
||||
isDragDisabled={isDragDisabled || isMobileView}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{totalIssues > 4 && isMonthLayout && (
|
||||
<div className="hidden items-center px-2.5 py-1 md:flex">
|
||||
<button
|
||||
type="button"
|
||||
className="w-min whitespace-nowrap rounded px-1.5 py-1 text-xs font-medium text-custom-text-400 hover:bg-custom-background-80 hover:text-custom-text-300"
|
||||
onClick={() => setShowAllIssues && setShowAllIssues(!showAllIssues)}
|
||||
>
|
||||
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{enableQuickIssueCreate && !disableIssueCreation && !readOnly && (
|
||||
<div className="border-b border-custom-border-200 px-1 py-1 md:border-none md:px-2">
|
||||
<CalendarQuickAddIssueForm
|
||||
@@ -90,21 +90,10 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
quickAddCallback={quickAddCallback}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={viewId}
|
||||
onOpen={() => setShowAllIssues(true)}
|
||||
onOpen={() => setShowAllIssues && setShowAllIssues(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{totalIssues > 4 && (
|
||||
<div className="hidden items-center px-2.5 py-1 md:flex">
|
||||
<button
|
||||
type="button"
|
||||
className="w-min whitespace-nowrap rounded px-1.5 py-1 text-xs font-medium text-custom-text-400 hover:bg-custom-background-80 hover:text-custom-text-300"
|
||||
onClick={() => setShowAllIssues(!showAllIssues)}
|
||||
>
|
||||
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,36 +1,21 @@
|
||||
import { DraggableLocation } from "@hello-pangea/dnd";
|
||||
import { TGroupedIssues, IIssueMap, TIssue } from "@plane/types";
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
export const handleDragDrop = async (
|
||||
source: DraggableLocation,
|
||||
destination: DraggableLocation,
|
||||
issueId: string,
|
||||
sourceDate: string,
|
||||
destinationDate: string,
|
||||
workspaceSlug: string | undefined,
|
||||
projectId: string | undefined,
|
||||
issueMap: IIssueMap,
|
||||
issueWithIds: TGroupedIssues,
|
||||
updateIssue?: (projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>
|
||||
) => {
|
||||
if (!issueMap || !issueWithIds || !workspaceSlug || !projectId || !updateIssue) return;
|
||||
if (!workspaceSlug || !projectId || !updateIssue) return;
|
||||
|
||||
const sourceColumnId = source?.droppableId || null;
|
||||
const destinationColumnId = destination?.droppableId || null;
|
||||
if (sourceDate === destinationDate) return;
|
||||
|
||||
if (!workspaceSlug || !projectId || !sourceColumnId || !destinationColumnId) return;
|
||||
const updatedIssue = {
|
||||
id: issueId,
|
||||
target_date: destinationDate,
|
||||
};
|
||||
|
||||
if (sourceColumnId === destinationColumnId) return;
|
||||
|
||||
// horizontal
|
||||
if (sourceColumnId != destinationColumnId) {
|
||||
const sourceIssues = issueWithIds[sourceColumnId] || [];
|
||||
|
||||
const [removed] = sourceIssues.splice(source.index, 1);
|
||||
const removedIssueDetail = issueMap[removed];
|
||||
|
||||
const updatedIssue = {
|
||||
id: removedIssueDetail?.id,
|
||||
target_date: destinationColumnId,
|
||||
};
|
||||
|
||||
return await updateIssue(projectId, updatedIssue.id, updatedIssue);
|
||||
}
|
||||
return await updateIssue(projectId, updatedIssue.id, updatedIssue);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,11 @@ type Props = {
|
||||
quickActions: TRenderQuickActions;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
handleDragAndDrop: (
|
||||
issueId: string | undefined,
|
||||
sourceDate: string | undefined,
|
||||
destinationDate: string | undefined
|
||||
) => Promise<void>;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@@ -38,6 +43,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
issuesFilterStore,
|
||||
issues,
|
||||
groupedIssueIds,
|
||||
handleDragAndDrop,
|
||||
week,
|
||||
quickActions,
|
||||
enableQuickIssueCreate,
|
||||
@@ -80,6 +86,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={viewId}
|
||||
readOnly={readOnly}
|
||||
handleDragAndDrop={handleDragAndDrop}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -68,7 +68,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 gap-2.5">
|
||||
<div className="flex justify-between p-4 gap-2.5">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
|
||||
@@ -77,7 +77,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
|
||||
if (Object.keys(appliedFilters).length === 0 || !workspaceSlug || !projectId) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 gap-2.5">
|
||||
<div className="flex justify-between p-4 gap-2.5">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
|
||||
@@ -63,7 +63,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => {
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 gap-2.5">
|
||||
<div className="flex justify-between p-4 gap-2.5">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
|
||||
@@ -76,7 +76,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
|
||||
if (!workspaceSlug || !projectId || Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 gap-2.5">
|
||||
<div className="flex justify-between p-4 gap-2.5">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
|
||||
@@ -68,7 +68,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 gap-2.5">
|
||||
<div className="flex justify-between p-4 gap-2.5">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
|
||||
@@ -95,7 +95,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 p-4">
|
||||
<div className="flex justify-between gap-4 p-4">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters ?? {}}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
@@ -106,14 +106,11 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||
/>
|
||||
|
||||
{!areFiltersEqual && (
|
||||
<>
|
||||
<div />
|
||||
<div className="flex flex-shrink-0 items-center justify-center">
|
||||
<Button variant="primary" size="sm" onClick={handleUpdateView}>
|
||||
Update view
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
<div>
|
||||
<Button variant="primary" size="sm" className="flex-shrink-0" onClick={handleUpdateView}>
|
||||
Update view
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -644,7 +644,8 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-200 px-1.5 py-5">
|
||||
<Disclosure>
|
||||
{/* Accessing link outside the disclosure as mobx is not considering the children inside Disclosure as part of the component hence not observing their state change*/}
|
||||
<Disclosure defaultOpen={!!moduleDetails?.link_module?.length}>
|
||||
{({ open }) => (
|
||||
<div className={`relative flex h-full w-full flex-col ${open ? "" : "flex-row"}`}>
|
||||
<Disclosure.Button className="flex w-full items-center justify-between gap-2 p-1.5">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
@@ -22,6 +22,8 @@ import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
import { PageService } from "@/services/page.service";
|
||||
const pageService = new PageService();
|
||||
// store
|
||||
import { IPageStore } from "@/store/pages/page.store";
|
||||
|
||||
@@ -31,7 +33,6 @@ type Props = {
|
||||
control: Control<TPage, any>;
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
|
||||
swrPageDetails: TPage | undefined;
|
||||
handleSubmit: () => void;
|
||||
markings: IMarking[];
|
||||
pageStore: IPageStore;
|
||||
@@ -49,12 +50,13 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
editorRef,
|
||||
markings,
|
||||
readOnlyEditorRef,
|
||||
handleSubmit,
|
||||
// handleSubmit,
|
||||
pageStore,
|
||||
swrPageDetails,
|
||||
sidePeekVisible,
|
||||
updateMarkings,
|
||||
} = props;
|
||||
// states
|
||||
const [descriptionYJS, setDescriptionYJS] = useState<Uint8Array | null>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
@@ -67,6 +69,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
} = useMember();
|
||||
// derived values
|
||||
const workspaceId = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "" : "";
|
||||
const pageId = pageStore?.id ?? "";
|
||||
const pageTitle = pageStore?.name ?? "";
|
||||
const pageDescription = pageStore?.description_html;
|
||||
const { description_html, isContentEditable, updateTitle, isSubmitting, setIsSubmitting } = pageStore;
|
||||
@@ -82,13 +85,70 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
// page filters
|
||||
const { isFullWidth } = usePageFilters();
|
||||
|
||||
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
||||
useReloadConfirmations(isSubmitting === "submitting");
|
||||
|
||||
// const { data: pageDescriptionYJS } = useSWR(
|
||||
// workspaceSlug && projectId && pageId ? `PAGE_DESCRIPTION_${workspaceSlug}_${projectId}_${pageId}` : null,
|
||||
// workspaceSlug && projectId && pageId
|
||||
// ? () => pageService.fetchDescriptionYJS(workspaceSlug.toString(), projectId.toString(), pageId.toString())
|
||||
// : null
|
||||
// );
|
||||
|
||||
const handleDescriptionChange = useCallback(
|
||||
(binaryString: string, descriptionHTML: string) => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
pageService.updateDescriptionYJS(workspaceSlug.toString(), projectId.toString(), pageId.toString(), {
|
||||
description_yjs: binaryString,
|
||||
description_html: descriptionHTML,
|
||||
});
|
||||
// setIsSubmitting("submitting");
|
||||
// setShowAlert(true);
|
||||
// onChange(description_html);
|
||||
// handleSubmit();
|
||||
},
|
||||
[pageId, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDescription = async () => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
console.log("fetching...");
|
||||
|
||||
const response = await fetch(
|
||||
`http://localhost:8000/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`,
|
||||
{
|
||||
credentials: "include",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = await response.arrayBuffer();
|
||||
setDescriptionYJS(new Uint8Array(data));
|
||||
// __AUTO_GENERATED_PRINT_VAR_START__
|
||||
console.log("fetchById data: %s", data); // __AUTO_GENERATED_PRINT_VAR_END__
|
||||
// if (data.byteLength === 0) {
|
||||
// const yjs = await fetchByIdIfExists(workspaceSlug, projectId, pageId);
|
||||
// if (yjs) {
|
||||
// console.log("not found in db:", yjs, yjs instanceof Uint8Array);
|
||||
// return yjs;
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchDescription();
|
||||
}, 15000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [pageId, projectId, workspaceSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
updateMarkings(description_html ?? "<p></p>");
|
||||
}, [description_html, updateMarkings]);
|
||||
|
||||
if (pageDescription === undefined) return <PageContentLoader />;
|
||||
if (pageDescription === undefined || pageId === undefined || !descriptionYJS) return <PageContentLoader />;
|
||||
|
||||
return (
|
||||
<div className="flex items-center h-full w-full overflow-y-auto">
|
||||
@@ -125,8 +185,9 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { onChange } }) => (
|
||||
render={() => (
|
||||
<DocumentEditorWithRef
|
||||
id={pageId}
|
||||
fileHandler={{
|
||||
cancel: fileService.cancelUpload,
|
||||
delete: fileService.getDeleteImageFunction(workspaceId),
|
||||
@@ -134,17 +195,11 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
upload: fileService.getUploadFileFunction(workspaceSlug as string, setIsSubmitting),
|
||||
}}
|
||||
handleEditorReady={handleEditorReady}
|
||||
initialValue={pageDescription ?? "<p></p>"}
|
||||
value={swrPageDetails?.description_html ?? "<p></p>"}
|
||||
value={descriptionYJS}
|
||||
ref={editorRef}
|
||||
containerClassName="p-0 pb-64"
|
||||
editorClassName="lg:px-10 pl-8"
|
||||
onChange={(_description_json, description_html) => {
|
||||
setIsSubmitting("submitting");
|
||||
setShowAlert(true);
|
||||
onChange(description_html);
|
||||
handleSubmit();
|
||||
}}
|
||||
onChange={handleDescriptionChange}
|
||||
mentionHandler={{
|
||||
highlights: mentionHighlights,
|
||||
suggestions: mentionSuggestions,
|
||||
|
||||
@@ -174,10 +174,10 @@ export const getFetchKeysForIssueMutation = (options: {
|
||||
const ganttFetchKey = cycleId
|
||||
? { ganttFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), ganttParams) }
|
||||
: moduleId
|
||||
? { ganttFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), ganttParams) }
|
||||
: viewId
|
||||
? { ganttFetchKey: VIEW_ISSUES(viewId.toString(), viewGanttParams) }
|
||||
: { ganttFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", ganttParams) };
|
||||
? { ganttFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), ganttParams) }
|
||||
: viewId
|
||||
? { ganttFetchKey: VIEW_ISSUES(viewId.toString(), viewGanttParams) }
|
||||
: { ganttFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", ganttParams) };
|
||||
|
||||
return {
|
||||
...ganttFetchKey,
|
||||
@@ -230,7 +230,7 @@ export const checkEmailValidity = (email: string): boolean => {
|
||||
|
||||
export const isEmptyHtmlString = (htmlString: string) => {
|
||||
// Remove HTML tags using regex
|
||||
const cleanText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: [] });
|
||||
const cleanText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: ["img"] });
|
||||
// Trim the string and check if it's empty
|
||||
return cleanText.trim() === "";
|
||||
};
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
const useDraggableInPortal = () => {
|
||||
const self = useRef<Element>();
|
||||
|
||||
useEffect(() => {
|
||||
const div = document.createElement("div");
|
||||
div.style.position = "absolute";
|
||||
div.style.pointerEvents = "none";
|
||||
div.style.top = "0";
|
||||
div.style.width = "100%";
|
||||
div.style.height = "100%";
|
||||
self.current = div;
|
||||
document.body.appendChild(div);
|
||||
return () => {
|
||||
document.body.removeChild(div);
|
||||
};
|
||||
}, [self.current]);
|
||||
|
||||
return (render: any) => (provided: DraggableProvided, snapshot: DraggableStateSnapshot) => {
|
||||
const element = render(provided, snapshot);
|
||||
if (self.current && snapshot?.isDragging) {
|
||||
return createPortal(element, self.current);
|
||||
}
|
||||
return element;
|
||||
};
|
||||
};
|
||||
|
||||
export default useDraggableInPortal;
|
||||
@@ -2,7 +2,7 @@ import { ReactElement, createContext } from "react";
|
||||
// mobx store
|
||||
import { RootStore } from "@/store/root.store";
|
||||
|
||||
let rootStore = new RootStore();
|
||||
export let rootStore = new RootStore();
|
||||
|
||||
export const StoreContext = createContext<RootStore>(rootStore);
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
// fetching page details
|
||||
const {
|
||||
data: swrPageDetails,
|
||||
isValidating,
|
||||
error: pageDetailsError,
|
||||
} = useSWR(pageId ? `PAGE_DETAILS_${pageId}` : null, pageId ? () => getPageById(pageId.toString()) : null, {
|
||||
@@ -145,7 +144,6 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
/>
|
||||
)}
|
||||
<PageEditorBody
|
||||
swrPageDetails={swrPageDetails}
|
||||
control={control}
|
||||
editorRef={editorRef}
|
||||
handleEditorReady={(val) => setEditorReady(val)}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
// store
|
||||
import { rootStore } from "@/lib/store-context";
|
||||
|
||||
export abstract class APIService {
|
||||
protected baseURL: string;
|
||||
@@ -19,14 +21,21 @@ export abstract class APIService {
|
||||
this.axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response && error.response.status === 401) window.location.href = "/accounts/sign-in";
|
||||
const store = rootStore;
|
||||
if (error.response && error.response.status === 401 && store.user.data) {
|
||||
store.user.reset();
|
||||
store.resetOnSignOut();
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get(url: string, params = {}) {
|
||||
return this.axiosInstance.get(url, params);
|
||||
get(url: string, params = {}, config = {}) {
|
||||
return this.axiosInstance.get(url, {
|
||||
...params,
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
post(url: string, data = {}, config = {}) {
|
||||
|
||||
@@ -119,4 +119,24 @@ export class PageService extends APIService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchDescriptionYJS(workspaceSlug: string, projectId: string, pageId: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, {
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateDescriptionYJS(workspaceSlug: string, projectId: string, pageId: string, data: any): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface IUserStore {
|
||||
updateCurrentUser: (data: Partial<IUser>) => Promise<IUser | undefined>;
|
||||
handleSetPassword: (csrfToken: string, data: { password: string }) => Promise<IUser | undefined>;
|
||||
deactivateAccount: () => Promise<void>;
|
||||
reset: () => void;
|
||||
signOut: () => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -79,6 +80,7 @@ export class UserStore implements IUserStore {
|
||||
updateCurrentUser: action,
|
||||
handleSetPassword: action,
|
||||
deactivateAccount: action,
|
||||
reset: action,
|
||||
signOut: action,
|
||||
});
|
||||
}
|
||||
@@ -191,6 +193,22 @@ export class UserStore implements IUserStore {
|
||||
this.store.resetOnSignOut();
|
||||
};
|
||||
|
||||
/**
|
||||
* @description resets the user store
|
||||
* @returns {void}
|
||||
*/
|
||||
reset = (): void => {
|
||||
runInAction(() => {
|
||||
this.isAuthenticated = false;
|
||||
this.isLoading = false;
|
||||
this.error = undefined;
|
||||
this.data = undefined;
|
||||
this.userProfile = new ProfileStore(this.store);
|
||||
this.userSettings = new UserSettingsStore();
|
||||
this.membership = new UserMembershipStore(this.store);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description signs out the current user
|
||||
* @returns {Promise<void>}
|
||||
|
||||
78
yarn.lock
78
yarn.lock
@@ -2410,6 +2410,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.3.1.tgz#cea919becab684688819b29481a5c43ee1ee9c52"
|
||||
integrity sha512-bVX0EnDZoRXnoA7dyoZe7w2gdRjxmFEcsatHLkcr3R3x4k9oSgZXLe1C2jGbjJWr4j32tYXZ1cpKte6f1WUKzg==
|
||||
|
||||
"@tiptap/extension-collaboration@^2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-collaboration/-/extension-collaboration-2.3.2.tgz#0780eabbe2e72665ed83f86dc70790589d1d0ff1"
|
||||
integrity sha512-1vN+crj5KgqoJhDV+CrfIrBWDIjfpVxiEWHBk+yQU/G2vmyQfbN/R/5gH6rOw5GT3mHqgWFtCDJo4+H/2Ete4w==
|
||||
|
||||
"@tiptap/extension-document@^2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.3.1.tgz#c2c3a1d1f87e262872012508555eda8227a3bc7a"
|
||||
@@ -2658,7 +2663,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.5.tgz#1e78a3ac2428e6d7e6c05c1665c242023a4601d8"
|
||||
integrity sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==
|
||||
|
||||
"@types/lodash@^4.14.202", "@types/lodash@^4.17.0":
|
||||
"@types/lodash@^4.14.202", "@types/lodash@^4.17.0", "@types/lodash@^4.17.1":
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.1.tgz#0fabfcf2f2127ef73b119d98452bd317c4a17eb8"
|
||||
integrity sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==
|
||||
@@ -2757,7 +2762,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@18.2.48", "@types/react@^18.2.42", "@types/react@^18.2.48":
|
||||
"@types/react@*", "@types/react@^18.2.42", "@types/react@^18.2.48":
|
||||
version "18.2.48"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1"
|
||||
integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==
|
||||
@@ -5635,6 +5640,11 @@ isexe@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||
|
||||
isomorphic.js@^0.2.4:
|
||||
version "0.2.5"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic.js/-/isomorphic.js-0.2.5.tgz#13eecf36f2dba53e85d355e11bf9d4208c6f7f88"
|
||||
integrity sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==
|
||||
|
||||
iterator.prototype@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0"
|
||||
@@ -5845,6 +5855,13 @@ levn@^0.4.1:
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "~0.4.0"
|
||||
|
||||
lib0@^0.2.42, lib0@^0.2.74, lib0@^0.2.85, lib0@^0.2.86:
|
||||
version "0.2.93"
|
||||
resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.93.tgz#95487c2a97657313cb1d91fbcf9f6d64b7fcd062"
|
||||
integrity sha512-M5IKsiFJYulS+8Eal8f+zAqf5ckm1vffW0fFDxfgxJ+uiVopvDdd3PxJmz0GsVi3YNO7QCFSq0nAsiDmNhLj9Q==
|
||||
dependencies:
|
||||
isomorphic.js "^0.2.4"
|
||||
|
||||
lie@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
|
||||
@@ -6935,10 +6952,10 @@ postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.29:
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
posthog-js@^1.105.0:
|
||||
version "1.131.2"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.131.2.tgz#c82b16a4074f773eaf41df47187fb88cc5aef28c"
|
||||
integrity sha512-un5c5CbDhJ1LRBDgy4I1D5a1++P8/mNl4CS9C5A1z95qIF7iY8OuA6XPW7sIA6tKSdda4PGwfa2Gmfz1nvnywQ==
|
||||
posthog-js@^1.131.3:
|
||||
version "1.131.3"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.131.3.tgz#bd3e6123dc715f089825a92d3ec62480b7ec0a76"
|
||||
integrity sha512-ds/TADDS+rT/WgUyeW4cJ+X+fX+O1KdkOyssNI/tP90PrFf0IJsck5B42YOLhfz87U2vgTyBaKHkdlMgWuOFog==
|
||||
dependencies:
|
||||
fflate "^0.4.8"
|
||||
preact "^10.19.3"
|
||||
@@ -7954,16 +7971,8 @@ streamx@^2.15.0, streamx@^2.16.1:
|
||||
optionalDependencies:
|
||||
bare-events "^2.2.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
||||
name string-width-cjs
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -8043,14 +8052,7 @@ stringify-object@^3.3.0:
|
||||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -9137,6 +9139,27 @@ wrappy@1:
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
y-indexeddb@^9.0.12:
|
||||
version "9.0.12"
|
||||
resolved "https://registry.yarnpkg.com/y-indexeddb/-/y-indexeddb-9.0.12.tgz#73657f31d52886d7532256610babf5cca4ad5e58"
|
||||
integrity sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg==
|
||||
dependencies:
|
||||
lib0 "^0.2.74"
|
||||
|
||||
y-prosemirror@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/y-prosemirror/-/y-prosemirror-1.2.5.tgz#c448f80a6017190bc69a30a33f3930e9924fad3a"
|
||||
integrity sha512-T/JATxC8P2Dbvq/dAiaiztD1a8KEwRP8oLRlT8YlaZdNlLGE1Ea0IJ8If25UlDYmk+4+uqLbqT/S+dzUmwwgbA==
|
||||
dependencies:
|
||||
lib0 "^0.2.42"
|
||||
|
||||
y-protocols@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/y-protocols/-/y-protocols-1.0.6.tgz#66dad8a95752623443e8e28c0e923682d2c0d495"
|
||||
integrity sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==
|
||||
dependencies:
|
||||
lib0 "^0.2.85"
|
||||
|
||||
yallist@^3.0.2:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
||||
@@ -9152,6 +9175,13 @@ yaml@^2.3.4:
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.2.tgz#7a2b30f2243a5fc299e1f14ca58d475ed4bc5362"
|
||||
integrity sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==
|
||||
|
||||
yjs@^13.6.15:
|
||||
version "13.6.15"
|
||||
resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.15.tgz#5a2402632aabf83e5baf56342b4c82fe40859306"
|
||||
integrity sha512-moFv4uNYhp8BFxIk3AkpoAnnjts7gwdpiG8RtyFiKbMtxKCS0zVZ5wPaaGpwC3V2N/K8TK8MwtSI3+WO9CHWjQ==
|
||||
dependencies:
|
||||
lib0 "^0.2.86"
|
||||
|
||||
yocto-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
|
||||
Reference in New Issue
Block a user