Compare commits

..
Author SHA1 Message Date
LAKHAN BAHETI 2448a60952 added module events 2024-06-03 17:06:55 +05:30
207 changed files with 2503 additions and 5077 deletions
@@ -3,11 +3,10 @@ name: Build and Lint on Pull Request
on:
workflow_dispatch:
pull_request:
types: ["opened", "synchronize", "ready_for_review"]
types: ["opened", "synchronize"]
jobs:
get-changed-files:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
outputs:
apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }}
+1 -1
View File
@@ -48,7 +48,7 @@ Meet [Plane](https://dub.sh/plane-website-readme), an open-source project manage
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account.
If you would like to self-host Plane, please see our [deployment guide](https://docs.plane.so/self-hosting/overview).
If you would like to self-host Plane, please see our [deployment guide](https://docs.plane.so/docker-compose).
| Installation methods | Docs link |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -1,59 +0,0 @@
"use client";
import React from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
// types
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
// ui
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useInstance } from "@/hooks/store";
type Props = {
disabled: boolean;
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
export const GitlabConfiguration: React.FC<Props> = observer((props) => {
const { disabled, updateConfig } = props;
// store
const { formattedConfig } = useInstance();
// derived values
const enableGitlabConfig = formattedConfig?.IS_GITLAB_ENABLED ?? "";
const isGitlabConfigured = !!formattedConfig?.GITLAB_CLIENT_ID && !!formattedConfig?.GITLAB_CLIENT_SECRET;
return (
<>
{isGitlabConfigured ? (
<div className="flex items-center gap-4">
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
Edit
</Link>
<ToggleSwitch
value={Boolean(parseInt(enableGitlabConfig))}
onChange={() => {
Boolean(parseInt(enableGitlabConfig)) === true
? updateConfig("IS_GITLAB_ENABLED", "0")
: updateConfig("IS_GITLAB_ENABLED", "1");
}}
size="sm"
disabled={disabled}
/>
</div>
) : (
<Link
href="/authentication/gitlab"
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
>
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
Configure
</Link>
)}
</>
);
});
@@ -1,6 +1,5 @@
export * from "./email-config-switch";
export * from "./password-config-switch";
export * from "./authentication-method-card";
export * from "./gitlab-config";
export * from "./github-config";
export * from "./google-config";
-212
View File
@@ -1,212 +0,0 @@
import { FC, useState } from "react";
import isEmpty from "lodash/isEmpty";
import Link from "next/link";
import { useForm } from "react-hook-form";
// types
import { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigurationKeys } from "@plane/types";
// ui
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
// components
import {
ConfirmDiscardModal,
ControllerInput,
CopyField,
TControllerInputFormField,
TCopyField,
} from "@/components/common";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper";
// hooks
import { useInstance } from "@/hooks/store";
type Props = {
config: IFormattedInstanceConfiguration;
};
type GitlabConfigFormValues = Record<TInstanceGitlabAuthenticationConfigurationKeys, string>;
export const InstanceGitlabConfigForm: FC<Props> = (props) => {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
// store hooks
const { updateInstanceConfigurations } = useInstance();
// form data
const {
handleSubmit,
control,
reset,
formState: { errors, isDirty, isSubmitting },
} = useForm<GitlabConfigFormValues>({
defaultValues: {
GITLAB_HOST: config["GITLAB_HOST"],
GITLAB_CLIENT_ID: config["GITLAB_CLIENT_ID"],
GITLAB_CLIENT_SECRET: config["GITLAB_CLIENT_SECRET"],
},
});
const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "";
const GITLAB_FORM_FIELDS: TControllerInputFormField[] = [
{
key: "GITLAB_HOST",
type: "text",
label: "Host",
description: (
<>
This is the <b>GitLab host</b> to use for login, <b>including scheme</b>.
</>
),
placeholder: "https://gitlab.com",
error: Boolean(errors.GITLAB_HOST),
required: true,
},
{
key: "GITLAB_CLIENT_ID",
type: "text",
label: "Application ID",
description: (
<>
Get this from your{" "}
<a
tabIndex={-1}
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitLab OAuth application settings
</a>
.
</>
),
placeholder: "c2ef2e7fc4e9d15aa7630f5637d59e8e4a27ff01dceebdb26b0d267b9adcf3c3",
error: Boolean(errors.GITLAB_CLIENT_ID),
required: true,
},
{
key: "GITLAB_CLIENT_SECRET",
type: "password",
label: "Secret",
description: (
<>
The client secret is also found in your{" "}
<a
tabIndex={-1}
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitLab OAuth application settings
</a>
.
</>
),
placeholder: "gloas-f79cfa9a03c97f6ffab303177a5a6778a53c61e3914ba093412f68a9298a1b28",
error: Boolean(errors.GITLAB_CLIENT_SECRET),
required: true,
},
];
const GITLAB_SERVICE_FIELD: TCopyField[] = [
{
key: "Callback_URL",
label: "Callback URL",
url: `${originURL}/auth/gitlab/callback/`,
description: (
<>
We will auto-generate this. Paste this into the <b>Redirect URI</b> field of your{" "}
<a
tabIndex={-1}
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitLab OAuth application
</a>
.
</>
),
},
];
const onSubmit = async (formData: GitlabConfigFormValues) => {
const payload: Partial<GitlabConfigFormValues> = { ...formData };
await updateInstanceConfigurations(payload)
.then((response = []) => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success",
message: "GitLab Configuration Settings updated successfully",
});
reset({
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value,
GITLAB_CLIENT_ID: response.find((item) => item.key === "GITLAB_CLIENT_ID")?.value,
GITLAB_CLIENT_SECRET: response.find((item) => item.key === "GITLAB_CLIENT_SECRET")?.value,
});
})
.catch((err) => console.error(err));
};
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (isDirty) {
e.preventDefault();
setIsDiscardChangesModalOpen(true);
}
};
return (
<>
<ConfirmDiscardModal
isOpen={isDiscardChangesModalOpen}
onDiscardHref="/authentication"
handleClose={() => setIsDiscardChangesModalOpen(false)}
/>
<div className="flex flex-col gap-8">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1">
<div className="pt-2 text-xl font-medium">Configuration</div>
{GITLAB_FORM_FIELDS.map((field) => (
<ControllerInput
key={field.key}
control={control}
type={field.type}
name={field.key}
label={field.label}
description={field.description}
placeholder={field.placeholder}
error={field.error}
required={field.required}
/>
))}
<div className="flex flex-col gap-1 pt-4">
<div className="flex items-center gap-4">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
<Link
href="/authentication"
className={cn(getButtonStyling("link-neutral", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back
</Link>
</div>
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 px-6 py-4 my-2 bg-custom-background-80/60 rounded-lg">
<div className="pt-2 text-xl font-medium">Service provider details</div>
{GITLAB_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
</div>
</div>
</div>
</div>
</>
);
};
-101
View File
@@ -1,101 +0,0 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
// icons
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
// local components
import { AuthenticationMethodCard } from "../components";
import { InstanceGitlabConfigForm } from "./form";
const InstanceGitlabAuthenticationPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// config
const enableGitlabConfig = formattedConfig?.IS_GITLAB_ENABLED ?? "";
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
const updateConfig = async (key: "IS_GITLAB_ENABLED", value: string) => {
setIsSubmitting(true);
const payload = {
[key]: value,
};
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `GitLab authentication is now ${value ? "active" : "disabled"}.`,
},
error: {
title: "Error",
message: () => "Failed to save configuration",
},
});
await updateConfigPromise
.then(() => {
setIsSubmitting(false);
})
.catch((err) => {
console.error(err);
setIsSubmitting(false);
});
};
return (
<>
<PageHeader title="Authentication - God Mode" />
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
name="GitLab"
description="Allow members to login or sign up to plane with their GitLab accounts."
icon={<Image src={GitlabLogo} height={24} width={24} alt="GitLab Logo" />}
config={
<ToggleSwitch
value={Boolean(parseInt(enableGitlabConfig))}
onChange={() => {
Boolean(parseInt(enableGitlabConfig)) === true
? updateConfig("IS_GITLAB_ENABLED", "0")
: updateConfig("IS_GITLAB_ENABLED", "1");
}}
size="sm"
disabled={isSubmitting || !formattedConfig}
/>
}
disabled={isSubmitting || !formattedConfig}
withBorder={false}
/>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md p-4">
{formattedConfig ? (
<InstanceGitlabConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
</Loader>
)}
</div>
</div>
</>
);
});
export default InstanceGitlabAuthenticationPage;
+3 -41
View File
@@ -7,24 +7,22 @@ import { useTheme } from "next-themes";
import useSWR from "swr";
import { Mails, KeyRound } from "lucide-react";
import { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
import { Loader, setPromiseToast } from "@plane/ui";
// components
import { PageHeader } from "@/components/core";
// hooks
// helpers
import { cn, resolveGeneralTheme } from "@/helpers/common.helper";
import { resolveGeneralTheme } from "@/helpers/common.helper";
import { useInstance } from "@/hooks/store";
// images
import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png";
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
import GoogleLogo from "@/public/logos/google-logo.svg";
// local components
import {
AuthenticationMethodCard,
EmailCodesConfiguration,
PasswordLoginConfiguration,
GitlabConfiguration,
GithubConfiguration,
GoogleConfiguration,
} from "./components";
@@ -47,8 +45,6 @@ const InstanceAuthenticationPage = observer(() => {
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// theme
const { resolvedTheme } = useTheme();
// derived values
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
setIsSubmitting(true);
@@ -118,13 +114,6 @@ const InstanceAuthenticationPage = observer(() => {
),
config: <GithubConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
},
{
key: "gitlab",
name: "GitLab",
description: "Allow members to login or sign up to plane with their GitLab accounts.",
icon: <Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
config: <GitlabConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
},
];
return (
@@ -140,34 +129,7 @@ const InstanceAuthenticationPage = observer(() => {
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<div className="space-y-3">
<div className="text-lg font-medium pb-1">Sign-up configuration</div>
<div className={cn("w-full flex items-center gap-14 rounded")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className={cn("font-medium leading-5 text-custom-text-100 text-sm")}>
Allow anyone to sign up without invite
</div>
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
Toggling this off will disable self sign ups.
</div>
</div>
</div>
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
<div className="flex items-center gap-4">
<ToggleSwitch
value={Boolean(parseInt(enableSignUpConfig))}
onChange={() => {
Boolean(parseInt(enableSignUpConfig)) === true
? updateConfig("ENABLE_SIGNUP", "0")
: updateConfig("ENABLE_SIGNUP", "1");
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
</div>
<div className="text-lg font-medium pt-6">Authentication modes</div>
<div className="text-lg font-medium">Authentication modes</div>
{authenticationMethodsCard.map((method) => (
<AuthenticationMethodCard
key={method.key}
@@ -5,11 +5,9 @@ import { observer } from "mobx-react-lite";
import Link from "next/link";
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react";
// ui
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
// helpers
import { WEB_BASE_URL, cn } from "@/helpers/common.helper";
// hooks
import { WEB_BASE_URL } from "@/helpers/common.helper";
import { useTheme } from "@/hooks/store";
// assets
import packageJson from "package.json";
@@ -44,12 +42,9 @@ export const HelpSection: FC = observer(() => {
return (
<div
className={cn(
"flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 h-14 flex-shrink-0",
{
"flex-col h-auto py-1.5": isSidebarCollapsed,
}
)}
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
isSidebarCollapsed ? "flex-col" : ""
}`}
>
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
<Tooltip tooltipContent="Redirect to plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
-2
View File
@@ -31,8 +31,6 @@ export const InstanceHeader: FC = observer(() => {
return "Google";
case "github":
return "Github";
case "gitlab":
return "GitLab";
default:
return pathName.toUpperCase();
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "admin",
"version": "0.21.0",
"version": "0.20.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="80 80 220 220"><defs><style>.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style></defs><g id="LOGO"><path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-2" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-3" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/><path class="cls-2" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File
View File
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.21.0"
"version": "0.20.0"
}
+37 -100
View File
@@ -4,8 +4,6 @@ import uuid
# Django imports
from django.utils import timezone
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
# Third party imports
from zxcvbn import zxcvbn
@@ -48,115 +46,53 @@ class Adapter:
def authenticate(self):
raise NotImplementedError
def sanitize_email(self, email):
# Check if email is present
if not email:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL",
payload={"email": email},
)
# Sanitize email
email = str(email).lower().strip()
# validate email
try:
validate_email(email)
except ValidationError:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL",
payload={"email": email},
)
# Return email
return email
def validate_password(self, email):
"""Validate password strength"""
results = zxcvbn(self.code)
if results["score"] < 3:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
payload={"email": email},
)
return
def __check_signup(self, email):
"""Check if sign up is enabled or not and raise exception if not enabled"""
# Get configuration value
(ENABLE_SIGNUP,) = get_configuration_value(
[
{
"key": "ENABLE_SIGNUP",
"default": os.environ.get("ENABLE_SIGNUP", "1"),
},
]
)
# Check if sign up is disabled and invite is present or not
if (
ENABLE_SIGNUP == "0"
and not WorkspaceMemberInvite.objects.filter(
email=email,
).exists()
):
# Raise exception
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["SIGNUP_DISABLED"],
error_message="SIGNUP_DISABLED",
payload={"email": email},
)
return True
def save_user_data(self, user):
# Update user details
user.last_login_medium = self.provider
user.last_active = timezone.now()
user.last_login_time = timezone.now()
user.last_login_ip = self.request.META.get("REMOTE_ADDR")
user.last_login_uagent = self.request.META.get("HTTP_USER_AGENT")
user.token_updated_at = timezone.now()
user.save()
return user
def complete_login_or_signup(self):
# Get email
email = self.user_data.get("email")
# Sanitize email
email = self.sanitize_email(email)
# Check if the user is present
user = User.objects.filter(email=email).first()
# Check if sign up case or login
is_signup = bool(user)
# If user is not present, create a new user
if not user:
# New user
self.__check_signup(email)
# Initialize user
(ENABLE_SIGNUP,) = get_configuration_value(
[
{
"key": "ENABLE_SIGNUP",
"default": os.environ.get("ENABLE_SIGNUP", "1"),
},
]
)
if (
ENABLE_SIGNUP == "0"
and not WorkspaceMemberInvite.objects.filter(
email=email,
).exists()
):
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["SIGNUP_DISABLED"],
error_message="SIGNUP_DISABLED",
payload={"email": email},
)
user = User(email=email, username=uuid.uuid4().hex)
# Check if password is autoset
if self.user_data.get("user").get("is_password_autoset"):
user.set_password(uuid.uuid4().hex)
user.is_password_autoset = True
user.is_email_verified = True
# Validate password
else:
# Validate password
self.validate_password(email)
# Set password
results = zxcvbn(self.code)
if results["score"] < 3:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INVALID_PASSWORD"
],
error_message="INVALID_PASSWORD",
payload={"email": email},
)
user.set_password(self.code)
user.is_password_autoset = False
# Set user details
avatar = self.user_data.get("user", {}).get("avatar", "")
first_name = self.user_data.get("user", {}).get("first_name", "")
last_name = self.user_data.get("user", {}).get("last_name", "")
@@ -164,8 +100,6 @@ class Adapter:
user.first_name = first_name if first_name else ""
user.last_name = last_name if last_name else ""
user.save()
# Create profile
Profile.objects.create(user=user)
if not user.is_active:
@@ -174,10 +108,15 @@ class Adapter:
error_message="USER_ACCOUNT_DEACTIVATED",
)
# Save user data
user = self.save_user_data(user=user)
# Update user details
user.last_login_medium = self.provider
user.last_active = timezone.now()
user.last_login_time = timezone.now()
user.last_login_ip = self.request.META.get("REMOTE_ADDR")
user.last_login_uagent = self.request.META.get("HTTP_USER_AGENT")
user.token_updated_at = timezone.now()
user.save()
# Call callback if present
if self.callback:
self.callback(
user,
@@ -185,9 +124,7 @@ class Adapter:
self.request,
)
# Create or update account if token data is present
if self.token_data:
self.create_update_account(user=user)
# Return user
return user
@@ -33,13 +33,10 @@ AUTHENTICATION_ERROR_CODES = {
"EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN": 5100,
"EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP": 5102,
# Oauth
"OAUTH_NOT_CONFIGURED": 5104,
"GOOGLE_NOT_CONFIGURED": 5105,
"GITHUB_NOT_CONFIGURED": 5110,
"GITLAB_NOT_CONFIGURED": 5111,
"GOOGLE_OAUTH_PROVIDER_ERROR": 5115,
"GITHUB_OAUTH_PROVIDER_ERROR": 5120,
"GITLAB_OAUTH_PROVIDER_ERROR": 5121,
# Reset Password
"INVALID_PASSWORD_TOKEN": 5125,
"EXPIRED_PASSWORD_TOKEN": 5130,
@@ -61,8 +58,6 @@ AUTHENTICATION_ERROR_CODES = {
"ADMIN_USER_DEACTIVATED": 5190,
# Rate limit
"RATE_LIMIT_EXCEEDED": 5900,
# Unknown
"AUTHENTICATION_FAILED": 5999,
}
+10 -10
View File
@@ -62,7 +62,11 @@ class OauthAdapter(Adapter):
response.raise_for_status()
return response.json()
except requests.RequestException:
code = self._provider_error_code()
code = (
"GOOGLE_OAUTH_PROVIDER_ERROR"
if self.provider == "google"
else "GITHUB_OAUTH_PROVIDER_ERROR"
)
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[code],
error_message=str(code),
@@ -77,15 +81,11 @@ class OauthAdapter(Adapter):
response.raise_for_status()
return response.json()
except requests.RequestException:
if self.provider == "google":
code = "GOOGLE_OAUTH_PROVIDER_ERROR"
elif self.provider == "github":
code = "GITHUB_OAUTH_PROVIDER_ERROR"
elif self.provider == "gitlab":
code = "GITLAB_OAUTH_PROVIDER_ERROR"
else:
code = "OAUTH_NOT_CONFIGURED"
code = (
"GOOGLE_OAUTH_PROVIDER_ERROR"
if self.provider == "google"
else "GITHUB_OAUTH_PROVIDER_ERROR"
)
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[code],
error_message=str(code),
@@ -1,145 +0,0 @@
# Python imports
import os
from datetime import datetime
from urllib.parse import urlencode
import pytz
# Module imports
from plane.authentication.adapter.oauth import OauthAdapter
from plane.license.utils.instance_value import get_configuration_value
from plane.authentication.adapter.error import (
AuthenticationException,
AUTHENTICATION_ERROR_CODES,
)
class GitLabOAuthProvider(OauthAdapter):
(GITLAB_HOST,) = get_configuration_value(
[
{
"key": "GITLAB_HOST",
"default": os.environ.get("GITLAB_HOST", "https://gitlab.com"),
},
]
)
if not GITLAB_HOST:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["GITLAB_NOT_CONFIGURED"],
error_message="GITLAB_NOT_CONFIGURED",
)
host = GITLAB_HOST
token_url = (
f"{host}/oauth/token"
)
userinfo_url = (
f"{host}/api/v4/user"
)
provider = "gitlab"
scope = "read_user"
def __init__(self, request, code=None, state=None, callback=None):
GITLAB_CLIENT_ID, GITLAB_CLIENT_SECRET = get_configuration_value(
[
{
"key": "GITLAB_CLIENT_ID",
"default": os.environ.get("GITLAB_CLIENT_ID"),
},
{
"key": "GITLAB_CLIENT_SECRET",
"default": os.environ.get("GITLAB_CLIENT_SECRET"),
},
]
)
if not (GITLAB_CLIENT_ID and GITLAB_CLIENT_SECRET):
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["GITLAB_NOT_CONFIGURED"],
error_message="GITLAB_NOT_CONFIGURED",
)
client_id = GITLAB_CLIENT_ID
client_secret = GITLAB_CLIENT_SECRET
redirect_uri = f"""{"https" if request.is_secure() else "http"}://{request.get_host()}/auth/gitlab/callback/"""
url_params = {
"client_id": client_id,
"redirect_uri": redirect_uri,
"response_type": "code",
"scope": self.scope,
"state": state,
}
auth_url = (
f"{self.host}/oauth/authorize?{urlencode(url_params)}"
)
super().__init__(
request,
self.provider,
client_id,
self.scope,
redirect_uri,
auth_url,
self.token_url,
self.userinfo_url,
client_secret,
code,
callback=callback,
)
def set_token_data(self):
data = {
"client_id": self.client_id,
"client_secret": self.client_secret,
"code": self.code,
"redirect_uri": self.redirect_uri,
"grant_type": "authorization_code"
}
token_response = self.get_user_token(
data=data, headers={"Accept": "application/json"}
)
super().set_token_data(
{
"access_token": token_response.get("access_token"),
"refresh_token": token_response.get("refresh_token", None),
"access_token_expired_at": (
datetime.fromtimestamp(
token_response.get("created_at") + token_response.get("expires_in"),
tz=pytz.utc,
)
if token_response.get("expires_in")
else None
),
"refresh_token_expired_at": (
datetime.fromtimestamp(
token_response.get("refresh_token_expired_at"),
tz=pytz.utc,
)
if token_response.get("refresh_token_expired_at")
else None
),
"id_token": token_response.get("id_token", ""),
}
)
def set_user_data(self):
user_info_response = self.get_user_response()
email = user_info_response.get("email")
super().set_user_data(
{
"email": email,
"user": {
"provider_id": user_info_response.get("id"),
"email": email,
"avatar": user_info_response.get("avatar_url"),
"first_name": user_info_response.get("name"),
"last_name": user_info_response.get("family_name"),
"is_password_autoset": True,
},
}
)
-25
View File
@@ -8,8 +8,6 @@ from .views import (
ChangePasswordEndpoint,
# App
EmailCheckEndpoint,
GitLabCallbackEndpoint,
GitLabOauthInitiateEndpoint,
GitHubCallbackEndpoint,
GitHubOauthInitiateEndpoint,
GoogleCallbackEndpoint,
@@ -24,8 +22,6 @@ from .views import (
ResetPasswordSpaceEndpoint,
# Space
EmailCheckSpaceEndpoint,
GitLabCallbackSpaceEndpoint,
GitLabOauthInitiateSpaceEndpoint,
GitHubCallbackSpaceEndpoint,
GitHubOauthInitiateSpaceEndpoint,
GoogleCallbackSpaceEndpoint,
@@ -155,27 +151,6 @@ urlpatterns = [
GitHubCallbackSpaceEndpoint.as_view(),
name="github-callback",
),
## Gitlab Oauth
path(
"gitlab/",
GitLabOauthInitiateEndpoint.as_view(),
name="gitlab-initiate",
),
path(
"gitlab/callback/",
GitLabCallbackEndpoint.as_view(),
name="gitlab-callback",
),
path(
"spaces/gitlab/",
GitLabOauthInitiateSpaceEndpoint.as_view(),
name="gitlab-initiate",
),
path(
"spaces/gitlab/callback/",
GitLabCallbackSpaceEndpoint.as_view(),
name="gitlab-callback",
),
# Email Check
path(
"email-check/",
@@ -4,7 +4,6 @@ from plane.db.models import (
WorkspaceMember,
WorkspaceMemberInvite,
)
from plane.utils.cache import invalidate_cache_directly
def process_workspace_project_invitations(user):
@@ -27,16 +26,6 @@ def process_workspace_project_invitations(user):
ignore_conflicts=True,
)
[
invalidate_cache_directly(
path=f"/api/workspaces/{str(workspace_member_invite.workspace.slug)}/members/",
url_params=False,
user=False,
multiple=True,
)
for workspace_member_invite in workspace_member_invites
]
# Check if user has any project invites
project_member_invites = ProjectMemberInvite.objects.filter(
email=user.email, accepted=True
@@ -14,10 +14,6 @@ from .app.github import (
GitHubCallbackEndpoint,
GitHubOauthInitiateEndpoint,
)
from .app.gitlab import (
GitLabCallbackEndpoint,
GitLabOauthInitiateEndpoint,
)
from .app.google import (
GoogleCallbackEndpoint,
GoogleOauthInitiateEndpoint,
@@ -38,11 +34,6 @@ from .space.github import (
GitHubOauthInitiateSpaceEndpoint,
)
from .space.gitlab import (
GitLabCallbackSpaceEndpoint,
GitLabOauthInitiateSpaceEndpoint,
)
from .space.google import (
GoogleCallbackSpaceEndpoint,
GoogleOauthInitiateSpaceEndpoint,
@@ -1,131 +0,0 @@
import uuid
from urllib.parse import urlencode, urljoin
# Django import
from django.http import HttpResponseRedirect
from django.views import View
# Module imports
from plane.authentication.provider.oauth.gitlab import GitLabOAuthProvider
from plane.authentication.utils.login import user_login
from plane.authentication.utils.redirection_path import get_redirection_path
from plane.authentication.utils.user_auth_workflow import (
post_user_auth_workflow,
)
from plane.license.models import Instance
from plane.authentication.utils.host import base_host
from plane.authentication.adapter.error import (
AuthenticationException,
AUTHENTICATION_ERROR_CODES,
)
class GitLabOauthInitiateEndpoint(View):
def get(self, request):
# Get host and next path
request.session["host"] = base_host(request=request, is_app=True)
next_path = request.GET.get("next_path")
if next_path:
request.session["next_path"] = str(next_path)
# Check instance configuration
instance = Instance.objects.first()
if instance is None or not instance.is_setup_done:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INSTANCE_NOT_CONFIGURED"
],
error_message="INSTANCE_NOT_CONFIGURED",
)
params = exc.get_error_dict()
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request, is_app=True),
"?" + urlencode(params),
)
return HttpResponseRedirect(url)
try:
state = uuid.uuid4().hex
provider = GitLabOAuthProvider(request=request, state=state)
request.session["state"] = state
auth_url = provider.get_auth_url()
return HttpResponseRedirect(auth_url)
except AuthenticationException as e:
params = e.get_error_dict()
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request, is_app=True),
"?" + urlencode(params),
)
return HttpResponseRedirect(url)
class GitLabCallbackEndpoint(View):
def get(self, request):
code = request.GET.get("code")
state = request.GET.get("state")
base_host = request.session.get("host")
next_path = request.session.get("next_path")
if state != request.session.get("state", ""):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"GITLAB_OAUTH_PROVIDER_ERROR"
],
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
)
params = exc.get_error_dict()
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host,
"?" + urlencode(params),
)
return HttpResponseRedirect(url)
if not code:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"GITLAB_OAUTH_PROVIDER_ERROR"
],
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
)
params = exc.get_error_dict()
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host,
"?" + urlencode(params),
)
return HttpResponseRedirect(url)
try:
provider = GitLabOAuthProvider(
request=request,
code=code,
callback=post_user_auth_workflow,
)
user = provider.authenticate()
# Login the user and record his device info
user_login(request=request, user=user, is_app=True)
# Get the redirection path
if next_path:
path = next_path
else:
path = get_redirection_path(user=user)
# redirect to referer path
url = urljoin(base_host, path)
return HttpResponseRedirect(url)
except AuthenticationException as e:
params = e.get_error_dict()
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host,
"?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -1,109 +0,0 @@
# Python imports
import uuid
from urllib.parse import urlencode
# Django import
from django.http import HttpResponseRedirect
from django.views import View
# Module imports
from plane.authentication.provider.oauth.gitlab import GitLabOAuthProvider
from plane.authentication.utils.login import user_login
from plane.license.models import Instance
from plane.authentication.utils.host import base_host
from plane.authentication.adapter.error import (
AUTHENTICATION_ERROR_CODES,
AuthenticationException,
)
class GitLabOauthInitiateSpaceEndpoint(View):
def get(self, request):
# Get host and next path
request.session["host"] = base_host(request=request, is_space=True)
next_path = request.GET.get("next_path")
if next_path:
request.session["next_path"] = str(next_path)
# Check instance configuration
instance = Instance.objects.first()
if instance is None or not instance.is_setup_done:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INSTANCE_NOT_CONFIGURED"
],
error_message="INSTANCE_NOT_CONFIGURED",
)
params = exc.get_error_dict()
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)
try:
state = uuid.uuid4().hex
provider = GitLabOAuthProvider(request=request, state=state)
request.session["state"] = state
auth_url = provider.get_auth_url()
return HttpResponseRedirect(auth_url)
except AuthenticationException as e:
params = e.get_error_dict()
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)
class GitLabCallbackSpaceEndpoint(View):
def get(self, request):
code = request.GET.get("code")
state = request.GET.get("state")
base_host = request.session.get("host")
next_path = request.session.get("next_path")
if state != request.session.get("state", ""):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"GITLAB_OAUTH_PROVIDER_ERROR"
],
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
)
params = exc.get_error_dict()
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)
if not code:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"GITLAB_OAUTH_PROVIDER_ERROR"
],
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
)
params = exc.get_error_dict()
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)
try:
provider = GitLabOAuthProvider(
request=request,
code=code,
)
user = provider.authenticate()
# Login the user and record his device info
user_login(request=request, user=user, is_space=True)
# Process workspace and project invitations
# redirect to referer path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)
except AuthenticationException as e:
params = e.get_error_dict()
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)
@@ -1,23 +0,0 @@
# Generated by Django 4.2.11 on 2024-06-03 17:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0066_account_id_token_cycle_logo_props_module_logo_props'),
]
operations = [
migrations.AlterField(
model_name='account',
name='provider',
field=models.CharField(choices=[('google', 'Google'), ('github', 'Github'), ('gitlab', 'GitLab')]),
),
migrations.AlterField(
model_name='socialloginconnection',
name='medium',
field=models.CharField(choices=[('Google', 'google'), ('Github', 'github'), ('GitLab', 'gitlab'), ('Jira', 'jira')], default=None, max_length=20),
),
]
-1
View File
@@ -12,7 +12,6 @@ from .base import BaseModel
def get_upload_path(instance, filename):
filename = filename[:50]
if instance.workspace_id is not None:
return f"{instance.workspace.id}/{uuid4().hex}-{filename}"
return f"user-{uuid4().hex}-{filename}"
@@ -10,7 +10,7 @@ from .base import BaseModel
class SocialLoginConnection(BaseModel):
medium = models.CharField(
max_length=20,
choices=(("Google", "google"), ("Github", "github"), ("GitLab", "gitlab"), ("Jira", "jira")),
choices=(("Google", "google"), ("Github", "github"), ("Jira", "jira")),
default=None,
)
last_login_at = models.DateTimeField(default=timezone.now, null=True)
+1 -1
View File
@@ -182,7 +182,7 @@ class Account(TimeAuditModel):
)
provider_account_id = models.CharField(max_length=255)
provider = models.CharField(
choices=(("google", "Google"), ("github", "Github"), ("gitlab", "GitLab")),
choices=(("google", "Google"), ("github", "Github")),
)
access_token = models.TextField()
access_token_expired_at = models.DateTimeField(null=True)
@@ -54,7 +54,6 @@ class InstanceEndpoint(BaseAPIView):
IS_GOOGLE_ENABLED,
IS_GITHUB_ENABLED,
GITHUB_APP_NAME,
IS_GITLAB_ENABLED,
EMAIL_HOST,
ENABLE_MAGIC_LINK_LOGIN,
ENABLE_EMAIL_PASSWORD,
@@ -77,10 +76,6 @@ class InstanceEndpoint(BaseAPIView):
"key": "GITHUB_APP_NAME",
"default": os.environ.get("GITHUB_APP_NAME", ""),
},
{
"key": "IS_GITLAB_ENABLED",
"default": os.environ.get("IS_GITLAB_ENABLED", "0"),
},
{
"key": "EMAIL_HOST",
"default": os.environ.get("EMAIL_HOST", ""),
@@ -120,7 +115,6 @@ class InstanceEndpoint(BaseAPIView):
# Authentication
data["is_google_enabled"] = IS_GOOGLE_ENABLED == "1"
data["is_github_enabled"] = IS_GITHUB_ENABLED == "1"
data["is_gitlab_enabled"] = IS_GITLAB_ENABLED == "1"
data["is_magic_login_enabled"] = ENABLE_MAGIC_LINK_LOGIN == "1"
data["is_email_password_enabled"] = ENABLE_EMAIL_PASSWORD == "1"
@@ -59,24 +59,6 @@ class Command(BaseCommand):
"category": "GITHUB",
"is_encrypted": True,
},
{
"key": "GITLAB_HOST",
"value": os.environ.get("GITLAB_HOST"),
"category": "GITLAB",
"is_encrypted": False,
},
{
"key": "GITLAB_CLIENT_ID",
"value": os.environ.get("GITLAB_CLIENT_ID"),
"category": "GITLAB",
"is_encrypted": False,
},
{
"key": "GITLAB_CLIENT_SECRET",
"value": os.environ.get("GITLAB_CLIENT_SECRET"),
"category": "GITLAB",
"is_encrypted": True,
},
{
"key": "EMAIL_HOST",
"value": os.environ.get("EMAIL_HOST", ""),
@@ -163,7 +145,7 @@ class Command(BaseCommand):
)
)
keys = ["IS_GOOGLE_ENABLED", "IS_GITHUB_ENABLED", "IS_GITLAB_ENABLED"]
keys = ["IS_GOOGLE_ENABLED", "IS_GITHUB_ENABLED"]
if not InstanceConfiguration.objects.filter(key__in=keys).exists():
for key in keys:
if key == "IS_GOOGLE_ENABLED":
@@ -234,46 +216,6 @@ class Command(BaseCommand):
f"{key} loaded with value from environment variable."
)
)
if key == "IS_GITLAB_ENABLED":
GITLAB_HOST, GITLAB_CLIENT_ID, GITLAB_CLIENT_SECRET = (
get_configuration_value(
[
{
"key": "GITLAB_HOST",
"default": os.environ.get(
"GITLAB_HOST", "https://gitlab.com"
),
},
{
"key": "GITLAB_CLIENT_ID",
"default": os.environ.get(
"GITLAB_CLIENT_ID", ""
),
},
{
"key": "GITLAB_CLIENT_SECRET",
"default": os.environ.get(
"GITLAB_CLIENT_SECRET", ""
),
},
]
)
)
if bool(GITLAB_HOST) and bool(GITLAB_CLIENT_ID) and bool(GITLAB_CLIENT_SECRET):
value = "1"
else:
value = "0"
InstanceConfiguration.objects.create(
key="IS_GITLAB_ENABLED",
value=value,
category="AUTHENTICATION",
is_encrypted=False,
)
self.stdout.write(
self.style.SUCCESS(
f"{key} loaded with value from environment variable."
)
)
else:
for key in keys:
self.stdout.write(
@@ -1,43 +0,0 @@
# Generated by Django 4.2.11 on 2024-06-05 13:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("license", "0002_rename_version_instance_current_version_and_more"),
]
operations = [
migrations.AlterField(
model_name="changelog",
name="title",
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name="changelog",
name="version",
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name="instance",
name="current_version",
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name="instance",
name="latest_version",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name="instance",
name="namespace",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name="instance",
name="product",
field=models.CharField(default="plane-ce", max_length=255),
),
]
+6 -6
View File
@@ -21,15 +21,15 @@ class Instance(BaseModel):
whitelist_emails = models.TextField(blank=True, null=True)
instance_id = models.CharField(max_length=255, unique=True)
license_key = models.CharField(max_length=256, null=True, blank=True)
current_version = models.CharField(max_length=255)
latest_version = models.CharField(max_length=255, null=True, blank=True)
current_version = models.CharField(max_length=10)
latest_version = models.CharField(max_length=10, null=True, blank=True)
product = models.CharField(
max_length=255, default=ProductTypes.PLANE_CE.value
max_length=50, default=ProductTypes.PLANE_CE.value
)
domain = models.TextField(blank=True)
# Instance specifics
last_checked_at = models.DateTimeField()
namespace = models.CharField(max_length=255, blank=True, null=True)
namespace = models.CharField(max_length=50, blank=True, null=True)
# telemetry and support
is_telemetry_enabled = models.BooleanField(default=True)
is_support_required = models.BooleanField(default=True)
@@ -86,9 +86,9 @@ class InstanceConfiguration(BaseModel):
class ChangeLog(BaseModel):
"""Change Log model to store the release changelogs made in the application."""
title = models.CharField(max_length=255)
title = models.CharField(max_length=100)
description = models.TextField(blank=True)
version = models.CharField(max_length=255)
version = models.CharField(max_length=100)
tags = models.JSONField(default=list)
release_date = models.DateTimeField(null=True)
is_release_candidate = models.BooleanField(default=False)
+1 -1
View File
@@ -66,7 +66,7 @@ def invalidate_cache_directly(
custom_path = path if path is not None else request.get_full_path()
auth_header = (
None
if request and request.user.is_anonymous
if request.user.is_anonymous
else str(request.user.id) if user else None
)
key = generate_cache_key(custom_path, auth_header)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"repository": "https://github.com/makeplane/plane.git",
"version": "0.21.0",
"version": "0.20.0",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.21.0",
"version": "0.20.0",
"private": true,
"main": "./src/index.ts",
"exports": {
-15
View File
@@ -60,13 +60,10 @@ export enum EAuthErrorCodes {
EXPIRED_MAGIC_CODE = "5095",
EMAIL_CODE_ATTEMPT_EXHAUSTED = "5100",
// Oauth
OAUTH_NOT_CONFIGURED = "5104",
GOOGLE_NOT_CONFIGURED = "5105",
GITHUB_NOT_CONFIGURED = "5110",
GITLAB_NOT_CONFIGURED = "5111",
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
GITLAB_OAUTH_PROVIDER_ERROR = "5121",
// Reset Password
INVALID_PASSWORD_TOKEN = "5125",
EXPIRED_PASSWORD_TOKEN = "5130",
@@ -218,10 +215,6 @@ const errorCodeMessages: {
},
// Oauth
[EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED]: {
title: `OAuth not configured`,
message: () => `OAuth not configured. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
title: `Google not configured`,
message: () => `Google not configured. Please contact your administrator.`,
@@ -230,10 +223,6 @@ const errorCodeMessages: {
title: `GitHub not configured`,
message: () => `GitHub not configured. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED]: {
title: `GitLab not configured`,
message: () => `GitLab not configured. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: {
title: `Google OAuth provider error`,
message: () => `Google OAuth provider error. Please try again.`,
@@ -242,10 +231,6 @@ const errorCodeMessages: {
title: `GitHub OAuth provider error`,
message: () => `GitHub OAuth provider error. Please try again.`,
},
[EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR]: {
title: `GitLab OAuth provider error`,
message: () => `GitLab OAuth provider error. Please try again.`,
},
// Reset Password
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor-core",
"version": "0.21.0",
"version": "0.20.0",
"description": "Core Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
@@ -112,7 +112,7 @@ export const useEditor = ({
if (value === null || value === undefined) return;
if (editor && !editor.isDestroyed && !editor.storage.image.uploadInProgress) {
try {
editor.commands.setContent(value, false, { preserveWhitespace: "full" });
editor.commands.setContent(value);
const currentSavedSelection = savedSelectionRef.current;
if (currentSavedSelection) {
const docLength = editor.state.doc.content.size;
@@ -3,7 +3,6 @@ import { startImageUpload } from "src/ui/plugins/image/image-upload-handler";
import { findTableAncestor } from "src/lib/utils";
import { Selection } from "@tiptap/pm/state";
import { UploadImage } from "src/types/upload-image";
import { replaceCodeWithText } from "src/ui/extensions/code/utils/replace-code-block-with-text";
export const setText = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).clearNodes().run();
@@ -55,11 +54,69 @@ export const toggleUnderline = (editor: Editor, range?: Range) => {
else editor.chain().focus().toggleUnderline().run();
};
const replaceCodeBlockWithContent = (editor: Editor) => {
try {
const { schema } = editor.state;
const { paragraph } = schema.nodes;
let replaced = false;
const replaceCodeBlock = (from: number, to: number, textContent: string) => {
const docSize = editor.state.doc.content.size;
if (from < 0 || to > docSize || from > to) {
console.error("Invalid range for replacement: ", from, to, "in a document of size", docSize);
return;
}
// split the textContent by new lines to handle each line as a separate paragraph
const lines = textContent.split(/\r?\n/);
const tr = editor.state.tr;
// Calculate the position for inserting the first paragraph
let insertPos = from;
// Remove the code block first
tr.delete(from, to);
// For each line, create a paragraph node and insert it
lines.forEach((line) => {
const paragraphNode = paragraph.create({}, schema.text(line));
tr.insert(insertPos, paragraphNode);
// Update insertPos for the next insertion
insertPos += paragraphNode.nodeSize;
});
// Dispatch the transaction
editor.view.dispatch(tr);
replaced = true;
};
editor.state.doc.nodesBetween(editor.state.selection.from, editor.state.selection.to, (node, pos) => {
if (node.type === schema.nodes.codeBlock) {
const startPos = pos;
const endPos = pos + node.nodeSize;
const textContent = node.textContent;
if (textContent.length === 0) {
editor.chain().focus().toggleCodeBlock().run();
}
replaceCodeBlock(startPos, endPos, textContent);
return false;
}
});
if (!replaced) {
console.log("No code block to replace.");
}
} catch (error) {
console.error("An error occurred while replacing code block content:", error);
}
};
export const toggleCodeBlock = (editor: Editor, range?: Range) => {
try {
// if it's a code block, replace it with the code with paragraphs
if (editor.isActive("codeBlock")) {
replaceCodeWithText(editor);
replaceCodeBlockWithContent(editor);
return;
}
@@ -67,16 +124,11 @@ export const toggleCodeBlock = (editor: Editor, range?: Range) => {
const text = editor.state.doc.textBetween(from, to, "\n");
const isMultiline = text.includes("\n");
// if the selection is not a range i.e. empty, then simply convert it into a code block
if (editor.state.selection.empty) {
editor.chain().focus().toggleCodeBlock().run();
} else if (isMultiline) {
// if the selection is multiline, then also replace the text content with
// a code block
editor.chain().focus().deleteRange({ from, to }).insertContentAt(from, `\`\`\`\n${text}\n\`\`\``).run();
} else {
// if the selection is single line, then simply convert it into inline
// code
editor.chain().focus().toggleCode().run();
}
} catch (error) {
@@ -110,11 +110,6 @@ ul[data-type="taskList"] li > label input[type="checkbox"]:checked:hover {
}
}
/* the p tag just after the ul tag */
ul[data-type="taskList"] + p {
margin-top: 0.4rem !important;
}
ul[data-type="taskList"] li > label input[type="checkbox"] {
position: relative;
-webkit-appearance: none;
@@ -157,10 +152,6 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
}
}
ul[data-type="taskList"] li > div > p {
margin-top: 10px;
}
ul[data-type="taskList"] li[data-checked="true"] > div > p {
color: rgb(var(--color-text-400));
text-decoration: line-through;
@@ -33,7 +33,7 @@ export const CustomCodeInlineExtension = Mark.create<CodeOptions>({
return {
HTMLAttributes: {
class:
"rounded bg-custom-background-80 px-1 py-[2px] font-mono font-medium text-orange-500 border-[0.5px] border-custom-border-200",
"rounded bg-custom-background-80 px-1 py-[2px] font-mono font-medium text-orange-500 border-[0.5px] border-custom-border-200 text-sm",
spellcheck: "false",
},
};
@@ -1,124 +0,0 @@
import { Editor, findParentNode } from "@tiptap/core";
type ReplaceCodeBlockParams = {
editor: Editor;
from: number;
to: number;
textContent: string;
cursorPosInsideCodeblock: number;
};
export function replaceCodeWithText(editor: Editor): void {
try {
const { from, to } = editor.state.selection;
const cursorPosInsideCodeblock = from;
let replaced = false;
editor.state.doc.nodesBetween(from, to, (node, pos) => {
if (node.type === editor.state.schema.nodes.codeBlock) {
const startPos = pos;
const endPos = pos + node.nodeSize;
const textContent = node.textContent;
if (textContent.length === 0) {
editor.chain().focus().toggleCodeBlock().run();
} else {
transformCodeBlockToParagraphs({
editor,
from: startPos,
to: endPos,
textContent,
cursorPosInsideCodeblock,
});
}
replaced = true;
return false;
}
});
if (!replaced) {
console.log("No code block to replace.");
}
} catch (error) {
console.error("An error occurred while replacing code block content:", error);
}
}
function transformCodeBlockToParagraphs({
editor,
from,
to,
textContent,
cursorPosInsideCodeblock,
}: ReplaceCodeBlockParams): void {
const { schema } = editor.state;
const { paragraph } = schema.nodes;
const docSize = editor.state.doc.content.size;
if (from < 0 || to > docSize || from > to) {
console.error("Invalid range for replacement: ", from, to, "in a document of size", docSize);
return;
}
// Split the textContent by new lines to handle each line as a separate paragraph for Windows (\r\n) and Unix (\n)
const lines = textContent.split(/\r?\n/);
const tr = editor.state.tr;
let insertPos = from;
// Remove the code block first
tr.delete(from, to);
// For each line, create a paragraph node and insert it
lines.forEach((line) => {
// if the line is empty, create a paragraph node with no content
const paragraphNode = line.length === 0 ? paragraph.create({}) : paragraph.create({}, schema.text(line));
tr.insert(insertPos, paragraphNode);
insertPos += paragraphNode.nodeSize;
});
// Now persist the focus to the converted paragraph
const parentNodeOffset = findParentNode((node) => node.type === schema.nodes.codeBlock)(editor.state.selection)?.pos;
if (parentNodeOffset === undefined) throw new Error("Invalid code block offset");
const lineNumber = getLineNumber(textContent, cursorPosInsideCodeblock, parentNodeOffset);
const cursorPosOutsideCodeblock = cursorPosInsideCodeblock + (lineNumber - 1);
editor.view.dispatch(tr);
editor.chain().focus(cursorPosOutsideCodeblock).run();
}
/**
* Calculates the line number where the cursor is located inside the code block.
* Assumes the indexing of the content inside the code block is like ProseMirror's indexing.
*
* @param {string} textContent - The content of the code block.
* @param {number} cursorPosition - The absolute cursor position in the document.
* @param {number} codeBlockNodePos - The starting position of the code block node in the document.
* @returns {number} The 1-based line number where the cursor is located.
*/
function getLineNumber(textContent: string, cursorPosition: number, codeBlockNodePos: number): number {
// Split the text content into lines, handling both Unix and Windows newlines
const lines = textContent.split(/\r?\n/);
const cursorPosInsideCodeblockRelative = cursorPosition - codeBlockNodePos;
let startPosition = 0;
let lineNumber = 0;
for (let i = 0; i < lines.length; i++) {
// Calculate the end position of the current line
const endPosition = startPosition + lines[i].length + 1; // +1 for the newline character
// Check if the cursor position is within the current line
if (cursorPosInsideCodeblockRelative >= startPosition && cursorPosInsideCodeblockRelative <= endPosition) {
lineNumber = i + 1; // Line numbers are 1-based
break;
}
// Update the start position for the next line
startPosition = endPosition;
}
return lineNumber;
}
@@ -72,7 +72,7 @@ const getPrevListDepth = (typeOrName: string, state: EditorState) => {
// Traverse up the document structure from the adjusted position
for (let d = resolvedPos.depth; d > 0; d--) {
const node = resolvedPos.node(d);
if (node.type.name === "bulletList" || node.type.name === "orderedList" || node.type.name === "taskList") {
if (node.type.name === "bulletList" || node.type.name === "orderedList") {
// Increment depth for each list ancestor found
depth++;
}
@@ -146,8 +146,6 @@ export const handleBackspace = (editor: Editor, name: string, parentListTypes: s
if (!isAtStartOfNode(editor.state)) {
return false;
}
// is the paragraph node inside of the current list item (maybe with a hard break)
const isParaSibling = isCurrentParagraphASibling(editor.state);
const isCurrentListItemSublist = prevListIsHigher(name, editor.state);
const listItemPos = findListItemPos(name, editor.state);
@@ -308,10 +306,7 @@ const isCurrentParagraphASibling = (state: EditorState): boolean => {
const currentParagraphNode = $from.parent; // Get the current node where the selection is.
// Ensure we're in a paragraph and the parent is a list item.
if (
currentParagraphNode.type.name === "paragraph" &&
(listItemNode.type.name === "listItem" || listItemNode.type.name === "taskItem")
) {
if (currentParagraphNode.type.name === "paragraph" && listItemNode.type.name === "listItem") {
let paragraphNodesCount = 0;
listItemNode.forEach((child) => {
if (child.type.name === "paragraph") {
@@ -332,19 +327,16 @@ export function isCursorInSubList(editor: Editor) {
// Check if the current node is a list item
const listItem = editor.schema.nodes.listItem;
const taskItem = editor.schema.nodes.taskItem;
// Traverse up the document tree from the current position
for (let depth = $from.depth; depth > 0; depth--) {
const node = $from.node(depth);
if (node.type === listItem || node.type === taskItem) {
if (node.type === listItem) {
// If the parent of the list item is also a list, it's a sub-list
const parent = $from.node(depth - 1);
if (
parent &&
(parent.type === editor.schema.nodes.bulletList ||
parent.type === editor.schema.nodes.orderedList ||
parent.type === editor.schema.nodes.taskList)
(parent.type === editor.schema.nodes.bulletList || parent.type === editor.schema.nodes.orderedList)
) {
return true;
}
@@ -69,7 +69,7 @@ export const ListKeymap = ({ tabIndex }: { tabIndex?: number }) =>
return handled;
} catch (e) {
console.log("Error in handling Delete:", e);
console.log("error in handling Backspac:", e);
return false;
}
},
@@ -104,7 +104,7 @@ export const ListKeymap = ({ tabIndex }: { tabIndex?: number }) =>
return handled;
} catch (e) {
console.log("Error in handling Backspace:", e);
console.log("error in handling Backspac:", e);
return false;
}
},
@@ -15,7 +15,9 @@ declare module "@tiptap/core" {
}
}
function autoJoin(tr: Transaction, newTr: Transaction, nodeTypes: NodeType[]) {
function autoJoin(tr: Transaction, newTr: Transaction, nodeType: NodeType) {
if (!tr.isGeneric) return false;
// Find all ranges where we might want to join.
const ranges: Array<number> = [];
for (let i = 0; i < tr.mapping.maps.length; i++) {
@@ -26,7 +28,7 @@ function autoJoin(tr: Transaction, newTr: Transaction, nodeTypes: NodeType[]) {
// Figure out which joinable points exist inside those ranges,
// by checking all node boundaries in their parent nodes.
const joinable: number[] = [];
const joinable = [];
for (let i = 0; i < ranges.length; i += 2) {
const from = ranges[i],
to = ranges[i + 1];
@@ -38,7 +40,7 @@ function autoJoin(tr: Transaction, newTr: Transaction, nodeTypes: NodeType[]) {
if (!after) break;
if (index && joinable.indexOf(pos) == -1) {
const before = parent.child(index - 1);
if (before.type == after.type && nodeTypes.includes(before.type)) joinable.push(pos);
if (before.type == after.type && before.type === nodeType) joinable.push(pos);
}
pos += after.nodeSize;
}
@@ -86,15 +88,25 @@ export const CustomKeymap = Extension.create({
// Create a new transaction.
const newTr = newState.tr;
const joinableNodes = [
newState.schema.nodes["orderedList"],
newState.schema.nodes["taskList"],
newState.schema.nodes["bulletList"],
];
let joined = false;
for (const transaction of transactions) {
const anotherJoin = autoJoin(transaction, newTr, newState.schema.nodes["orderedList"]);
joined = anotherJoin || joined;
}
if (joined) {
return newTr;
}
},
}),
new Plugin({
key: new PluginKey("unordered-list-merging"),
appendTransaction(transactions, oldState, newState) {
// Create a new transaction.
const newTr = newState.tr;
let joined = false;
for (const transaction of transactions) {
const anotherJoin = autoJoin(transaction, newTr, joinableNodes);
const anotherJoin = autoJoin(transaction, newTr, newState.schema.nodes["bulletList"]);
joined = anotherJoin || joined;
}
if (joined) {
@@ -50,25 +50,9 @@ export async function startImageUpload(
};
try {
const fileNameTrimmed = trimFileName(file.name);
const fileWithTrimmedName = new File([file], fileNameTrimmed, { type: file.type });
const resolvedPos = view.state.doc.resolve(pos ?? 0);
const nodeBefore = resolvedPos.nodeBefore;
// if the image is at the start of the line i.e. when nodeBefore is null
if (nodeBefore === null) {
if (pos) {
// so that the image is not inserted at the next line, else incase the
// image is inserted at any line where there's some content, the
// position is kept as it is to be inserted at the next line
pos -= 1;
}
}
view.focus();
const src = await uploadAndValidateImage(fileWithTrimmedName, uploadFile);
const src = await uploadAndValidateImage(file, uploadFile);
if (src == null) {
throw new Error("Resolved image URL is undefined.");
@@ -128,14 +112,3 @@ async function uploadAndValidateImage(file: File, uploadFile: UploadImage): Prom
throw error;
}
}
function trimFileName(fileName: string, maxLength = 100) {
if (fileName.length > maxLength) {
const extension = fileName.split(".").pop();
const nameWithoutExtension = fileName.slice(0, -(extension?.length ?? 0 + 1));
const allowedNameLength = maxLength - (extension?.length ?? 0) - 1; // -1 for the dot
return `${nameWithoutExtension.slice(0, allowedNameLength)}.${extension}`;
}
return fileName;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/document-editor",
"version": "0.21.0",
"version": "0.20.0",
"description": "Package that powers Plane's Pages Editor",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor-extensions",
"version": "0.21.0",
"version": "0.20.0",
"description": "Package that powers Plane's Editor with extensions",
"private": true,
"main": "./dist/index.mjs",
@@ -69,6 +69,7 @@ const Command = Extension.create<SlashCommandOptions>({
return true;
},
allowSpaces: true,
},
};
},
@@ -1,6 +1,6 @@
{
"name": "@plane/lite-text-editor",
"version": "0.21.0",
"version": "0.20.0",
"description": "Package that powers Plane's Comment Editor",
"private": true,
"main": "./dist/index.mjs",
@@ -1,6 +1,6 @@
{
"name": "@plane/rich-text-editor",
"version": "0.21.0",
"version": "0.20.0",
"description": "Rich Text Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "eslint-config-custom",
"private": true,
"version": "0.21.0",
"version": "0.20.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tailwind-config-custom",
"version": "0.21.0",
"version": "0.20.0",
"description": "common tailwind configuration across monorepo",
"main": "index.js",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tsconfig",
"version": "0.21.0",
"version": "0.20.0",
"private": true,
"files": [
"base.json",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "0.21.0",
"version": "0.20.0",
"private": true,
"main": "./src/index.d.ts"
}
+1 -1
View File
@@ -4,7 +4,7 @@ export type TCurrentUserAccount = {
user: string | undefined;
provider_account_id: string | undefined;
provider: "google" | "github" | "gitlab" | string | undefined;
provider: "google" | "github" | string | undefined;
access_token: string | undefined;
access_token_expired_at: Date | undefined;
refresh_token: string | undefined;
+2 -9
View File
@@ -3,8 +3,7 @@ export type TInstanceAuthenticationMethodKeys =
| "ENABLE_MAGIC_LINK_LOGIN"
| "ENABLE_EMAIL_PASSWORD"
| "IS_GOOGLE_ENABLED"
| "IS_GITHUB_ENABLED"
| "IS_GITLAB_ENABLED";
| "IS_GITHUB_ENABLED";
export type TInstanceGoogleAuthenticationConfigurationKeys =
| "GOOGLE_CLIENT_ID"
@@ -14,15 +13,9 @@ export type TInstanceGithubAuthenticationConfigurationKeys =
| "GITHUB_CLIENT_ID"
| "GITHUB_CLIENT_SECRET";
export type TInstanceGitlabAuthenticationConfigurationKeys =
| "GITLAB_HOST"
| "GITLAB_CLIENT_ID"
| "GITLAB_CLIENT_SECRET";
type TInstanceAuthenticationConfigurationKeys =
| TInstanceGoogleAuthenticationConfigurationKeys
| TInstanceGithubAuthenticationConfigurationKeys
| TInstanceGitlabAuthenticationConfigurationKeys;
| TInstanceGithubAuthenticationConfigurationKeys;
export type TInstanceAuthenticationKeys =
| TInstanceAuthenticationMethodKeys
-1
View File
@@ -38,7 +38,6 @@ export interface IInstance {
export interface IInstanceConfig {
is_google_enabled: boolean;
is_github_enabled: boolean;
is_gitlab_enabled: boolean;
is_magic_login_enabled: boolean;
is_email_password_enabled: boolean;
github_app_name: string | undefined;
+1 -2
View File
@@ -5,7 +5,7 @@ import {
TStateGroups,
} from ".";
type TLoginMediums = "email" | "magic-code" | "github" | "gitlab" | "google";
type TLoginMediums = "email" | "magic-code" | "github" | "google";
export interface IUser {
id: string;
@@ -128,7 +128,6 @@ export interface IUserActivityResponse {
prev_page_results: boolean;
results: IIssueActivity[];
total_pages: number;
total_results: number;
}
export type UserAuth = {
+1 -4
View File
@@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
"version": "0.21.0",
"version": "0.20.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
@@ -20,15 +20,12 @@
"postcss": "postcss styles/globals.css -o styles/output.css --watch"
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.1.10",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@blueprintjs/core": "^4.16.3",
"@blueprintjs/popover2": "^1.13.3",
"@headlessui/react": "^1.7.17",
"@popperjs/core": "^2.11.8",
"clsx": "^2.0.0",
"emoji-picker-react": "^4.5.16",
"lodash": "^4.17.21",
"lucide-react": "^0.379.0",
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
+2 -11
View File
@@ -7,11 +7,10 @@ export type TControlLink = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
target?: string;
disabled?: boolean;
className?: string;
draggable?: boolean;
};
export const ControlLink = React.forwardRef<HTMLAnchorElement, TControlLink>((props, ref) => {
const { href, onClick, children, target = "_self", disabled = false, className, draggable = false, ...rest } = props;
const { href, onClick, children, target = "_self", disabled = false, className, ...rest } = props;
const LEFT_CLICK_EVENT_CODE = 0;
const handleOnClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
@@ -34,15 +33,7 @@ export const ControlLink = React.forwardRef<HTMLAnchorElement, TControlLink>((pr
if (disabled) return <>{children}</>;
return (
<a
href={href}
target={target}
onClick={handleOnClick}
{...rest}
ref={ref}
className={className}
draggable={draggable}
>
<a href={href} target={target} onClick={handleOnClick} {...rest} ref={ref} className={className}>
{children}
</a>
);
-44
View File
@@ -1,44 +0,0 @@
Below is a detailed list of the props included:
### Root Props
- value: string | string[]; - Current selected value.
- onChange: (value: string | string []) => void; - Callback function for handling value changes.
- options: TDropdownOption[] | undefined; - Array of options.
- onOpen?: () => void; - Callback function triggered when the dropdown opens.
- onClose?: () => void; - Callback function triggered when the dropdown closes.
- containerClassName?: (isOpen: boolean) => string; - Function to return the class name for the container based on the open state.
- tabIndex?: number; - Sets the tab index for the dropdown.
- placement?: Placement; - Determines the placement of the dropdown (e.g., top, bottom, left, right).
- disabled?: boolean; - Disables the dropdown if set to true.
---
### Button Props
- buttonContent?: (isOpen: boolean) => React.ReactNode; - Function to render the content of the button based on the open state.
- buttonContainerClassName?: string; - Class name for the button container.
- buttonClassName?: string; - Class name for the button itself.
---
### Input Props
- disableSearch?: boolean; - Disables the search input if set to true.
- inputPlaceholder?: string; - Placeholder text for the search input.
- inputClassName?: string; - Class name for the search input.
- inputIcon?: React.ReactNode; - Icon to be displayed in the search input.
- inputContainerClassName?: string; - Class name for the search input container.
---
### Options Props
- keyExtractor: (option: TDropdownOption) => string; - Function to extract the key from each option.
- optionsContainerClassName?: string; - Class name for the options container.
- queryArray: string[]; - Array of strings to be used for querying the options.
- sortByKey: string; - Key to sort the options by.
- firstItem?: (optionValue: string) => boolean; - Function to determine if an option should be the first item.
- renderItem?: ({ value, selected }: { value: string; selected: boolean }) => React.ReactNode; - Function to render each option.
- loader?: React.ReactNode; - Loader element to be displayed while options are being loaded.
- disableSorting?: boolean; - Disables sorting of the options if set to true.
---
These properties offer extensive control over the dropdown's behavior and presentation, making it a highly versatile component suitable for various scenarios.
@@ -1,38 +0,0 @@
import React, { Fragment } from "react";
// headless ui
import { Combobox } from "@headlessui/react";
// helper
import { cn } from "../../../helpers";
import { IMultiSelectDropdownButton, ISingleSelectDropdownButton } from "../dropdown";
export const DropdownButton: React.FC<IMultiSelectDropdownButton | ISingleSelectDropdownButton> = (props) => {
const {
isOpen,
buttonContent,
buttonClassName,
buttonContainerClassName,
handleOnClick,
value,
setReferenceElement,
disabled,
} = props;
return (
<Combobox.Button as={Fragment}>
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
{buttonContent ? <>{buttonContent(isOpen)}</> : <span className={cn("", buttonClassName)}>{value}</span>}
</button>
</Combobox.Button>
);
};
-4
View File
@@ -1,4 +0,0 @@
export * from "./input-search";
export * from "./button";
export * from "./options";
export * from "./loader";
@@ -1,58 +0,0 @@
import React, { FC, useEffect, useRef } from "react";
// headless ui
import { Combobox } from "@headlessui/react";
// icons
import { Search } from "lucide-react";
// helpers
import { cn } from "../../../helpers";
interface IInputSearch {
isOpen: boolean;
query: string;
updateQuery: (query: string) => void;
inputIcon?: React.ReactNode;
inputContainerClassName?: string;
inputClassName?: string;
inputPlaceholder?: string;
}
export const InputSearch: FC<IInputSearch> = (props) => {
const { isOpen, query, updateQuery, inputIcon, inputContainerClassName, inputClassName, inputPlaceholder } = props;
const inputRef = useRef<HTMLInputElement | null>(null);
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
updateQuery("");
}
};
useEffect(() => {
if (isOpen) {
inputRef.current && inputRef.current.focus();
}
}, [isOpen]);
return (
<div
className={cn(
"flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2",
inputContainerClassName
)}
>
{inputIcon ? <>{inputIcon}</> : <Search className="h-4 w-4 text-custom-text-300" aria-hidden="true" />}
<Combobox.Input
as="input"
ref={inputRef}
className={cn(
"w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none",
inputClassName
)}
value={query}
onChange={(e) => updateQuery(e.target.value)}
placeholder={inputPlaceholder ?? "Search"}
onKeyDown={searchInputKeyDown}
/>
</div>
);
};
@@ -1,9 +0,0 @@
import React from "react";
export const DropdownOptionsLoader = () => (
<div className="flex flex-col gap-1 animate-pulse">
{Array.from({ length: 6 }, (_, i) => (
<div key={i} className="flex h-[1.925rem] w-full rounded px-1 py-1.5 bg-custom-background-90" />
))}
</div>
);
@@ -1,88 +0,0 @@
import React from "react";
// headless ui
import { Combobox } from "@headlessui/react";
// icons
import { Check } from "lucide-react";
// components
import { DropdownOptionsLoader, InputSearch } from ".";
// helpers
import { cn } from "../../../helpers";
// types
import { IMultiSelectDropdownOptions, ISingleSelectDropdownOptions } from "../dropdown";
export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSelectDropdownOptions> = (props) => {
const {
isOpen,
query,
setQuery,
inputIcon,
inputPlaceholder,
inputClassName,
inputContainerClassName,
disableSearch,
keyExtractor,
options,
value,
renderItem,
loader,
} = props;
return (
<>
{!disableSearch && (
<InputSearch
isOpen={isOpen}
query={query}
updateQuery={(query) => setQuery(query)}
inputIcon={inputIcon}
inputPlaceholder={inputPlaceholder}
inputClassName={inputClassName}
inputContainerClassName={inputContainerClassName}
/>
)}
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
<>
{options ? (
options.length > 0 ? (
options?.map((option) => (
<Combobox.Option
key={keyExtractor(option)}
value={option.data[option.value]}
className={({ active, selected }) =>
cn(
"flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5",
{
"bg-custom-background-80": active,
"text-custom-text-100": selected,
"text-custom-text-200": !selected,
},
option.className && option.className({ active, selected })
)
}
>
{({ selected }) => (
<>
{renderItem ? (
<>{renderItem({ value: option.data[option.value], selected })}</>
) : (
<>
<span className="flex-grow truncate">{value}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</>
)}
</Combobox.Option>
))
) : (
<p className="px-1.5 py-1 italic text-custom-text-400">No matching results</p>
)
) : loader ? (
<> {loader} </>
) : (
<DropdownOptionsLoader />
)}
</>
</div>
</>
);
};
-94
View File
@@ -1,94 +0,0 @@
import { Placement } from "@popperjs/core";
export interface IDropdown {
// root props
onOpen?: () => void;
onClose?: () => void;
containerClassName?: (isOpen: boolean) => string;
tabIndex?: number;
placement?: Placement;
disabled?: boolean;
// button props
buttonContent?: (isOpen: boolean) => React.ReactNode;
buttonContainerClassName?: string;
buttonClassName?: string;
// input props
disableSearch?: boolean;
inputPlaceholder?: string;
inputClassName?: string;
inputIcon?: React.ReactNode;
inputContainerClassName?: string;
// options props
keyExtractor: (option: TDropdownOption) => string;
optionsContainerClassName?: string;
queryArray: string[];
sortByKey: string;
firstItem?: (optionValue: string) => boolean;
renderItem?: ({ value, selected }: { value: string; selected: boolean }) => React.ReactNode;
loader?: React.ReactNode;
disableSorting?: boolean;
}
export interface TDropdownOption {
data: any;
value: string;
className?: ({ active, selected }: { active: boolean; selected: boolean }) => string;
}
export interface IMultiSelectDropdown extends IDropdown {
value: string[];
onChange: (value: string[]) => void;
options: TDropdownOption[] | undefined;
}
export interface ISingleSelectDropdown extends IDropdown {
value: string;
onChange: (value: string) => void;
options: TDropdownOption[] | undefined;
}
export interface IDropdownButton {
isOpen: boolean;
buttonContent?: (isOpen: boolean) => React.ReactNode;
buttonClassName?: string;
buttonContainerClassName?: string;
handleOnClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
setReferenceElement: (element: HTMLButtonElement | null) => void;
disabled?: boolean;
}
export interface IMultiSelectDropdownButton extends IDropdownButton {
value: string[];
}
export interface ISingleSelectDropdownButton extends IDropdownButton {
value: string;
}
export interface IDropdownOptions {
isOpen: boolean;
query: string;
setQuery: (query: string) => void;
inputPlaceholder?: string;
inputClassName?: string;
inputIcon?: React.ReactNode;
inputContainerClassName?: string;
disableSearch?: boolean;
keyExtractor: (option: TDropdownOption) => string;
renderItem: (({ value, selected }: { value: string; selected: boolean }) => React.ReactNode) | undefined;
options: TDropdownOption[] | undefined;
loader?: React.ReactNode;
}
export interface IMultiSelectDropdownOptions extends IDropdownOptions {
value: string[];
}
export interface ISingleSelectDropdownOptions extends IDropdownOptions {
value: string;
}
-3
View File
@@ -1,3 +0,0 @@
export * from "./common";
export * from "./multi-select";
export * from "./single-select";
-167
View File
@@ -1,167 +0,0 @@
import React, { FC, useMemo, useRef, useState } from "react";
import sortBy from "lodash/sortBy";
// headless ui
import { Combobox } from "@headlessui/react";
// popper-js
import { usePopper } from "react-popper";
// components
import { DropdownButton } from "./common";
import { DropdownOptions } from "./common/options";
// hooks
import { useDropdownKeyPressed } from "../hooks/use-dropdown-key-pressed";
import useOutsideClickDetector from "../hooks/use-outside-click-detector";
// helper
import { cn } from "../../helpers";
// types
import { IMultiSelectDropdown } from "./dropdown";
export const MultiSelectDropdown: FC<IMultiSelectDropdown> = (props) => {
const {
value,
onChange,
options,
onOpen,
onClose,
containerClassName,
tabIndex,
placement,
disabled,
buttonContent,
buttonContainerClassName,
buttonClassName,
disableSearch,
inputPlaceholder,
inputClassName,
inputIcon,
inputContainerClassName,
keyExtractor,
optionsContainerClassName,
queryArray,
sortByKey,
firstItem,
renderItem,
loader = false,
disableSorting,
} = props;
// states
const [isOpen, setIsOpen] = useState(false);
const [query, setQuery] = useState("");
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
// handlers
const toggleDropdown = () => {
if (!isOpen) onOpen?.();
setIsOpen((prevIsOpen) => !prevIsOpen);
if (isOpen) onClose?.();
};
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
onClose?.();
setQuery?.("");
};
// options
const sortedOptions = useMemo(() => {
if (!options) return undefined;
const filteredOptions = (options || []).filter((options) => {
const queryString = queryArray.map((query) => options.data[query]).join(" ");
return queryString.toLowerCase().includes(query.toLowerCase());
});
if (disableSorting) return filteredOptions;
return sortBy(filteredOptions, [
(option) => firstItem && firstItem(option.data[option.value]),
(option) => !(value ?? []).includes(option.data[option.value]),
() => sortByKey && sortByKey.toLowerCase(),
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query, options]);
// hooks
const handleKeyDown = useDropdownKeyPressed(toggleDropdown, handleClose);
useOutsideClickDetector(dropdownRef, handleClose);
return (
<Combobox
as="div"
ref={dropdownRef}
value={value}
onChange={onChange}
className={cn("h-full", containerClassName)}
tabIndex={tabIndex}
multiple
onKeyDown={handleKeyDown}
disabled={disabled}
>
<DropdownButton
value={value}
isOpen={isOpen}
setReferenceElement={setReferenceElement}
handleOnClick={handleOnClick}
buttonContent={buttonContent}
buttonClassName={buttonClassName}
buttonContainerClassName={buttonContainerClassName}
disabled={disabled}
/>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className={cn(
"my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none",
optionsContainerClassName
)}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<DropdownOptions
isOpen={isOpen}
query={query}
setQuery={setQuery}
inputIcon={inputIcon}
inputPlaceholder={inputPlaceholder}
inputClassName={inputClassName}
inputContainerClassName={inputContainerClassName}
disableSearch={disableSearch}
keyExtractor={keyExtractor}
options={sortedOptions}
value={value}
renderItem={renderItem}
loader={loader}
/>
</div>
</Combobox.Options>
)}
</Combobox>
);
};
-166
View File
@@ -1,166 +0,0 @@
import React, { FC, useMemo, useRef, useState } from "react";
import sortBy from "lodash/sortBy";
// headless ui
import { Combobox } from "@headlessui/react";
// popper-js
import { usePopper } from "react-popper";
// components
import { DropdownButton } from "./common";
import { DropdownOptions } from "./common/options";
// hooks
import { useDropdownKeyPressed } from "../hooks/use-dropdown-key-pressed";
import useOutsideClickDetector from "../hooks/use-outside-click-detector";
// helper
import { cn } from "../../helpers";
// types
import { ISingleSelectDropdown } from "./dropdown";
export const Dropdown: FC<ISingleSelectDropdown> = (props) => {
const {
value,
onChange,
options,
onOpen,
onClose,
containerClassName,
tabIndex,
placement,
disabled,
buttonContent,
buttonContainerClassName,
buttonClassName,
disableSearch,
inputPlaceholder,
inputClassName,
inputIcon,
inputContainerClassName,
keyExtractor,
optionsContainerClassName,
queryArray,
sortByKey,
firstItem,
renderItem,
loader = false,
disableSorting,
} = props;
// states
const [isOpen, setIsOpen] = useState(false);
const [query, setQuery] = useState("");
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
// handlers
const toggleDropdown = () => {
if (!isOpen) onOpen?.();
setIsOpen((prevIsOpen) => !prevIsOpen);
if (isOpen) onClose?.();
};
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
onClose?.();
setQuery?.("");
};
// options
const sortedOptions = useMemo(() => {
if (!options) return undefined;
const filteredOptions = (options || []).filter((options) => {
const queryString = queryArray.map((query) => options.data[query]).join(" ");
return queryString.toLowerCase().includes(query.toLowerCase());
});
if (disableSorting) return filteredOptions;
return sortBy(filteredOptions, [
(option) => firstItem && firstItem(option.data[option.value]),
(option) => !(value ?? []).includes(option.data[option.value]),
() => sortByKey && sortByKey.toLowerCase(),
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query, options]);
// hooks
const handleKeyDown = useDropdownKeyPressed(toggleDropdown, handleClose);
useOutsideClickDetector(dropdownRef, handleClose);
return (
<Combobox
as="div"
ref={dropdownRef}
value={value}
onChange={onChange}
className={cn("h-full", containerClassName)}
tabIndex={tabIndex}
onKeyDown={handleKeyDown}
disabled={disabled}
>
<DropdownButton
value={value}
isOpen={isOpen}
setReferenceElement={setReferenceElement}
handleOnClick={handleOnClick}
buttonContent={buttonContent}
buttonClassName={buttonClassName}
buttonContainerClassName={buttonContainerClassName}
disabled={disabled}
/>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className={cn(
"my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none",
optionsContainerClassName
)}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<DropdownOptions
isOpen={isOpen}
query={query}
setQuery={setQuery}
inputIcon={inputIcon}
inputPlaceholder={inputPlaceholder}
inputClassName={inputClassName}
inputContainerClassName={inputContainerClassName}
disableSearch={disableSearch}
keyExtractor={keyExtractor}
options={sortedOptions}
value={value}
renderItem={renderItem}
loader={loader}
/>
</div>
</Combobox.Options>
)}
</Combobox>
);
};
@@ -1,36 +0,0 @@
import { useCallback } from "react";
type TUseDropdownKeyPressed = {
(
onEnterKeyDown: () => void,
onEscKeyDown: () => void,
stopPropagation?: boolean
): (event: React.KeyboardEvent<HTMLElement>) => void;
};
export const useDropdownKeyPressed: TUseDropdownKeyPressed = (onEnterKeyDown, onEscKeyDown, stopPropagation = true) => {
const stopEventPropagation = useCallback(
(event: React.KeyboardEvent<HTMLElement>) => {
if (stopPropagation) {
event.stopPropagation();
event.preventDefault();
}
},
[stopPropagation]
);
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "Enter") {
stopEventPropagation(event);
onEnterKeyDown();
} else if (event.key === "Escape") {
stopEventPropagation(event);
onEscKeyDown();
} else if (event.key === "Tab") onEscKeyDown();
},
[onEnterKeyDown, onEscKeyDown, stopEventPropagation]
);
return handleKeyDown;
};
-28
View File
@@ -1,28 +0,0 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const GitlabIcon: React.FC<ISvgIcons> = ({ width = "24", height = "24", className, color }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_282_232)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 0C4.475 0 0 4.475 0 10C0 14.425 2.8625 18.1625 6.8375 19.4875C7.3375 19.575 7.525 19.275 7.525 19.0125C7.525 18.775 7.5125 17.9875 7.5125 17.15C5 17.6125 4.35 16.5375 4.15 15.975C4.0375 15.6875 3.55 14.8 3.125 14.5625C2.775 14.375 2.275 13.9125 3.1125 13.9C3.9 13.8875 4.4625 14.625 4.65 14.925C5.55 16.4375 6.9875 16.0125 7.5625 15.75C7.65 15.1 7.9125 14.6625 8.2 14.4125C5.975 14.1625 3.65 13.3 3.65 9.475C3.65 8.3875 4.0375 7.4875 4.675 6.7875C4.575 6.5375 4.225 5.5125 4.775 4.1375C4.775 4.1375 5.6125 3.875 7.525 5.1625C8.325 4.9375 9.175 4.825 10.025 4.825C10.875 4.825 11.725 4.9375 12.525 5.1625C14.4375 3.8625 15.275 4.1375 15.275 4.1375C15.825 5.5125 15.475 6.5375 15.375 6.7875C16.0125 7.4875 16.4 8.375 16.4 9.475C16.4 13.3125 14.0625 14.1625 11.8375 14.4125C12.2 14.725 12.5125 15.325 12.5125 16.2625C12.5125 17.6 12.5 18.675 12.5 19.0125C12.5 19.275 12.6875 19.5875 13.1875 19.4875C15.1726 18.8173 16.8976 17.5414 18.1197 15.8395C19.3418 14.1375 19.9994 12.0952 20 10C20 4.475 15.525 0 10 0Z"
fill={color ? color : "rgb(var(--color-text-200))"}
/>
</g>
<defs>
<clipPath id="clip0_282_232">
<rect width={width} height={height} />
</clipPath>
</defs>
</svg>
);
-1
View File
@@ -12,7 +12,6 @@ export * from "./dice-icon";
export * from "./discord-icon";
export * from "./full-screen-panel-icon";
export * from "./github-icon";
export * from "./gitlab-icon";
export * from "./layer-stack";
export * from "./layers-icon";
export * from "./photo-filter-icon";
+1 -3
View File
@@ -4,7 +4,6 @@ export * from "./badge";
export * from "./button";
export * from "./emoji";
export * from "./dropdowns";
export * from "./dropdown";
export * from "./form-fields";
export * from "./icons";
export * from "./progress";
@@ -14,5 +13,4 @@ export * from "./loader";
export * from "./control-link";
export * from "./toast";
export * from "./drag-handle";
export * from "./drop-indicator";
export * from "./sortable";
export * from "./drop-indicator";
-62
View File
@@ -1,62 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { isEqual } from "lodash";
import { cn } from "../../helpers";
import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
import { DropIndicator } from "../drop-indicator";
type Props = {
children: React.ReactNode;
data: any; //@todo make this generic
className?: string;
};
const Draggable = ({ children, data, className }: Props) => {
const ref = useRef<HTMLDivElement>(null);
const [dragging, setDragging] = useState<boolean>(false); // NEW
const [isDraggedOver, setIsDraggedOver] = useState(false);
const [closestEdge, setClosestEdge] = useState<string | null>(null);
useEffect(() => {
const el = ref.current;
if (el) {
combine(
draggable({
element: el,
onDragStart: () => setDragging(true), // NEW
onDrop: () => setDragging(false), // NEW
getInitialData: () => data,
}),
dropTargetForElements({
element: el,
onDragEnter: (args) => {
setIsDraggedOver(true);
setClosestEdge(extractClosestEdge(args.self.data));
},
onDragLeave: () => setIsDraggedOver(false),
onDrop: () => {
setIsDraggedOver(false);
},
canDrop: ({ source }) => !isEqual(source.data, data) && source.data.__uuid__ === data.__uuid__,
getData: ({ input, element }) =>
attachClosestEdge(data, {
input,
element,
allowedEdges: ["top", "bottom"],
}),
})
);
}
}, [data]);
return (
<div ref={ref} className={cn(dragging && "opacity-25", className)}>
{<DropIndicator isVisible={isDraggedOver && closestEdge === "top"} />}
{children}
{<DropIndicator isVisible={isDraggedOver && closestEdge === "bottom"} />}
</div>
);
};
export { Draggable };
-2
View File
@@ -1,2 +0,0 @@
export * from "./sortable";
export * from "./draggable";
@@ -1,32 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { Draggable } from "./draggable";
import { Sortable } from "./sortable";
const meta: Meta<typeof Sortable> = {
title: "Sortable",
component: Sortable,
};
export default meta;
type Story = StoryObj<typeof Sortable>;
const data = [
{ id: "1", name: "John Doe" },
{ id: "2", name: "Jane Doe 2" },
{ id: "3", name: "Alice" },
{ id: "4", name: "Bob" },
{ id: "5", name: "Charlie" },
];
export const Default: Story = {
args: {
data,
render: (item: any) => (
// <Draggable data={item} className="rounded-lg">
<div className="border ">{item.name}</div>
// </Draggable>
),
onChange: (data) => console.log(data.map(({ id }) => id)),
keyExtractor: (item: any) => item.id,
},
};
-79
View File
@@ -1,79 +0,0 @@
import React, { Fragment, useEffect, useMemo } from "react";
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { Draggable } from "./draggable";
type Props<T> = {
data: T[];
render: (item: T, index: number) => React.ReactNode;
onChange: (data: T[]) => void;
keyExtractor: (item: T, index: number) => string;
containerClassName?: string;
id: string;
};
const moveItem = <T,>(
data: T[],
source: T,
destination: T & Record<symbol, string>,
keyExtractor: (item: T, index: number) => string
) => {
const sourceIndex = data.indexOf(source);
if (sourceIndex === -1) return data;
const destinationIndex = data.findIndex((item, index) => keyExtractor(item, index) === keyExtractor(destination, 0));
if (destinationIndex === -1) return data;
const symbolKey = Reflect.ownKeys(destination).find((key) => key.toString() === "Symbol(closestEdge)");
const position = symbolKey ? destination[symbolKey as symbol] : "bottom"; // Add 'as symbol' to cast symbolKey to symbol
const newData = [...data];
const [movedItem] = newData.splice(sourceIndex, 1);
let adjustedDestinationIndex = destinationIndex;
if (position === "bottom") {
adjustedDestinationIndex++;
}
// Prevent moving item out of bounds
if (adjustedDestinationIndex > newData.length) {
adjustedDestinationIndex = newData.length;
}
newData.splice(adjustedDestinationIndex, 0, movedItem);
return newData;
};
export const Sortable = <T,>({ data, render, onChange, keyExtractor, containerClassName, id }: Props<T>) => {
useEffect(() => {
const unsubscribe = monitorForElements({
onDrop({ source, location }) {
const destination = location?.current?.dropTargets[0];
if (!destination) return;
onChange(moveItem(data, source.data as T, destination.data as T & { closestEdge: string }, keyExtractor));
},
});
// Clean up the subscription on unmount
return () => {
if (unsubscribe) unsubscribe();
};
}, [data, keyExtractor, onChange]);
const enhancedData = useMemo(() => {
const uuid = id ? id : Math.random().toString(36).substring(7);
return data.map((item) => ({ ...item, __uuid__: uuid }));
}, [data, id]);
return (
<>
{enhancedData.map((item, index) => (
<Draggable key={keyExtractor(item, index)} data={item} className={containerClassName}>
<Fragment>{render(item, index)} </Fragment>
</Draggable>
))}
</>
);
};
export default Sortable;
-1
View File
@@ -1 +0,0 @@
export * from "./sub-heading";
@@ -1,15 +0,0 @@
import React from "react";
import { cn } from "../../helpers";
type Props = {
children: React.ReactNode;
className?: string;
noMargin?: boolean;
};
const SubHeading = ({ children, className, noMargin }: Props) => (
<h3 className={cn("text-xl font-medium text-custom-text-200 block leading-7", !noMargin && "mb-2", className)}>
{children}
</h3>
);
export { SubHeading };
@@ -85,7 +85,7 @@ export const AuthRoot: FC = observer(() => {
const isSMTPConfigured = config?.is_smtp_configured || false;
const isMagicLoginEnabled = config?.is_magic_login_enabled || false;
const isEmailPasswordEnabled = config?.is_email_password_enabled || false;
const isOAuthEnabled = (config && (config?.is_google_enabled || config?.is_github_enabled || config?.is_gitlab_enabled)) || false;
const isOAuthEnabled = (config && (config?.is_google_enabled || config?.is_github_enabled)) || false;
// submit handler- email verification
const handleEmailVerification = async (data: IEmailCheckData) => {
@@ -1,36 +0,0 @@
import { FC } from "react";
import { useSearchParams } from "next/navigation";
import Image from "next/image";
import { useTheme } from "next-themes";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// images
import GitlabLogo from "/public/logos/gitlab-logo.svg";
export type GitlabOAuthButtonProps = {
text: string;
};
export const GitlabOAuthButton: FC<GitlabOAuthButtonProps> = (props) => {
const searchParams = useSearchParams();
const nextPath = searchParams.get("next_path") || undefined;
const { text } = props;
// hooks
const { resolvedTheme } = useTheme();
const handleSignIn = () => {
window.location.assign(`${API_BASE_URL}/auth/spaces/gitlab/${nextPath ? `?next_path=${nextPath}` : ``}`);
};
return (
<button
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-onboarding-background-300 ${
resolvedTheme === "dark" ? "border-[#43484F] bg-[#2F3135]" : "border-[#D9E4FF]"
}`}
onClick={handleSignIn}
>
<Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />
{text}
</button>
);
};
-1
View File
@@ -1,4 +1,3 @@
export * from "./oauth-options";
export * from "./google-button";
export * from "./github-button";
export * from "./gitlab-button";
@@ -1,6 +1,6 @@
import { observer } from "mobx-react-lite";
// components
import { GithubOAuthButton, GitlabOAuthButton, GoogleOAuthButton } from "@/components/account";
import { GithubOAuthButton, GoogleOAuthButton } from "@/components/account";
// hooks
import { useInstance } from "@/hooks/store";
@@ -22,7 +22,6 @@ export const OAuthOptions: React.FC = observer(() => {
</div>
)}
{config?.is_github_enabled && <GithubOAuthButton text="Sign in with Github" />}
{config?.is_gitlab_enabled && <GitlabOAuthButton text="Sign in with GitLab" />}
</div>
</>
);
-18
View File
@@ -52,13 +52,10 @@ export enum EAuthenticationErrorCodes {
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN = "5100",
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP = "5102",
// Oauth
OAUTH_NOT_CONFIGURED = "5104",
GOOGLE_NOT_CONFIGURED = "5105",
GITHUB_NOT_CONFIGURED = "5110",
GITLAB_NOT_CONFIGURED = "5111",
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
GITLAB_OAUTH_PROVIDER_ERROR = "5121",
// Reset Password
INVALID_PASSWORD_TOKEN = "5125",
EXPIRED_PASSWORD_TOKEN = "5130",
@@ -223,10 +220,6 @@ const errorCodeMessages: {
},
// Oauth
[EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED]: {
title: `OAuth not configured`,
message: () => `OAuth not configured. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
title: `Google not configured`,
message: () => `Google not configured. Please contact your administrator.`,
@@ -235,10 +228,6 @@ const errorCodeMessages: {
title: `GitHub not configured`,
message: () => `GitHub not configured. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED]: {
title: `GitLab not configured`,
message: () => `GitLab not configured. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: {
title: `Google OAuth provider error`,
message: () => `Google OAuth provider error. Please try again.`,
@@ -247,10 +236,6 @@ const errorCodeMessages: {
title: `GitHub OAuth provider error`,
message: () => `GitHub OAuth provider error. Please try again.`,
},
[EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR]: {
title: `GitLab OAuth provider error`,
message: () => `GitLab OAuth provider error. Please try again.`,
},
// Reset Password
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
@@ -362,13 +347,10 @@ export const authErrorHandler = (
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP,
EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED,
EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED,
EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED,
EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED,
EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR,
EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR,
EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR,
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.21.0",
"version": "0.20.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="80 80 220 220"><defs><style>.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style></defs><g id="LOGO"><path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-2" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-3" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/><path class="cls-2" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

-1
View File
@@ -4,7 +4,6 @@ export interface IAppConfig {
google_client_id: string | null;
github_app_name: string | null;
github_client_id: string | null;
gitlab_client_id: string | null;
magic_login: boolean;
slack_client_id: string | null;
posthog_api_key: string | null;
@@ -1,36 +0,0 @@
import { FC } from "react";
import { useRouter } from "next/router";
import Image from "next/image";
import { useTheme } from "next-themes";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// images
import GitlabLogo from "/public/logos/gitlab-logo.svg";
export type GitlabOAuthButtonProps = {
text: string;
};
export const GitlabOAuthButton: FC<GitlabOAuthButtonProps> = (props) => {
const { query } = useRouter();
const { next_path } = query;
const { text } = props;
// hooks
const { resolvedTheme } = useTheme();
const handleSignIn = () => {
window.location.assign(`${API_BASE_URL}/auth/gitlab/${next_path ? `?next_path=${next_path}` : ``}`);
};
return (
<button
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 bg-onboarding-background-200 hover:bg-onboarding-background-300 ${
resolvedTheme === "dark" ? "border-[#43484F]" : "border-[#D9E4FF]"
}`}
onClick={handleSignIn}
>
<Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />
{text}
</button>
);
};
-1
View File
@@ -1,4 +1,3 @@
export * from "./oauth-options";
export * from "./google-button";
export * from "./github-button";
export * from "./gitlab-button";
@@ -1,6 +1,6 @@
import { observer } from "mobx-react";
// components
import { GithubOAuthButton, GitlabOAuthButton, GoogleOAuthButton } from "@/components/account";
import { GithubOAuthButton, GoogleOAuthButton } from "@/components/account";
// hooks
import { useInstance } from "@/hooks/store";
@@ -12,7 +12,7 @@ export const OAuthOptions: React.FC<TOAuthOptionProps> = observer(() => {
// hooks
const { config } = useInstance();
const isOAuthEnabled = (config && (config?.is_google_enabled || config?.is_github_enabled || config?.is_gitlab_enabled)) || false;
const isOAuthEnabled = (config && (config?.is_google_enabled || config?.is_github_enabled)) || false;
if (!isOAuthEnabled) return null;
@@ -30,7 +30,6 @@ export const OAuthOptions: React.FC<TOAuthOptionProps> = observer(() => {
</div>
)}
{config?.is_github_enabled && <GithubOAuthButton text="Continue with Github" />}
{config?.is_gitlab_enabled && <GitlabOAuthButton text="Continue with GitLab" />}
</div>
</>
);
@@ -210,7 +210,11 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
{response !== "" && (
<div className="page-block-section max-h-[8rem] text-sm">
Response:
<RichTextReadOnlyEditor initialValue={`<p>${response}</p>`} ref={responseRef} />
<RichTextReadOnlyEditor
initialValue={`<p>${response}</p>`}
containerClassName={response ? "-mx-3 -my-3" : ""}
ref={responseRef}
/>
</div>
)}
{invalidResponse && (
@@ -18,8 +18,6 @@ export const MultipleSelectEntityAction: React.FC<Props> = (props) => {
// derived values
const isSelected = selectionHelpers.getIsEntitySelected(id);
if (selectionHelpers.isSelectionDisabled) return null;
return (
<Checkbox
className={cn("!outline-none size-3.5", className)}
@@ -17,8 +17,6 @@ export const MultipleSelectGroupAction: React.FC<Props> = (props) => {
// derived values
const groupSelectionStatus = selectionHelpers.isGroupSelected(groupID);
if (selectionHelpers.isSelectionDisabled) return null;
return (
<Checkbox
className={cn("size-3.5 !outline-none", className)}
@@ -5,16 +5,14 @@ import { TSelectionHelper, useMultipleSelect } from "@/hooks/use-multiple-select
type Props = {
children: (helpers: TSelectionHelper) => React.ReactNode;
containerRef: React.MutableRefObject<HTMLElement | null>;
disabled?: boolean;
entities: Record<string, string[]>; // { groupID: entityIds[] }
};
export const MultipleSelectGroup: React.FC<Props> = observer((props) => {
const { children, containerRef, disabled = false, entities } = props;
const { children, containerRef, entities } = props;
const helpers = useMultipleSelect({
containerRef,
disabled,
entities,
});
@@ -156,8 +156,6 @@ export const CycleMobileHeader = () => {
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
+7 -15
View File
@@ -1,16 +1,15 @@
import { observer } from "mobx-react";
// hooks
// components
// helpers
import { cn } from "@/helpers/common.helper";
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// hooks
import { useIssueDetail } from "@/hooks/store";
import { TSelectionHelper } from "@/hooks/use-multiple-select";
// types
// constants
import { BLOCK_HEIGHT } from "../constants";
// components
import { ChartAddBlock, ChartDraggable } from "../helpers";
import { useGanttChart } from "../hooks";
// types
import { IBlockUpdateData, IGanttBlock } from "../types";
type Props = {
@@ -22,7 +21,6 @@ type Props = {
enableBlockMove: boolean;
enableAddBlock: boolean;
ganttContainerRef: React.RefObject<HTMLDivElement>;
selectionHelpers: TSelectionHelper;
};
export const GanttChartBlock: React.FC<Props> = observer((props) => {
@@ -35,7 +33,6 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
enableBlockMove,
enableAddBlock,
ganttContainerRef,
selectionHelpers,
} = props;
// store hooks
const { updateActiveBlockId, isBlockActive } = useGanttChart();
@@ -73,10 +70,6 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
});
};
const isBlockSelected = selectionHelpers.getIsEntitySelected(block.id);
const isBlockFocused = selectionHelpers.getIsEntityActive(block.id);
const isBlockHoveredOn = isBlockActive(block.id);
return (
<div
key={`block-${block.id}`}
@@ -87,11 +80,10 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
>
<div
className={cn("relative h-full", {
"rounded-l border border-r-0 border-custom-primary-70": getIsIssuePeeked(block.data.id),
"bg-custom-background-90": isBlockHoveredOn,
"bg-custom-primary-100/5 hover:bg-custom-primary-100/10": isBlockSelected,
"bg-custom-primary-100/10": isBlockSelected && isBlockHoveredOn,
"border border-r-0 border-custom-border-400": isBlockFocused,
"bg-custom-background-80": isBlockActive(block.id),
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(
block.data.id
),
})}
onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)}
@@ -1,12 +1,10 @@
import { FC } from "react";
// hooks
import { TSelectionHelper } from "@/hooks/use-multiple-select";
// constants
import { HEADER_HEIGHT } from "../constants";
// types
import { IBlockUpdateData, IGanttBlock } from "../types";
// components
import { HEADER_HEIGHT } from "../constants";
import { IBlockUpdateData, IGanttBlock } from "../types";
import { GanttChartBlock } from "./block";
// types
// constants
export type GanttChartBlocksProps = {
itemsContainerWidth: number;
@@ -19,7 +17,6 @@ export type GanttChartBlocksProps = {
enableAddBlock: boolean;
ganttContainerRef: React.RefObject<HTMLDivElement>;
showAllBlocks: boolean;
selectionHelpers: TSelectionHelper;
};
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
@@ -34,7 +31,6 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
enableAddBlock,
ganttContainerRef,
showAllBlocks,
selectionHelpers,
} = props;
return (
@@ -60,7 +56,6 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
enableBlockMove={enableBlockMove}
enableAddBlock={enableAddBlock}
ganttContainerRef={ganttContainerRef}
selectionHelpers={selectionHelpers}
/>
);
})}
@@ -2,8 +2,8 @@ import { useEffect, useRef } 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";
// hooks
// components
import { MultipleSelectGroup } from "@/components/core";
import {
BiWeekChartView,
DayChartView,
@@ -18,12 +18,8 @@ import {
WeekChartView,
YearChartView,
} from "@/components/gantt-chart";
import { IssueBulkOperationsRoot } from "@/components/issues";
// helpers
import { cn } from "@/helpers/common.helper";
// constants
import { GANTT_SELECT_GROUP } from "../constants";
// hooks
import { useGanttChart } from "../hooks/use-gantt-chart";
type Props = {
@@ -37,7 +33,6 @@ type Props = {
enableBlockRightResize: boolean;
enableReorder: boolean;
enableAddBlock: boolean;
enableSelection: boolean;
itemsContainerWidth: number;
showAllBlocks: boolean;
sidebarToRender: (props: any) => React.ReactNode;
@@ -58,7 +53,6 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
enableBlockRightResize,
enableReorder,
enableAddBlock,
enableSelection,
itemsContainerWidth,
showAllBlocks,
sidebarToRender,
@@ -113,59 +107,43 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
const ActiveChartView = CHART_VIEW_COMPONENTS[currentView];
return (
<MultipleSelectGroup
containerRef={ganttContainerRef}
entities={{
[GANTT_SELECT_GROUP]: chartBlocks?.map((block) => block.id) ?? [],
}}
disabled
>
{(helpers) => (
<>
<div
// DO NOT REMOVE THE ID
id="gantt-container"
className={cn(
"h-full w-full overflow-auto vertical-scrollbar horizontal-scrollbar scrollbar-lg flex border-t-[0.5px] border-custom-border-200",
{
"mb-8": bottomSpacing,
}
)}
ref={ganttContainerRef}
onScroll={onScroll}
>
<GanttChartSidebar
blocks={blocks}
blockUpdateHandler={blockUpdateHandler}
enableReorder={enableReorder}
enableSelection={enableSelection}
sidebarToRender={sidebarToRender}
title={title}
quickAdd={quickAdd}
selectionHelpers={helpers}
/>
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
<ActiveChartView />
{currentViewData && (
<GanttChartBlocksList
itemsContainerWidth={itemsContainerWidth}
blocks={chartBlocks}
blockToRender={blockToRender}
blockUpdateHandler={blockUpdateHandler}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
enableAddBlock={enableAddBlock}
ganttContainerRef={ganttContainerRef}
showAllBlocks={showAllBlocks}
selectionHelpers={helpers}
/>
)}
</div>
</div>
<IssueBulkOperationsRoot selectionHelpers={helpers} />
</>
<div
// DO NOT REMOVE THE ID
id="gantt-container"
className={cn(
"h-full w-full overflow-auto vertical-scrollbar horizontal-scrollbar scrollbar-lg flex border-t-[0.5px] border-custom-border-200",
{
"mb-8": bottomSpacing,
}
)}
</MultipleSelectGroup>
ref={ganttContainerRef}
onScroll={onScroll}
>
<GanttChartSidebar
blocks={blocks}
blockUpdateHandler={blockUpdateHandler}
enableReorder={enableReorder}
sidebarToRender={sidebarToRender}
title={title}
quickAdd={quickAdd}
/>
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
<ActiveChartView />
{currentViewData && (
<GanttChartBlocksList
itemsContainerWidth={itemsContainerWidth}
blocks={chartBlocks}
blockToRender={blockToRender}
blockUpdateHandler={blockUpdateHandler}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
enableAddBlock={enableAddBlock}
ganttContainerRef={ganttContainerRef}
showAllBlocks={showAllBlocks}
/>
)}
</div>
</div>
);
});
@@ -32,7 +32,6 @@ type ChartViewRootProps = {
enableBlockMove: boolean;
enableReorder: boolean;
enableAddBlock: boolean;
enableSelection: boolean;
bottomSpacing: boolean;
showAllBlocks: boolean;
quickAdd?: React.JSX.Element | undefined;
@@ -52,7 +51,6 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
enableBlockMove,
enableReorder,
enableAddBlock,
enableSelection,
bottomSpacing,
showAllBlocks,
quickAdd,
@@ -186,7 +184,6 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
enableBlockRightResize={enableBlockRightResize}
enableReorder={enableReorder}
enableAddBlock={enableAddBlock}
enableSelection={enableSelection}
itemsContainerWidth={itemsContainerWidth}
showAllBlocks={showAllBlocks}
sidebarToRender={sidebarToRender}

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