mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge pull request #3093 from makeplane/develop
promote: develop to preview
This commit is contained in:
commit
449ac06fd7
14
ENV_SETUP.md
14
ENV_SETUP.md
@ -49,24 +49,10 @@ NGINX_PORT=80
|
||||
|
||||
|
||||
```
|
||||
# Enable/Disable OAUTH - default 0 for selfhosted instance
|
||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||
# Public boards deploy URL
|
||||
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
||||
```
|
||||
|
||||
|
||||
|
||||
## {PROJECT_FOLDER}/spaces/.env.example
|
||||
|
||||
|
||||
|
||||
```
|
||||
# Flag to toggle OAuth
|
||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||
```
|
||||
|
||||
|
||||
|
||||
## {PROJECT_FOLDER}/apiserver/.env
|
||||
|
||||
|
@ -343,13 +343,6 @@ class MagicSignInEndpoint(BaseAPIView):
|
||||
|
||||
if str(token) == str(user_token):
|
||||
user = User.objects.get(email=email)
|
||||
if not user.is_active:
|
||||
return Response(
|
||||
{
|
||||
"error": "Your account has been deactivated. Please contact your site administrator."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
# Send event
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
|
@ -167,12 +167,6 @@ class OauthEndpoint(BaseAPIView):
|
||||
]
|
||||
)
|
||||
|
||||
if not GOOGLE_CLIENT_ID or not GITHUB_CLIENT_ID:
|
||||
return Response(
|
||||
{"error": "Github or Google login is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not medium or not id_token:
|
||||
return Response(
|
||||
{
|
||||
@ -182,9 +176,19 @@ class OauthEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
if medium == "google":
|
||||
if not GOOGLE_CLIENT_ID:
|
||||
return Response(
|
||||
{"error": "Google login is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
data = validate_google_token(id_token, client_id)
|
||||
|
||||
if medium == "github":
|
||||
if not GITHUB_CLIENT_ID:
|
||||
return Response(
|
||||
{"error": "Github login is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
access_token = get_access_token(id_token, client_id)
|
||||
data = get_user_data(access_token)
|
||||
|
||||
|
@ -9,14 +9,20 @@ def derive_key(secret_key):
|
||||
dk = hashlib.pbkdf2_hmac('sha256', secret_key.encode(), b'salt', 100000)
|
||||
return base64.urlsafe_b64encode(dk)
|
||||
|
||||
|
||||
# Encrypt data
|
||||
def encrypt_data(data):
|
||||
cipher_suite = Fernet(derive_key(settings.SECRET_KEY))
|
||||
encrypted_data = cipher_suite.encrypt(data.encode())
|
||||
return encrypted_data.decode() # Convert bytes to string
|
||||
|
||||
if data:
|
||||
cipher_suite = Fernet(derive_key(settings.SECRET_KEY))
|
||||
encrypted_data = cipher_suite.encrypt(data.encode())
|
||||
return encrypted_data.decode() # Convert bytes to string
|
||||
else:
|
||||
return ""
|
||||
|
||||
# Decrypt data
|
||||
def decrypt_data(encrypted_data):
|
||||
cipher_suite = Fernet(derive_key(settings.SECRET_KEY))
|
||||
decrypted_data = cipher_suite.decrypt(encrypted_data.encode()) # Convert string back to bytes
|
||||
return decrypted_data.decode()
|
||||
if encrypted_data:
|
||||
cipher_suite = Fernet(derive_key(settings.SECRET_KEY))
|
||||
decrypted_data = cipher_suite.decrypt(encrypted_data.encode()) # Convert string back to bytes
|
||||
return decrypted_data.decode()
|
||||
else:
|
||||
return ""
|
@ -19,8 +19,11 @@ urlpatterns = [
|
||||
|
||||
|
||||
if settings.DEBUG:
|
||||
import debug_toolbar
|
||||
try:
|
||||
import debug_toolbar
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
urlpatterns = [
|
||||
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
except ImportError:
|
||||
pass
|
||||
|
@ -7,7 +7,6 @@ services:
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
environment:
|
||||
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0}
|
||||
- NEXT_PUBLIC_DEPLOY_URL=$SERVICE_FQDN_SPACE_8082
|
||||
depends_on:
|
||||
- api
|
||||
@ -21,7 +20,6 @@ services:
|
||||
command: /usr/local/bin/start.sh space/server.js space
|
||||
environment:
|
||||
- SERVICE_FQDN_SPACE_8082=/api
|
||||
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0}
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
|
@ -6,7 +6,6 @@ x-app-env : &app-env
|
||||
- WEB_URL=${WEB_URL:-http://localhost}
|
||||
- DEBUG=${DEBUG:-0}
|
||||
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.production} # deprecated
|
||||
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0} # deprecated
|
||||
- NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces} # deprecated
|
||||
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
|
||||
|
@ -7,7 +7,6 @@ API_REPLICAS=1
|
||||
NGINX_PORT=80
|
||||
WEB_URL=http://localhost
|
||||
DEBUG=0
|
||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||
NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
|
||||
SENTRY_DSN=""
|
||||
SENTRY_ENVIRONMENT="production"
|
||||
|
@ -55,7 +55,7 @@
|
||||
"highlight.js": "^11.8.0",
|
||||
"jsx-dom-cjs": "^8.0.3",
|
||||
"lowlight": "^3.0.0",
|
||||
"lucide-react": "^0.244.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"react-moveable": "^0.54.2",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
|
@ -28,16 +28,16 @@
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/react": "^2.1.7",
|
||||
"@tiptap/core": "^2.1.7",
|
||||
"@tiptap/suggestion": "^2.0.4",
|
||||
"@plane/editor-types": "*",
|
||||
"@plane/editor-core": "*",
|
||||
"@plane/editor-types": "*",
|
||||
"@tiptap/core": "^2.1.7",
|
||||
"@tiptap/pm": "^2.1.7",
|
||||
"@tiptap/react": "^2.1.7",
|
||||
"@tiptap/suggestion": "^2.0.4",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"lucide-react": "^0.293.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"@tiptap/pm": "^2.1.7"
|
||||
"lucide-react": "^0.294.0",
|
||||
"tippy.js": "^6.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.15.3",
|
||||
|
@ -30,11 +30,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@plane/editor-core": "*",
|
||||
"@tiptap/core": "^2.1.11",
|
||||
"@plane/editor-types": "*",
|
||||
"@plane/editor-extensions": "*",
|
||||
"@plane/editor-types": "*",
|
||||
"@tiptap/core": "^2.1.11",
|
||||
"@tiptap/extension-placeholder": "^2.1.11",
|
||||
"lucide-react": "^0.244.0"
|
||||
"lucide-react": "^0.294.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.15.3",
|
||||
|
@ -1,2 +0,0 @@
|
||||
# Flag to toggle OAuth
|
||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
@ -8,12 +8,12 @@ import { useTheme } from "next-themes";
|
||||
import githubBlackImage from "public/logos/github-black.svg";
|
||||
import githubWhiteImage from "public/logos/github-white.svg";
|
||||
|
||||
export interface GithubLoginButtonProps {
|
||||
type Props = {
|
||||
handleSignIn: React.Dispatch<string>;
|
||||
clientId: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
export const GitHubSignInButton: FC<Props> = (props) => {
|
||||
const { handleSignIn, clientId } = props;
|
||||
// states
|
||||
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
@ -1,12 +1,12 @@
|
||||
import { FC, useEffect, useRef, useCallback, useState } from "react";
|
||||
import Script from "next/script";
|
||||
|
||||
export interface IGoogleLoginButton {
|
||||
type Props = {
|
||||
clientId: string;
|
||||
handleSignIn: React.Dispatch<any>;
|
||||
}
|
||||
};
|
||||
|
||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
export const GoogleSignInButton: FC<Props> = (props) => {
|
||||
const { handleSignIn, clientId } = props;
|
||||
// refs
|
||||
const googleSignInButton = useRef<HTMLDivElement>(null);
|
||||
@ -30,6 +30,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
size: "large",
|
||||
logo_alignment: "center",
|
||||
text: "signin_with",
|
||||
width: 384,
|
||||
} as GsiButtonConfiguration // customization attributes
|
||||
);
|
||||
} catch (err) {
|
||||
@ -53,7 +54,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
||||
<div className="w-full overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
|
||||
<div className="!w-full overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
export * from "./github-login-button";
|
||||
export * from "./google-login";
|
||||
export * from "./github-sign-in";
|
||||
export * from "./google-sign-in";
|
||||
export * from "./onboarding-form";
|
||||
export * from "./user-logged-in";
|
||||
export * from "./sign-in-forms";
|
||||
|
@ -7,7 +7,7 @@ import { AppConfigService } from "services/app-config.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { GithubLoginButton, GoogleLoginButton } from "components/accounts";
|
||||
import { GitHubSignInButton, GoogleSignInButton } from "components/accounts";
|
||||
|
||||
type Props = {
|
||||
handleSignInRedirection: () => Promise<void>;
|
||||
@ -73,12 +73,12 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
|
||||
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">Or continue with</p>
|
||||
<hr className="w-full border-onboarding-border-100" />
|
||||
</div>
|
||||
<div className="mx-auto flex flex-col items-center gap-2 overflow-hidden pt-7 sm:w-96 sm:flex-row">
|
||||
<div className="mx-auto space-y-4 overflow-hidden pt-7 sm:w-96">
|
||||
{envConfig?.google_client_id && (
|
||||
<GoogleLoginButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} />
|
||||
<GoogleSignInButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} />
|
||||
)}
|
||||
{envConfig?.github_client_id && (
|
||||
<GithubLoginButton clientId={envConfig?.github_client_id} handleSignIn={handleGitHubSignIn} />
|
||||
<GitHubSignInButton clientId={envConfig?.github_client_id} handleSignIn={handleGitHubSignIn} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
@ -20,7 +20,11 @@ export const LatestFeatureBlock = () => {
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto mt-8 overflow-hidden rounded-md border border-onboarding-border-200 bg-onboarding-background-100 object-cover sm:h-52 sm:w-96">
|
||||
<div
|
||||
className={`mx-auto mt-8 overflow-hidden rounded-md border border-onboarding-border-200 object-cover sm:h-52 sm:w-96 ${
|
||||
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
|
||||
}`}
|
||||
>
|
||||
<div className="h-[90%]">
|
||||
<Image
|
||||
src={latestFeatures}
|
||||
|
@ -27,7 +27,7 @@
|
||||
"dotenv": "^16.3.1",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lowlight": "^2.9.0",
|
||||
"lucide-react": "^0.293.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react-lite": "^4.0.3",
|
||||
"next": "^14.0.3",
|
||||
|
@ -7,7 +7,6 @@
|
||||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
||||
"NEXT_PUBLIC_ENABLE_SENTRY",
|
||||
"NEXT_PUBLIC_ENABLE_OAUTH",
|
||||
"NEXT_PUBLIC_TRACK_EVENTS",
|
||||
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
|
||||
"NEXT_PUBLIC_CRISP_ID",
|
||||
|
@ -1,4 +1,2 @@
|
||||
# Enable/Disable OAUTH - default 0 for selfhosted instance
|
||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||
# Public boards deploy URL
|
||||
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
@ -9,12 +9,12 @@ import { useTheme } from "next-themes";
|
||||
import githubLightModeImage from "/public/logos/github-black.png";
|
||||
import githubDarkModeImage from "/public/logos/github-dark.svg";
|
||||
|
||||
export interface GithubLoginButtonProps {
|
||||
type Props = {
|
||||
handleSignIn: React.Dispatch<string>;
|
||||
clientId: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
export const GitHubSignInButton: FC<Props> = (props) => {
|
||||
const { handleSignIn, clientId } = props;
|
||||
// states
|
||||
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
@ -1,12 +1,12 @@
|
||||
import { FC, useEffect, useRef, useCallback, useState } from "react";
|
||||
import Script from "next/script";
|
||||
|
||||
export interface IGoogleLoginButton {
|
||||
type Props = {
|
||||
handleSignIn: React.Dispatch<any>;
|
||||
clientId: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
export const GoogleSignInButton: FC<Props> = (props) => {
|
||||
const { handleSignIn, clientId } = props;
|
||||
// refs
|
||||
const googleSignInButton = useRef<HTMLDivElement>(null);
|
||||
@ -30,6 +30,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
size: "large",
|
||||
logo_alignment: "center",
|
||||
text: "signin_with",
|
||||
width: 384,
|
||||
} as GsiButtonConfiguration // customization attributes
|
||||
);
|
||||
} catch (err) {
|
||||
@ -53,7 +54,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
||||
<div className="w-full overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
|
||||
<div className="!w-full overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
export * from "./sign-in-forms";
|
||||
export * from "./deactivate-account-modal";
|
||||
export * from "./github-login-button";
|
||||
export * from "./google-login";
|
||||
export * from "./github-sign-in";
|
||||
export * from "./google-sign-in";
|
||||
export * from "./email-signup-form";
|
||||
|
@ -6,7 +6,7 @@ import { AuthService } from "services/auth.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { GithubLoginButton, GoogleLoginButton } from "components/account";
|
||||
import { GitHubSignInButton, GoogleSignInButton } from "components/account";
|
||||
|
||||
type Props = {
|
||||
handleSignInRedirection: () => Promise<void>;
|
||||
@ -73,12 +73,12 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
|
||||
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">Or continue with</p>
|
||||
<hr className="w-full border-onboarding-border-100" />
|
||||
</div>
|
||||
<div className="mx-auto flex flex-col items-center gap-2 overflow-hidden pt-7 sm:w-96 sm:flex-row">
|
||||
<div className="mx-auto mt-7 space-y-4 overflow-hidden sm:w-96">
|
||||
{envConfig?.google_client_id && (
|
||||
<GoogleLoginButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} />
|
||||
<GoogleSignInButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} />
|
||||
)}
|
||||
{envConfig?.github_client_id && (
|
||||
<GithubLoginButton clientId={envConfig?.github_client_id} handleSignIn={handleGitHubSignIn} />
|
||||
<GitHubSignInButton clientId={envConfig?.github_client_id} handleSignIn={handleGitHubSignIn} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
@ -27,18 +27,17 @@ export const ProjectAnalyticsModal: React.FC<Props> = observer((props) => {
|
||||
return (
|
||||
<Transition.Root appear show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="transition-transform duration-300"
|
||||
enterFrom="translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition-transform duration-200"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
{/* TODO: fix full screen mode */}
|
||||
<Dialog.Panel
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="transition-transform duration-300"
|
||||
enterFrom="translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition-transform duration-200"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
<Dialog.Panel className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
||||
<div
|
||||
className={`fixed right-0 top-0 z-20 h-full bg-custom-background-100 shadow-custom-shadow-md ${
|
||||
fullScreen ? "w-full p-2" : "w-1/2"
|
||||
}`}
|
||||
@ -61,9 +60,9 @@ export const ProjectAnalyticsModal: React.FC<Props> = observer((props) => {
|
||||
projectDetails={projectDetails}
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
|
@ -20,14 +20,18 @@ export const LatestFeatureBlock = () => {
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto mt-8 overflow-hidden rounded-md border border-onboarding-border-200 bg-onboarding-background-100 object-cover sm:h-52 sm:w-96">
|
||||
<div
|
||||
className={`mx-auto mt-8 overflow-hidden rounded-md border border-onboarding-border-200 object-cover sm:h-52 sm:w-96 ${
|
||||
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
|
||||
}`}
|
||||
>
|
||||
<div className="h-[90%]">
|
||||
<Image
|
||||
src={latestFeatures}
|
||||
alt="Plane Issues"
|
||||
className={`-mt-2 ml-10 h-full rounded-md ${
|
||||
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
|
||||
} `}
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@ import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// components
|
||||
import { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "@plane/rich-text-editor";
|
||||
import { RichReadOnlyEditorWithRef } from "@plane/rich-text-editor";
|
||||
// types
|
||||
import { IIssue, IPageBlock } from "types";
|
||||
|
||||
@ -42,6 +42,7 @@ export const GptAssistantModal: React.FC<Props> = (props) => {
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const editorRef = useRef<any>(null);
|
||||
const responseRef = useRef<any>(null);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@ -115,6 +116,10 @@ export const GptAssistantModal: React.FC<Props> = (props) => {
|
||||
editorRef.current?.setEditorValue(htmlContent ?? `<p>${content}</p>`);
|
||||
}, [htmlContent, editorRef, content]);
|
||||
|
||||
useEffect(() => {
|
||||
responseRef.current?.setEditorValue(`<p>${response}</p>`);
|
||||
}, [response, responseRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute ${inset} z-20 flex w-full flex-col space-y-4 overflow-hidden rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${
|
||||
@ -137,11 +142,12 @@ export const GptAssistantModal: React.FC<Props> = (props) => {
|
||||
{response !== "" && (
|
||||
<div className="page-block-section text-sm">
|
||||
Response:
|
||||
<RichReadOnlyEditor
|
||||
<RichReadOnlyEditorWithRef
|
||||
value={`<p>${response}</p>`}
|
||||
customClassName="-mx-3 -my-3"
|
||||
noBorder
|
||||
borderOnFocus={false}
|
||||
ref={responseRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -14,8 +14,8 @@ type Props = {
|
||||
handleClose: () => void;
|
||||
data?: ILinkDetails | null;
|
||||
status: boolean;
|
||||
createIssueLink: (formData: IIssueLink | ModuleLink) => Promise<void>;
|
||||
updateIssueLink: (formData: IIssueLink | ModuleLink, linkId: string) => Promise<void>;
|
||||
createIssueLink: (formData: IIssueLink | ModuleLink) => Promise<ILinkDetails> | Promise<void> | void;
|
||||
updateIssueLink: (formData: IIssueLink | ModuleLink, linkId: string) => Promise<ILinkDetails> | Promise<void> | void;
|
||||
};
|
||||
|
||||
const defaultValues: IIssueLink | ModuleLink = {
|
||||
@ -31,7 +31,7 @@ export const LinkModal: FC<Props> = (props) => {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
} = useForm<ModuleLink>({
|
||||
} = useForm<IIssueLink | ModuleLink>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
@ -158,8 +158,8 @@ export const LinkModal: FC<Props> = (props) => {
|
||||
? "Updating Link..."
|
||||
: "Update Link"
|
||||
: isSubmitting
|
||||
? "Adding Link..."
|
||||
: "Add Link"}
|
||||
? "Adding Link..."
|
||||
: "Add Link"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -23,6 +23,7 @@ import { ICycle } from "types";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// constants
|
||||
import { CYCLE_STATUS } from "constants/cycle";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export interface ICyclesBoardCard {
|
||||
workspaceSlug: string;
|
||||
@ -36,6 +37,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
const {
|
||||
cycle: cycleStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: userStore,
|
||||
} = useMobxStore();
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
@ -49,6 +51,9 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
const startDate = new Date(cycle.start_date ?? "");
|
||||
const isDateValid = cycle.start_date || cycle.end_date;
|
||||
|
||||
const { currentProjectRole } = userStore;
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
||||
@ -68,8 +73,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
? cycleTotalIssues === 0
|
||||
? "0 Issue"
|
||||
: cycleTotalIssues === cycle.completed_issues
|
||||
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
|
||||
: `${cycle.completed_issues}/${cycleTotalIssues} Issues`
|
||||
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
|
||||
: `${cycle.completed_issues}/${cycleTotalIssues} Issues`
|
||||
: "0 Issue";
|
||||
|
||||
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
@ -235,17 +240,18 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
<span className="text-xs text-custom-text-400">No due date</span>
|
||||
)}
|
||||
<div className="z-10 flex items-center gap-1.5">
|
||||
{cycle.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</button>
|
||||
)}
|
||||
{isEditingAllowed &&
|
||||
(cycle.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</button>
|
||||
))}
|
||||
<CustomMenu width="auto" ellipsis className="z-10">
|
||||
{!isCompleted && (
|
||||
{!isCompleted && isEditingAllowed && (
|
||||
<>
|
||||
<CustomMenu.MenuItem onClick={handleEditCycle}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
|
@ -24,6 +24,7 @@ import { copyTextToClipboard } from "helpers/string.helper";
|
||||
import { ICycle } from "types";
|
||||
// constants
|
||||
import { CYCLE_STATUS } from "constants/cycle";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
type TCyclesListItem = {
|
||||
cycle: ICycle;
|
||||
@ -41,6 +42,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
const {
|
||||
cycle: cycleStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: userStore,
|
||||
} = useMobxStore();
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
@ -53,6 +55,9 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
const endDate = new Date(cycle.end_date ?? "");
|
||||
const startDate = new Date(cycle.start_date ?? "");
|
||||
|
||||
const { currentProjectRole } = userStore;
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const cycleTotalIssues =
|
||||
@ -226,19 +231,19 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{cycle.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</button>
|
||||
)}
|
||||
{isEditingAllowed &&
|
||||
(cycle.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</button>
|
||||
))}
|
||||
|
||||
<CustomMenu width="auto" ellipsis>
|
||||
{!isCompleted && (
|
||||
{!isCompleted && isEditingAllowed && (
|
||||
<>
|
||||
<CustomMenu.MenuItem onClick={handleEditCycle}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
|
@ -97,7 +97,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
||||
id="cycle_description"
|
||||
name="description"
|
||||
placeholder="Description..."
|
||||
className="h-24 w-full resize-none text-sm"
|
||||
className="!h-24 w-full resize-none text-sm"
|
||||
hasError={Boolean(errors?.description)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
@ -135,18 +135,12 @@ export const CycleForm: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-100 pt-5 ">
|
||||
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-100 pt-5 ">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{data
|
||||
? isSubmitting
|
||||
? "Updating Cycle..."
|
||||
: "Update Cycle"
|
||||
: isSubmitting
|
||||
? "Creating Cycle..."
|
||||
: "Create Cycle"}
|
||||
{data ? (isSubmitting ? "Updating" : "Update cycle") : isSubmitting ? "Creating" : "Create cycle"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -49,11 +49,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
state: "SUCCESS",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Error in creating cycle. Please try again.",
|
||||
message: err.detail ?? "Error in creating cycle. Please try again.",
|
||||
});
|
||||
postHogEventTracker("CYCLE_CREATE", {
|
||||
state: "FAILED",
|
||||
@ -73,11 +73,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
message: "Cycle updated successfully.",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Error in updating cycle. Please try again.",
|
||||
message: err.detail ?? "Error in updating cycle. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -281,10 +281,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
cycleDetails.total_issues === 0
|
||||
? "0 Issue"
|
||||
: cycleDetails.total_issues === cycleDetails.completed_issues
|
||||
? cycleDetails.total_issues > 1
|
||||
? `${cycleDetails.total_issues}`
|
||||
: `${cycleDetails.total_issues}`
|
||||
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
|
||||
? cycleDetails.total_issues > 1
|
||||
? `${cycleDetails.total_issues}`
|
||||
: `${cycleDetails.total_issues}`
|
||||
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -468,7 +468,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
{progressPercentage ? (
|
||||
<span className="flex h-5 w-9 items-center justify-center rounded bg-amber-50 text-xs font-medium text-amber-500">
|
||||
<span className="flex h-5 w-9 items-center justify-center rounded bg-amber-500/20 text-xs font-medium text-amber-500">
|
||||
{progressPercentage ? `${progressPercentage}%` : ""}
|
||||
</span>
|
||||
) : (
|
||||
|
@ -21,6 +21,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@ -42,6 +43,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
cycleIssuesFilter: { issueFilters, updateFilters },
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const activeLayout = projectIssueFiltersStore.issueFilters?.displayFilters?.layout;
|
||||
@ -99,6 +101,9 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
const cyclesList = cycleStore.projectCycles;
|
||||
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@ -190,16 +195,18 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
{canUserCreateIssue && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
||||
|
@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const CyclesHeader: FC = observer(() => {
|
||||
// router
|
||||
@ -16,11 +17,15 @@ export const CyclesHeader: FC = observer(() => {
|
||||
// store
|
||||
const {
|
||||
project: projectStore,
|
||||
user: { currentProjectRole },
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
} = useMobxStore();
|
||||
const { currentProjectDetails } = projectStore;
|
||||
|
||||
const canUserCreateCycle =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
@ -50,19 +55,21 @@ export const CyclesHeader: FC = observer(() => {
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLES_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateCycleModal(true);
|
||||
}}
|
||||
>
|
||||
Add Cycle
|
||||
</Button>
|
||||
</div>
|
||||
{canUserCreateCycle && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLES_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateCycleModal(true);
|
||||
}}
|
||||
>
|
||||
Add Cycle
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -21,6 +21,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@ -41,6 +42,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
trackEvent: { setTrackElement },
|
||||
projectLabel: { projectLabels },
|
||||
moduleIssuesFilter: { issueFilters, updateFilters },
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const { currentProjectDetails } = projectStore;
|
||||
@ -100,6 +102,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const modulesList = projectId ? moduleStore.modules[projectId.toString()] : undefined;
|
||||
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@ -191,16 +196,18 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("MODULE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
{canUserCreateIssue && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("MODULE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
||||
|
@ -11,17 +11,25 @@ import { Breadcrumbs, Button, Tooltip, DiceIcon } from "@plane/ui";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// constants
|
||||
import { MODULE_VIEW_LAYOUTS } from "constants/module";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ModulesListHeader: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// store
|
||||
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore();
|
||||
const {
|
||||
project: projectStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
const { currentProjectDetails } = projectStore;
|
||||
|
||||
const { storedValue: modulesView, setValue: setModulesView } = useLocalStorage("modules_view", "grid");
|
||||
|
||||
const canUserCreateModule =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
@ -72,14 +80,16 @@ export const ModulesListHeader: React.FC = observer(() => {
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() => commandPaletteStore.toggleCreateModuleModal(true)}
|
||||
>
|
||||
Add Module
|
||||
</Button>
|
||||
{canUserCreateModule && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() => commandPaletteStore.toggleCreateModuleModal(true)}
|
||||
>
|
||||
Add Module
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -18,6 +18,7 @@ import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@ -36,6 +37,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
// issue filters
|
||||
projectIssuesFilter: { issueFilters, updateFilters },
|
||||
projectIssues: {},
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
@ -87,6 +89,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
|
||||
const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@ -200,16 +205,18 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
{canUserCreateIssue && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Plus } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
@ -16,7 +17,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { Plus } from "lucide-react";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
@ -35,6 +36,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
viewIssuesFilter: { issueFilters, updateFilters },
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
@ -85,6 +87,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined;
|
||||
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@ -170,16 +175,18 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
{
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -5,7 +5,7 @@ import { useTheme } from "next-themes";
|
||||
import { Button } from "@plane/ui";
|
||||
import { UserCog2 } from "lucide-react";
|
||||
// images
|
||||
import instanceSetupDone from "public/instance-setup-done.svg";
|
||||
import instanceSetupDone from "public/instance-setup-done.webp";
|
||||
import PlaneBlackLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
|
||||
import PlaneWhiteLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
@ -84,10 +84,10 @@ export const IssueAttachmentUpload: React.FC<Props> = observer((props) => {
|
||||
disabled: isLoading || disabled,
|
||||
});
|
||||
|
||||
const maxFileSize = envConfig?.file_size_limit ?? MAX_FILE_SIZE;
|
||||
|
||||
const fileError =
|
||||
fileRejections.length > 0
|
||||
? `Invalid file type or size (max ${envConfig?.file_size_limit ?? MAX_FILE_SIZE / 1024 / 1024} MB)`
|
||||
: null;
|
||||
fileRejections.length > 0 ? `Invalid file type or size (max ${maxFileSize / 1024 / 1024} MB)` : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -3,6 +3,8 @@ import { Dialog, Transition } from "@headlessui/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import type { IIssue } from "types";
|
||||
|
||||
@ -18,6 +20,8 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
|
||||
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
setIsDeleteLoading(false);
|
||||
}, [isOpen]);
|
||||
@ -31,7 +35,21 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
|
||||
setIsDeleteLoading(true);
|
||||
if (onSubmit)
|
||||
await onSubmit()
|
||||
.then(() => onClose())
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Issue deleted successfully",
|
||||
});
|
||||
onClose();
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
type: "error",
|
||||
message: "Failed to delete issue",
|
||||
});
|
||||
})
|
||||
.finally(() => setIsDeleteLoading(false));
|
||||
};
|
||||
|
||||
|
@ -226,10 +226,9 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
|
||||
reset({
|
||||
...defaultValues,
|
||||
project: projectId,
|
||||
...initialData,
|
||||
});
|
||||
}, [setFocus, initialData, projectId, reset]);
|
||||
}, [setFocus, initialData, reset]);
|
||||
|
||||
// update projectId in form when projectId changes
|
||||
useEffect(() => {
|
||||
@ -629,8 +628,8 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
? "Updating Issue..."
|
||||
: "Update Issue"
|
||||
: isSubmitting
|
||||
? "Adding Issue..."
|
||||
: "Add Issue"}
|
||||
? "Adding Issue..."
|
||||
: "Add Issue"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,6 +4,8 @@ import { observer } from "mobx-react-lite";
|
||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||
// components
|
||||
import { CalendarChart, IssuePeekOverview } from "components/issues";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import {
|
||||
@ -34,7 +36,7 @@ interface IBaseCalendarRoot {
|
||||
[EIssueActions.REMOVE]?: (issue: IIssue) => Promise<void>;
|
||||
};
|
||||
viewId?: string;
|
||||
handleDragDrop: (source: any, destination: any, issues: any, issueWithIds: any) => void;
|
||||
handleDragDrop: (source: any, destination: any, issues: any, issueWithIds: any) => Promise<void>;
|
||||
}
|
||||
|
||||
export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
@ -44,12 +46,15 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
||||
|
||||
// hooks
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const displayFilters = issuesFilterStore.issueFilters?.displayFilters;
|
||||
|
||||
const issues = issueStore.getIssues;
|
||||
const groupedIssueIds = (issueStore.getIssuesIds ?? {}) as IGroupedIssues;
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
const onDragEnd = async (result: DropResult) => {
|
||||
if (!result) return;
|
||||
|
||||
// return if not dropped on the correct place
|
||||
@ -58,7 +63,15 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
// return if dropped on the same date
|
||||
if (result.destination.droppableId === result.source.droppableId) return;
|
||||
|
||||
if (handleDragDrop) handleDragDrop(result.source, result.destination, issues, groupedIssueIds);
|
||||
if (handleDragDrop) {
|
||||
await handleDragDrop(result.source, result.destination, issues, groupedIssueIds).catch((err) => {
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
type: "error",
|
||||
message: err.detail ?? "Failed to perform this action",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleIssues = useCallback(
|
||||
|
@ -41,9 +41,9 @@ export const CycleCalendarLayout: React.FC = observer(() => {
|
||||
},
|
||||
};
|
||||
|
||||
const handleDragDrop = (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
||||
const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
||||
if (workspaceSlug && projectId && cycleId)
|
||||
handleCalenderDragDrop(
|
||||
await handleCalenderDragDrop(
|
||||
source,
|
||||
destination,
|
||||
workspaceSlug.toString(),
|
||||
|
@ -38,8 +38,8 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
|
||||
},
|
||||
};
|
||||
|
||||
const handleDragDrop = (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
||||
handleCalenderDragDrop(
|
||||
const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
||||
await handleCalenderDragDrop(
|
||||
source,
|
||||
destination,
|
||||
workspaceSlug,
|
||||
|
@ -31,9 +31,9 @@ export const CalendarLayout: React.FC = observer(() => {
|
||||
},
|
||||
};
|
||||
|
||||
const handleDragDrop = (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
||||
const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
||||
if (workspaceSlug && projectId)
|
||||
handleCalenderDragDrop(
|
||||
await handleCalenderDragDrop(
|
||||
source,
|
||||
destination,
|
||||
workspaceSlug.toString(),
|
||||
|
@ -32,9 +32,9 @@ export const ProjectViewCalendarLayout: React.FC = observer(() => {
|
||||
},
|
||||
};
|
||||
|
||||
const handleDragDrop = (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
||||
const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
||||
if (workspaceSlug && projectId)
|
||||
handleCalenderDragDrop(
|
||||
await handleCalenderDragDrop(
|
||||
source,
|
||||
destination,
|
||||
workspaceSlug.toString(),
|
||||
|
@ -54,20 +54,19 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
|
||||
const issues = issueIds.map((id) => issuesResponse?.[id]);
|
||||
|
||||
const updateIssue = async (issue: IIssue, payload: IBlockUpdateData) => {
|
||||
const updateIssueBlockStructure = async (issue: IIssue, data: IBlockUpdateData) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
//Todo fix sort order in the structure
|
||||
await issueStore.updateIssue(
|
||||
workspaceSlug.toString(),
|
||||
issue.project,
|
||||
issue.id,
|
||||
{
|
||||
start_date: payload.start_date,
|
||||
target_date: payload.target_date,
|
||||
},
|
||||
viewId
|
||||
);
|
||||
const payload: any = { ...data };
|
||||
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
|
||||
|
||||
await issueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, payload, viewId);
|
||||
};
|
||||
|
||||
const updateIssue = async (projectId: string, issueId: string, payload: Partial<IIssue>) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await issueStore.updateIssue(workspaceSlug.toString(), projectId, issueId, payload, viewId);
|
||||
};
|
||||
|
||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
@ -80,7 +79,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
||||
blockUpdateHandler={updateIssue}
|
||||
blockUpdateHandler={updateIssueBlockStructure}
|
||||
blockToRender={(data: IIssue) => <IssueGanttBlock data={data} />}
|
||||
sidebarToRender={(props) => (
|
||||
<IssueGanttSidebar
|
||||
@ -102,8 +101,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
projectId={peekProjectId.toString()}
|
||||
issueId={peekIssueId.toString()}
|
||||
handleIssue={async (issueToUpdate) => {
|
||||
// TODO: update the logic here
|
||||
await updateIssue(issueToUpdate as IIssue, {});
|
||||
await updateIssue(peekProjectId.toString(), peekIssueId.toString(), issueToUpdate);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -7,9 +7,15 @@ import { useRouter } from "next/router";
|
||||
|
||||
export const CycleGanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { cycleId } = router.query as { cycleId: string };
|
||||
const { cycleId } = router.query;
|
||||
|
||||
const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore();
|
||||
|
||||
return <BaseGanttRoot issueFiltersStore={cycleIssueFilterStore} issueStore={cycleIssueStore} viewId={cycleId} />;
|
||||
return (
|
||||
<BaseGanttRoot
|
||||
issueFiltersStore={cycleIssueFilterStore}
|
||||
issueStore={cycleIssueStore}
|
||||
viewId={cycleId?.toString()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -7,9 +7,15 @@ import { useRouter } from "next/router";
|
||||
|
||||
export const ModuleGanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { moduleId } = router.query as { moduleId: string };
|
||||
const { moduleId } = router.query;
|
||||
|
||||
const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore();
|
||||
|
||||
return <BaseGanttRoot issueFiltersStore={moduleIssueFilterStore} issueStore={moduleIssueStore} viewId={moduleId} />;
|
||||
return (
|
||||
<BaseGanttRoot
|
||||
issueFiltersStore={moduleIssueFilterStore}
|
||||
issueStore={moduleIssueStore}
|
||||
viewId={moduleId?.toString()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -121,8 +121,9 @@ export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
|
||||
try {
|
||||
quickAddCallback && quickAddCallback(workspaceSlug, projectId, payload, viewId);
|
||||
|
||||
if (quickAddCallback) {
|
||||
await quickAddCallback(workspaceSlug, projectId, payload, viewId);
|
||||
}
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
|
@ -147,7 +147,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
setIsDragStarted(true);
|
||||
};
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
const onDragEnd = async (result: DropResult) => {
|
||||
setIsDragStarted(false);
|
||||
|
||||
if (!result) return;
|
||||
@ -171,7 +171,15 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
});
|
||||
setDeleteIssueModal(true);
|
||||
} else {
|
||||
handleDragDrop(result.source, result.destination, sub_group_by, group_by, issues, issueIds);
|
||||
await handleDragDrop(result.source, result.destination, sub_group_by, group_by, issues, issueIds).catch(
|
||||
(err) => {
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
type: "error",
|
||||
message: err.detail ?? "Failed to perform this action",
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -187,25 +195,12 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
|
||||
const handleDeleteIssue = async () => {
|
||||
if (!handleDragDrop) return;
|
||||
await handleDragDrop(dragState.source, dragState.destination, sub_group_by, group_by, issues, issueIds)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Issue deleted successfully",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
type: "error",
|
||||
message: "Failed to delete issue",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
await handleDragDrop(dragState.source, dragState.destination, sub_group_by, group_by, issues, issueIds).finally(
|
||||
() => {
|
||||
setDeleteIssueModal(false);
|
||||
setDragState({});
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||
|
@ -124,7 +124,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
value={issue?.start_date || null}
|
||||
onChange={(date: string) => handleStartDate(date)}
|
||||
disabled={isReadOnly}
|
||||
placeHolder="Start date"
|
||||
type="start_date"
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -134,7 +134,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
value={issue?.target_date || null}
|
||||
onChange={(date: string) => handleTargetDate(date)}
|
||||
disabled={isReadOnly}
|
||||
placeHolder="Target date"
|
||||
type="target_date"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -108,7 +108,7 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
|
||||
value={issue?.start_date || null}
|
||||
onChange={(date: string) => handleStartDate(date)}
|
||||
disabled={isReadonly}
|
||||
placeHolder="Start date"
|
||||
type="start_date"
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -118,7 +118,7 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
|
||||
value={issue?.target_date || null}
|
||||
onChange={(date: string) => handleTargetDate(date)}
|
||||
disabled={isReadonly}
|
||||
placeHolder="Target date"
|
||||
type="target_date"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { Check, ChevronDown, Search, User2 } from "lucide-react";
|
||||
import { Check, ChevronDown, CircleUser, Search } from "lucide-react";
|
||||
// ui
|
||||
import { Avatar, AvatarGroup, Tooltip } from "@plane/ui";
|
||||
// types
|
||||
@ -110,8 +110,8 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
|
||||
})}
|
||||
</AvatarGroup>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
||||
<User2 className="h-4 w-4 text-custom-text-400" />
|
||||
<span className="h-5 w-5 grid place-items-center">
|
||||
<CircleUser className="h-4 w-4" strokeWidth={1.5} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -140,7 +140,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex w-full items-center justify-between gap-1 text-xs ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer"
|
||||
} ${buttonClassName}`}
|
||||
onClick={() => !projectMembers && getWorkspaceMembers()}
|
||||
>
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
// headless ui
|
||||
import { Popover } from "@headlessui/react";
|
||||
// lucide icons
|
||||
import { Calendar, X } from "lucide-react";
|
||||
import { CalendarCheck2, CalendarClock, X } from "lucide-react";
|
||||
// react date picker
|
||||
import DatePicker from "react-datepicker";
|
||||
// mobx
|
||||
@ -18,11 +18,24 @@ export interface IIssuePropertyDate {
|
||||
value: any;
|
||||
onChange: (date: any) => void;
|
||||
disabled?: boolean;
|
||||
placeHolder?: string;
|
||||
type: "start_date" | "target_date";
|
||||
}
|
||||
|
||||
const DATE_OPTIONS = {
|
||||
start_date: {
|
||||
key: "start_date",
|
||||
placeholder: "Start date",
|
||||
icon: CalendarClock,
|
||||
},
|
||||
target_date: {
|
||||
key: "target_date",
|
||||
placeholder: "Target date",
|
||||
icon: CalendarCheck2,
|
||||
},
|
||||
};
|
||||
|
||||
export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props) => {
|
||||
const { value, onChange, disabled, placeHolder } = props;
|
||||
const { value, onChange, disabled, type } = props;
|
||||
|
||||
const dropdownBtn = React.useRef<any>(null);
|
||||
const dropdownOptions = React.useRef<any>(null);
|
||||
@ -31,6 +44,8 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
const dateOptionDetails = DATE_OPTIONS[type];
|
||||
|
||||
return (
|
||||
<Popover as="div" className="relative">
|
||||
{({ open }) => {
|
||||
@ -49,10 +64,10 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2 overflow-hidden">
|
||||
<Calendar className="h-3 w-3" strokeWidth={2} />
|
||||
<dateOptionDetails.icon className="h-3 w-3" strokeWidth={2} />
|
||||
{value && (
|
||||
<>
|
||||
<Tooltip tooltipHeading={placeHolder} tooltipContent={value ?? "None"}>
|
||||
<Tooltip tooltipHeading={dateOptionDetails.placeholder} tooltipContent={value ?? "None"}>
|
||||
<div className="text-xs">{value}</div>
|
||||
</Tooltip>
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { usePopper } from "react-popper";
|
||||
// components
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import { Check, ChevronDown, Search, Tags } from "lucide-react";
|
||||
// types
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { RootStore } from "store/root";
|
||||
@ -25,6 +25,7 @@ export interface IIssuePropertyLabels {
|
||||
placement?: Placement;
|
||||
maxRender?: number;
|
||||
noLabelBorder?: boolean;
|
||||
placeholderText?: string;
|
||||
}
|
||||
|
||||
export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((props) => {
|
||||
@ -41,6 +42,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
placement,
|
||||
maxRender = 2,
|
||||
noLabelBorder = false,
|
||||
placeholderText,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
@ -144,11 +146,12 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
className={`flex h-full items-center justify-center rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${
|
||||
className={`h-full flex items-center justify-center gap-2 rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${
|
||||
noLabelBorder ? "" : "border-[0.5px] border-custom-border-300"
|
||||
}`}
|
||||
>
|
||||
Select labels
|
||||
<Tags className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
{placeholderText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -171,8 +174,8 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: value.length <= maxRender
|
||||
? "cursor-pointer"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
? "cursor-pointer"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={() => !storeLabels && fetchLabels()}
|
||||
>
|
||||
|
@ -94,7 +94,7 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
|
||||
|
||||
const label = (
|
||||
<Tooltip tooltipHeading="State" tooltipContent={selectedOption?.name ?? ""} position="top">
|
||||
<div className="flex w-full cursor-pointer items-center gap-2 text-custom-text-200">
|
||||
<div className="flex w-full items-center gap-2 text-custom-text-200">
|
||||
{selectedOption && <StateGroupIcon stateGroup={selectedOption?.group as any} color={selectedOption?.color} />}
|
||||
<span className="line-clamp-1 inline-block truncate">{selectedOption?.name ?? "State"}</span>
|
||||
</div>
|
||||
|
@ -17,7 +17,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { workspaceSlug, cycleId } = router.query;
|
||||
|
||||
// states
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
@ -75,7 +75,10 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIssueToEdit(issue);
|
||||
setIssueToEdit({
|
||||
...issue,
|
||||
cycle: cycleId?.toString() ?? null,
|
||||
});
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
|
@ -17,7 +17,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { workspaceSlug, moduleId } = router.query;
|
||||
|
||||
// states
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
@ -75,7 +75,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIssueToEdit(issue);
|
||||
setIssueToEdit({ ...issue, module: moduleId?.toString() ?? null });
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
|
@ -2,6 +2,8 @@ import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { Copy, Link, Pencil, Trash2 } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
@ -12,6 +14,8 @@ import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
import { IIssue } from "types";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
// constant
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, customActionButton } = props;
|
||||
@ -24,6 +28,12 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
const [issueToEdit, setIssueToEdit] = useState<IIssue | null>(null);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
|
||||
const { user: userStore } = useMobxStore();
|
||||
|
||||
const { currentProjectRole } = userStore;
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleCopyIssueLink = () => {
|
||||
@ -71,43 +81,47 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
Copy link
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIssueToEdit(issue);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit issue
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Copy className="h-3 w-3" />
|
||||
Make a copy
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete issue
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{isEditingAllowed && (
|
||||
<>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIssueToEdit(issue);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit issue
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Copy className="h-3 w-3" />
|
||||
Make a copy
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete issue
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
)}
|
||||
</CustomMenu>
|
||||
</>
|
||||
);
|
||||
|
@ -27,7 +27,7 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
|
||||
value={issue.assignees}
|
||||
defaultOptions={issue?.assignee_details ? issue.assignee_details : []}
|
||||
onChange={(data) => onChange({ assignees: data })}
|
||||
className="h-full w-full"
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
|
||||
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1 "
|
||||
noLabelBorder
|
||||
hideDropdownArrow
|
||||
@ -40,14 +40,16 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue) => (
|
||||
<SpreadsheetAssigneeColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
members={members}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetAssigneeColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
members={members}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -18,7 +18,7 @@ export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full items-center px-2.5 py-1 text-xs">
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200">
|
||||
{issue.attachment_count} {issue.attachment_count === 1 ? "attachment" : "attachments"}
|
||||
</div>
|
||||
|
||||
@ -27,7 +27,9 @@ export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetAttachmentColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetAttachmentColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -19,7 +19,7 @@ export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIss
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full items-center justify-center text-xs">
|
||||
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200">
|
||||
{renderLongDetailDateFormat(issue.created_at)}
|
||||
</div>
|
||||
|
||||
@ -28,7 +28,9 @@ export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIss
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetCreatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
<div className="h-11">
|
||||
<SpreadsheetCreatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -24,7 +24,7 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issue, onChange, exp
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
onChange={(val) => onChange({ target_date: val })}
|
||||
className="flex !h-full !w-full max-w-full items-center px-2.5 py-1"
|
||||
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200"
|
||||
noBorder
|
||||
disabled={disabled}
|
||||
/>
|
||||
@ -34,13 +34,15 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issue, onChange, exp
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetDueDateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetDueDateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -25,7 +25,7 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
|
||||
projectId={issue.project_detail?.id ?? null}
|
||||
value={issue.estimate_point}
|
||||
onChange={(data) => onChange({ estimate_point: data })}
|
||||
className="h-full w-full"
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
|
||||
buttonClassName="h-full w-full px-2.5 py-1 !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={disabled}
|
||||
@ -36,13 +36,15 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetEstimateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetEstimateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -29,12 +29,12 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
|
||||
value={issue.labels}
|
||||
defaultOptions={issue?.label_details ? issue.label_details : []}
|
||||
onChange={(data) => onChange({ labels: data })}
|
||||
className="h-full w-full"
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
|
||||
buttonClassName="px-2.5 h-full"
|
||||
noLabelBorder
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
disabled={disabled}
|
||||
placeholderText="Select labels"
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
@ -42,14 +42,16 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetLabelColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
labels={labels}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetLabelColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
labels={labels}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -18,7 +18,7 @@ export const SpreadsheetLinkColumn: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full items-center px-2.5 py-1 text-xs">
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200">
|
||||
{issue.link_count} {issue.link_count === 1 ? "link" : "links"}
|
||||
</div>
|
||||
|
||||
@ -27,7 +27,9 @@ export const SpreadsheetLinkColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetLinkColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetLinkColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -24,8 +24,8 @@ export const SpreadsheetPriorityColumn: React.FC<Props> = ({ issue, onChange, ex
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={(data) => onChange({ priority: data })}
|
||||
className="h-full w-full"
|
||||
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1 "
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
|
||||
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1"
|
||||
showTitle
|
||||
highlightUrgentPriority={false}
|
||||
hideDropdownArrow
|
||||
@ -37,13 +37,15 @@ export const SpreadsheetPriorityColumn: React.FC<Props> = ({ issue, onChange, ex
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetPriorityColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetPriorityColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -24,7 +24,7 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issue, onChange, e
|
||||
<ViewStartDateSelect
|
||||
issue={issue}
|
||||
onChange={(val) => onChange({ start_date: val })}
|
||||
className="flex !h-full !w-full max-w-full items-center px-2.5 py-1"
|
||||
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200"
|
||||
noBorder
|
||||
disabled={disabled}
|
||||
/>
|
||||
@ -34,13 +34,15 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issue, onChange, e
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetStartDateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetStartDateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -29,7 +29,7 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
||||
value={issue.state}
|
||||
defaultOptions={issue?.state_detail ? [issue.state_detail] : []}
|
||||
onChange={(data) => onChange({ state: data.id, state_detail: data })}
|
||||
className="h-full w-full"
|
||||
className="w-full !h-11 border-b-[0.5px] border-custom-border-200"
|
||||
buttonClassName="!shadow-none !border-0 h-full w-full"
|
||||
hideDropdownArrow
|
||||
disabled={disabled}
|
||||
@ -40,14 +40,16 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue) => (
|
||||
<SpreadsheetStateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
states={states}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className="h-11">
|
||||
<SpreadsheetStateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
states={states}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -18,7 +18,7 @@ export const SpreadsheetSubIssueColumn: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full items-center px-2.5 py-1 text-xs">
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200">
|
||||
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
</div>
|
||||
|
||||
@ -27,7 +27,9 @@ export const SpreadsheetSubIssueColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetSubIssueColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetSubIssueColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -21,7 +21,7 @@ export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full items-center justify-center text-xs">
|
||||
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200">
|
||||
{renderLongDetailDateFormat(issue.updated_at)}
|
||||
</div>
|
||||
|
||||
@ -30,7 +30,9 @@ export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetUpdatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetUpdatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -165,7 +165,7 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<div
|
||||
key={`${property}-${issue.id}`}
|
||||
className={`h-11 border-b-[0.5px] border-custom-border-200 ${
|
||||
className={`h-fit ${
|
||||
disableUserActions ? "" : "cursor-pointer hover:bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
|
@ -77,6 +77,13 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!issues || issues.length === 0)
|
||||
return (
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full overflow-x-auto whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200">
|
||||
<div className="flex h-full w-full flex-col">
|
||||
@ -84,7 +91,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
ref={containerRef}
|
||||
className="horizontal-scroll-enable flex divide-x-[0.5px] divide-custom-border-200 overflow-y-auto"
|
||||
>
|
||||
{issues && issues.length > 0 ? (
|
||||
{issues && issues.length > 0 && (
|
||||
<>
|
||||
<div className="sticky left-0 z-[2] w-[28rem]">
|
||||
<div
|
||||
@ -134,10 +141,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
states={states}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<div /> {/* empty div to show right most border */}
|
||||
</div>
|
||||
|
@ -266,11 +266,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be created. Please try again.",
|
||||
message: err.detail ?? "Issue could not be created. Please try again.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"ISSUE_CREATED",
|
||||
@ -312,11 +312,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
|
||||
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be created. Please try again.",
|
||||
message: err.detail ?? "Issue could not be created. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -347,11 +347,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be updated. Please try again.",
|
||||
message: err.detail ?? "Issue could not be updated. Please try again.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"ISSUE_UPDATED",
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { FC, useState } from "react";
|
||||
import { mutate } from "swr";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
@ -17,30 +17,25 @@ import {
|
||||
SidebarPrioritySelect,
|
||||
SidebarStateSelect,
|
||||
} from "../sidebar-select";
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
import { LinkModal, LinksList } from "components/core";
|
||||
// types
|
||||
import { IIssue, IIssueLink, TIssuePriorities, ILinkDetails } from "types";
|
||||
// fetch-keys
|
||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
import { IIssue, TIssuePriorities, ILinkDetails, IIssueLink } from "types";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
interface IPeekOverviewProperties {
|
||||
issue: IIssue;
|
||||
issueUpdate: (issue: Partial<IIssue>) => void;
|
||||
issueLinkCreate: (data: IIssueLink) => Promise<ILinkDetails>;
|
||||
issueLinkUpdate: (data: IIssueLink, linkId: string) => Promise<ILinkDetails>;
|
||||
issueLinkDelete: (linkId: string) => Promise<void>;
|
||||
disableUserActions: boolean;
|
||||
}
|
||||
|
||||
const issueService = new IssueService();
|
||||
|
||||
export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((props) => {
|
||||
const { issue, issueUpdate, disableUserActions } = props;
|
||||
const { issue, issueUpdate, issueLinkCreate, issueLinkUpdate, issueLinkDelete, disableUserActions } = props;
|
||||
// states
|
||||
const [linkModal, setLinkModal] = useState(false);
|
||||
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
|
||||
@ -54,8 +49,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleState = (_state: string) => {
|
||||
issueUpdate({ ...issue, state: _state });
|
||||
};
|
||||
@ -81,61 +74,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
issueUpdate({ ...issue, ...formData });
|
||||
};
|
||||
|
||||
const handleCreateLink = async (formData: IIssueLink) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const payload = { metadata: {}, ...formData };
|
||||
|
||||
await issueService
|
||||
.createIssueLink(workspaceSlug as string, projectId as string, issue.id, payload)
|
||||
.then(() => mutate(ISSUE_DETAILS(issue.id)))
|
||||
.catch((err) => {
|
||||
if (err.status === 400)
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "This URL already exists for this issue.",
|
||||
});
|
||||
else
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateLink = async (formData: IIssueLink, linkId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const payload = { metadata: {}, ...formData };
|
||||
|
||||
const updatedLinks = issue.issue_link.map((l) =>
|
||||
l.id === linkId
|
||||
? {
|
||||
...l,
|
||||
title: formData.title,
|
||||
url: formData.url,
|
||||
}
|
||||
: l
|
||||
);
|
||||
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issue.id),
|
||||
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
|
||||
false
|
||||
);
|
||||
|
||||
await issueService
|
||||
.updateIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId, payload)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issue.id));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCycleOrModuleChange = async () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
@ -147,27 +85,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
setLinkModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteLink = async (linkId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const updatedLinks = issue.issue_link.filter((l) => l.id !== linkId);
|
||||
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issue.id),
|
||||
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
|
||||
false
|
||||
);
|
||||
|
||||
await issueService
|
||||
.deleteIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issue.id));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
const projectDetails = workspaceSlug ? getProjectById(workspaceSlug.toString(), issue.project) : null;
|
||||
const isEstimateEnabled = projectDetails?.estimate;
|
||||
|
||||
@ -187,8 +104,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
}}
|
||||
data={selectedLinkToUpdate}
|
||||
status={selectedLinkToUpdate ? true : false}
|
||||
createIssueLink={handleCreateLink}
|
||||
updateIssueLink={handleUpdateLink}
|
||||
createIssueLink={issueLinkCreate}
|
||||
updateIssueLink={issueLinkUpdate}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex w-full flex-col gap-5 py-5">
|
||||
@ -373,7 +290,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
{issue?.issue_link && issue.issue_link.length > 0 ? (
|
||||
<LinksList
|
||||
links={issue.issue_link}
|
||||
handleDeleteLink={handleDeleteLink}
|
||||
handleDeleteLink={issueLinkDelete}
|
||||
handleEditLink={handleEditLink}
|
||||
userAuth={{
|
||||
isGuest: currentProjectRole === EUserWorkspaceRoles.GUEST,
|
||||
|
@ -10,7 +10,7 @@ import { IssueView } from "components/issues";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IIssue, IIssueLink } from "types";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
@ -42,6 +42,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
removeIssueReaction,
|
||||
createIssueSubscription,
|
||||
removeIssueSubscription,
|
||||
createIssueLink,
|
||||
updateIssueLink,
|
||||
deleteIssueLink,
|
||||
getIssue,
|
||||
loader,
|
||||
fetchPeekIssueDetails,
|
||||
@ -121,6 +124,13 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
|
||||
const issueSubscriptionRemove = () => removeIssueSubscription(workspaceSlug, projectId, issueId);
|
||||
|
||||
const issueLinkCreate = (formData: IIssueLink) => createIssueLink(workspaceSlug, projectId, issueId, formData);
|
||||
|
||||
const issueLinkUpdate = (formData: IIssueLink, linkId: string) =>
|
||||
updateIssueLink(workspaceSlug, projectId, issueId, linkId, formData);
|
||||
|
||||
const issueLinkDelete = (linkId: string) => deleteIssueLink(workspaceSlug, projectId, issueId, linkId);
|
||||
|
||||
const handleDeleteIssue = async () => {
|
||||
if (isArchived) await deleteArchivedIssue(workspaceSlug, projectId, issue!);
|
||||
else removeIssueFromStructure(workspaceSlug, projectId, issue!);
|
||||
@ -159,6 +169,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
issueSubscriptionCreate={issueSubscriptionCreate}
|
||||
issueSubscriptionRemove={issueSubscriptionRemove}
|
||||
issueLinkCreate={issueLinkCreate}
|
||||
issueLinkUpdate={issueLinkUpdate}
|
||||
issueLinkDelete={issueLinkDelete}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
disableUserActions={[5, 10].includes(userRole)}
|
||||
showCommentAccessSpecifier={currentProjectDetails?.is_deployed}
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
// ui
|
||||
import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IIssue, IIssueLink, ILinkDetails } from "types";
|
||||
|
||||
interface IIssueView {
|
||||
workspaceSlug: string;
|
||||
@ -38,6 +38,9 @@ interface IIssueView {
|
||||
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
|
||||
issueSubscriptionCreate: () => void;
|
||||
issueSubscriptionRemove: () => void;
|
||||
issueLinkCreate: (formData: IIssueLink) => Promise<ILinkDetails>;
|
||||
issueLinkUpdate: (formData: IIssueLink, linkId: string) => Promise<ILinkDetails>;
|
||||
issueLinkDelete: (linkId: string) => Promise<void>;
|
||||
handleDeleteIssue: () => Promise<void>;
|
||||
children: ReactNode;
|
||||
disableUserActions?: boolean;
|
||||
@ -84,6 +87,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
issueCommentReactionRemove,
|
||||
issueSubscriptionCreate,
|
||||
issueSubscriptionRemove,
|
||||
issueLinkCreate,
|
||||
issueLinkUpdate,
|
||||
issueLinkDelete,
|
||||
handleDeleteIssue,
|
||||
children,
|
||||
disableUserActions = false,
|
||||
@ -286,6 +292,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
<PeekOverviewProperties
|
||||
issue={issue}
|
||||
issueUpdate={issueUpdate}
|
||||
issueLinkCreate={issueLinkCreate}
|
||||
issueLinkUpdate={issueLinkUpdate}
|
||||
issueLinkDelete={issueLinkDelete}
|
||||
disableUserActions={disableUserActions}
|
||||
/>
|
||||
|
||||
@ -342,6 +351,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
<PeekOverviewProperties
|
||||
issue={issue}
|
||||
issueUpdate={issueUpdate}
|
||||
issueLinkCreate={issueLinkCreate}
|
||||
issueLinkUpdate={issueLinkUpdate}
|
||||
issueLinkDelete={issueLinkDelete}
|
||||
disableUserActions={disableUserActions}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import type { FieldError } from "react-hook-form";
|
||||
// mobx store
|
||||
@ -23,9 +22,6 @@ export const IssueProjectSelect: React.FC<IssueProjectSelectProps> = observer((p
|
||||
const { value, onChange } = props;
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
@ -33,13 +29,13 @@ export const IssueProjectSelect: React.FC<IssueProjectSelectProps> = observer((p
|
||||
placement: "bottom-start",
|
||||
});
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
const {
|
||||
project: { joinedProjects },
|
||||
} = useMobxStore();
|
||||
|
||||
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
|
||||
const selectedProject = joinedProjects?.find((i) => i.id === value);
|
||||
|
||||
const selectedProject = projects?.find((i) => i.id === value);
|
||||
|
||||
const options = projects?.map((project) => ({
|
||||
const options = joinedProjects?.map((project) => ({
|
||||
value: project.id,
|
||||
query: project.name,
|
||||
content: (
|
||||
@ -61,8 +57,8 @@ export const IssueProjectSelect: React.FC<IssueProjectSelectProps> = observer((p
|
||||
{selectedProject.emoji
|
||||
? renderEmoji(selectedProject.emoji)
|
||||
: selectedProject.icon_prop
|
||||
? renderEmoji(selectedProject.icon_prop)
|
||||
: null}
|
||||
? renderEmoji(selectedProject.icon_prop)
|
||||
: null}
|
||||
</span>
|
||||
<div className="truncate">{selectedProject.identifier}</div>
|
||||
</div>
|
||||
|
@ -84,6 +84,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
user: { currentUser, currentProjectRole },
|
||||
projectState: { states },
|
||||
projectIssues: { removeIssue },
|
||||
issueDetail: { createIssueLink, updateIssueLink, deleteIssueLink },
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
@ -129,80 +130,19 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
[workspaceSlug, projectId, issueId, issueDetail, currentUser]
|
||||
);
|
||||
|
||||
const handleCreateLink = async (formData: IIssueLink) => {
|
||||
if (!workspaceSlug || !projectId || !issueDetail) return;
|
||||
|
||||
const payload = { metadata: {}, ...formData };
|
||||
|
||||
await issueService
|
||||
.createIssueLink(workspaceSlug as string, projectId as string, issueDetail.id, payload)
|
||||
.then(() => mutate(ISSUE_DETAILS(issueDetail.id)))
|
||||
.catch((err) => {
|
||||
if (err.status === 400)
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "This URL already exists for this issue.",
|
||||
});
|
||||
else
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
});
|
||||
const issueLinkCreate = (formData: IIssueLink) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
createIssueLink(workspaceSlug.toString(), projectId.toString(), issueId.toString(), formData);
|
||||
};
|
||||
|
||||
const handleUpdateLink = async (formData: IIssueLink, linkId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueDetail) return;
|
||||
|
||||
const payload = { metadata: {}, ...formData };
|
||||
|
||||
const updatedLinks = issueDetail.issue_link.map((l) =>
|
||||
l.id === linkId
|
||||
? {
|
||||
...l,
|
||||
title: formData.title,
|
||||
url: formData.url,
|
||||
}
|
||||
: l
|
||||
);
|
||||
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueDetail.id),
|
||||
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
|
||||
false
|
||||
);
|
||||
|
||||
await issueService
|
||||
.updateIssueLink(workspaceSlug as string, projectId as string, issueDetail.id, linkId, payload)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issueDetail.id));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
const issueLinkUpdate = (formData: IIssueLink, linkId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
updateIssueLink(workspaceSlug.toString(), projectId.toString(), issueId.toString(), linkId, formData);
|
||||
};
|
||||
|
||||
const handleDeleteLink = async (linkId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueDetail) return;
|
||||
|
||||
const updatedLinks = issueDetail.issue_link.filter((l) => l.id !== linkId);
|
||||
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueDetail.id),
|
||||
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
|
||||
false
|
||||
);
|
||||
|
||||
await issueService
|
||||
.deleteIssueLink(workspaceSlug as string, projectId as string, issueDetail.id, linkId)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issueDetail.id));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
const issueLinkDelete = (linkId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
deleteIssueLink(workspaceSlug.toString(), projectId.toString(), issueId.toString(), linkId);
|
||||
};
|
||||
|
||||
const handleCopyText = () => {
|
||||
@ -264,8 +204,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
}}
|
||||
data={selectedLinkToUpdate}
|
||||
status={selectedLinkToUpdate ? true : false}
|
||||
createIssueLink={handleCreateLink}
|
||||
updateIssueLink={handleUpdateLink}
|
||||
createIssueLink={issueLinkCreate}
|
||||
updateIssueLink={issueLinkUpdate}
|
||||
/>
|
||||
{workspaceSlug && projectId && issueDetail && (
|
||||
<DeleteIssueModal
|
||||
@ -659,7 +599,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
{
|
||||
<LinksList
|
||||
links={issueDetail.issue_link}
|
||||
handleDeleteLink={handleDeleteLink}
|
||||
handleDeleteLink={issueLinkDelete}
|
||||
handleEditLink={handleEditLink}
|
||||
userAuth={{
|
||||
isGuest: currentProjectRole === 5,
|
||||
|
@ -1,7 +1,7 @@
|
||||
// ui
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { CalendarDays } from "lucide-react";
|
||||
import { CalendarCheck } from "lucide-react";
|
||||
// helpers
|
||||
import {
|
||||
findHowManyDaysLeft,
|
||||
@ -51,8 +51,8 @@ export const ViewDueDateSelect: React.FC<Props> = ({
|
||||
issue.target_date === null
|
||||
? ""
|
||||
: issue.target_date < new Date().toISOString()
|
||||
? "text-red-600"
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
|
||||
? "text-red-600"
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
|
||||
}`}
|
||||
>
|
||||
<CustomDatePicker
|
||||
@ -67,7 +67,7 @@ export const ViewDueDateSelect: React.FC<Props> = ({
|
||||
>
|
||||
{issue.target_date ? (
|
||||
<>
|
||||
<CalendarDays className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<CalendarCheck className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>
|
||||
{areYearsEqual
|
||||
? renderShortDate(issue.target_date ?? "", "_ _")
|
||||
@ -76,7 +76,7 @@ export const ViewDueDateSelect: React.FC<Props> = ({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CalendarDays className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<CalendarCheck className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>Due Date</span>
|
||||
</>
|
||||
)}
|
||||
|
@ -1,7 +1,7 @@
|
||||
// ui
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { CalendarDays } from "lucide-react";
|
||||
import { CalendarClock } from "lucide-react";
|
||||
// helpers
|
||||
import { renderShortDate, renderShortDateWithYearFormat, renderShortMonthDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
@ -55,7 +55,7 @@ export const ViewStartDateSelect: React.FC<Props> = ({
|
||||
>
|
||||
{issue?.start_date ? (
|
||||
<>
|
||||
<CalendarDays className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<CalendarClock className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>
|
||||
{areYearsEqual
|
||||
? renderShortDate(issue?.start_date, "_ _")
|
||||
@ -64,7 +64,7 @@ export const ViewStartDateSelect: React.FC<Props> = ({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CalendarDays className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<CalendarClock className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>Start Date</span>
|
||||
</>
|
||||
)}
|
||||
|
@ -77,7 +77,6 @@ export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
|
||||
<Droppable
|
||||
key={`label.group.droppable.${label.id}`}
|
||||
droppableId={`label.group.droppable.${label.id}`}
|
||||
isCombineEnabled={!groupDragSnapshot.isDragging && !isUpdating}
|
||||
isDropDisabled={groupDragSnapshot.isDragging || isUpdating || isDropDisabled}
|
||||
>
|
||||
{(droppableProvided) => (
|
||||
|
@ -10,11 +10,17 @@ import {
|
||||
DropResult,
|
||||
Droppable,
|
||||
} from "@hello-pangea/dnd";
|
||||
|
||||
// store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useDraggableInPortal from "hooks/use-draggable-portal";
|
||||
// components
|
||||
import { CreateUpdateLabelInline, DeleteLabelModal, ProjectSettingLabelGroup } from "components/labels";
|
||||
import {
|
||||
CreateUpdateLabelInline,
|
||||
DeleteLabelModal,
|
||||
ProjectSettingLabelGroup,
|
||||
ProjectSettingLabelItem,
|
||||
} from "components/labels";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { EmptyState } from "components/common";
|
||||
@ -22,9 +28,6 @@ import { EmptyState } from "components/common";
|
||||
import emptyLabel from "public/empty-state/label.svg";
|
||||
// types
|
||||
import { IIssueLabel } from "types";
|
||||
//component
|
||||
import { ProjectSettingLabelItem } from "./project-setting-label-item";
|
||||
import useDraggableInPortal from "hooks/use-draggable-portal";
|
||||
|
||||
const LABELS_ROOT = "labels.root";
|
||||
|
||||
@ -137,7 +140,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||
isDropDisabled={isUpdating}
|
||||
>
|
||||
{(droppableProvided, droppableSnapshot) => (
|
||||
<div className={`mt-3`} ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
||||
<div className="mt-3" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
||||
{projectLabelsTree.map((label, index) => {
|
||||
if (label.children && label.children.length) {
|
||||
return (
|
||||
|
@ -69,11 +69,11 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
||||
state: "SUCCESS",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Module could not be created. Please try again.",
|
||||
message: err.detail ?? "Module could not be created. Please try again.",
|
||||
});
|
||||
postHogEventTracker("MODULE_CREATED", {
|
||||
state: "FAILED",
|
||||
@ -99,11 +99,11 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
||||
state: "SUCCESS",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Module could not be updated. Please try again.",
|
||||
message: err.detail ?? "Module could not be updated. Please try again.",
|
||||
});
|
||||
postHogEventTracker("MODULE_UPDATED", {
|
||||
state: "FAILED",
|
||||
|
@ -19,6 +19,7 @@ import { renderShortDate, renderShortMonthDate } from "helpers/date-time.helper"
|
||||
import { IModule } from "types";
|
||||
// constants
|
||||
import { MODULE_STATUS } from "constants/module";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
type Props = {
|
||||
module: IModule;
|
||||
@ -35,7 +36,11 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { module: moduleStore } = useMobxStore();
|
||||
const { module: moduleStore, user: userStore } = useMobxStore();
|
||||
|
||||
const { currentProjectRole } = userStore;
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
const moduleTotalIssues =
|
||||
module.backlog_issues +
|
||||
@ -59,8 +64,8 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
? !moduleTotalIssues || moduleTotalIssues === 0
|
||||
? "0 Issue"
|
||||
: moduleTotalIssues === module.completed_issues
|
||||
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}`
|
||||
: `${module.completed_issues}/${moduleTotalIssues} Issues`
|
||||
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}`
|
||||
: `${module.completed_issues}/${moduleTotalIssues} Issues`
|
||||
: "0 Issue";
|
||||
|
||||
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@ -217,28 +222,34 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
|
||||
<div className="z-10 flex items-center gap-1.5">
|
||||
{module.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</button>
|
||||
)}
|
||||
{isEditingAllowed &&
|
||||
(module.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</button>
|
||||
))}
|
||||
|
||||
<CustomMenu width="auto" ellipsis className="z-10">
|
||||
<CustomMenu.MenuItem onClick={handleEditModule}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
<span>Edit module</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleDeleteModule}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span>Delete module</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
{isEditingAllowed && (
|
||||
<>
|
||||
<CustomMenu.MenuItem onClick={handleEditModule}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
<span>Edit module</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleDeleteModule}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span>Delete module</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-3 w-3" />
|
||||
|
@ -19,6 +19,7 @@ import { renderShortDate, renderShortMonthDate } from "helpers/date-time.helper"
|
||||
import { IModule } from "types";
|
||||
// constants
|
||||
import { MODULE_STATUS } from "constants/module";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
type Props = {
|
||||
module: IModule;
|
||||
@ -35,7 +36,11 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { module: moduleStore } = useMobxStore();
|
||||
const { module: moduleStore, user: userStore } = useMobxStore();
|
||||
|
||||
const { currentProjectRole } = userStore;
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
const completionPercentage = ((module.completed_issues + module.cancelled_issues) / module.total_issues) * 100;
|
||||
|
||||
@ -194,29 +199,34 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{module.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites} className="z-[1]">
|
||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites} className="z-[1]">
|
||||
<Star className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
</button>
|
||||
)}
|
||||
{isEditingAllowed &&
|
||||
(module.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites} className="z-[1]">
|
||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites} className="z-[1]">
|
||||
<Star className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
</button>
|
||||
))}
|
||||
|
||||
<CustomMenu width="auto" verticalEllipsis buttonClassName="z-[1]">
|
||||
<CustomMenu.MenuItem onClick={handleEditModule}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
<span>Edit module</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleDeleteModule}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span>Delete module</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
{isEditingAllowed && (
|
||||
<>
|
||||
<CustomMenu.MenuItem onClick={handleEditModule}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
<span>Edit module</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleDeleteModule}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span>Delete module</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-3 w-3" />
|
||||
|
@ -256,10 +256,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
moduleDetails.total_issues === 0
|
||||
? "0 Issue"
|
||||
: moduleDetails.total_issues === moduleDetails.completed_issues
|
||||
? moduleDetails.total_issues > 1
|
||||
? `${moduleDetails.total_issues}`
|
||||
: `${moduleDetails.total_issues}`
|
||||
: `${moduleDetails.completed_issues}/${moduleDetails.total_issues}`;
|
||||
? moduleDetails.total_issues > 1
|
||||
? `${moduleDetails.total_issues}`
|
||||
: `${moduleDetails.total_issues}`
|
||||
: `${moduleDetails.completed_issues}/${moduleDetails.total_issues}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -311,7 +311,11 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<CustomSelect
|
||||
customButton={
|
||||
<span
|
||||
className={`flex h-6 w-20 cursor-default items-center justify-center rounded-sm text-sm ${moduleStatus?.textColor} ${moduleStatus?.bgColor}`}
|
||||
className="flex h-6 w-20 cursor-default items-center justify-center rounded-sm text-center text-xs"
|
||||
style={{
|
||||
color: moduleStatus ? moduleStatus.color : "#a3a3a2",
|
||||
backgroundColor: moduleStatus ? `${moduleStatus.color}20` : "#a3a3a220",
|
||||
}}
|
||||
>
|
||||
{moduleStatus?.label ?? "Backlog"}
|
||||
</span>
|
||||
@ -459,7 +463,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
{progressPercentage ? (
|
||||
<span className="flex h-5 w-9 items-center justify-center rounded bg-amber-50 text-xs font-medium text-amber-500">
|
||||
<span className="flex h-5 w-9 items-center justify-center rounded bg-amber-500/20 text-xs font-medium text-amber-500">
|
||||
{progressPercentage ? `${progressPercentage}%` : ""}
|
||||
</span>
|
||||
) : (
|
||||
|
@ -60,11 +60,11 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Page could not be created. Please try again.",
|
||||
message: err.detail ?? "Page could not be created. Please try again.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"PAGE_CREATED",
|
||||
@ -104,11 +104,11 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Page could not be updated. Please try again.",
|
||||
message: err.detail ?? "Page could not be updated. Please try again.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"PAGE_UPDATED",
|
||||
|
@ -154,6 +154,7 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
||||
const userCanChangeAccess = isCurrentUserOwner;
|
||||
const userCanArchive = isCurrentUserOwner || currentProjectRole === EUserWorkspaceRoles.ADMIN;
|
||||
const userCanDelete = isCurrentUserOwner || currentProjectRole === EUserWorkspaceRoles.ADMIN;
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -208,17 +209,19 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
||||
<p className="text-sm text-custom-text-200">{render24HourFormatTime(page.updated_at)}</p>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip tooltipContent={`${page.is_favorite ? "Remove from favorites" : "Mark as favorite"}`}>
|
||||
{page.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 fill-orange-400 text-orange-400" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<Star className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</Tooltip>
|
||||
{isEditingAllowed && (
|
||||
<Tooltip tooltipContent={`${page.is_favorite ? "Remove from favorites" : "Mark as favorite"}`}>
|
||||
{page.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 fill-orange-400 text-orange-400" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<Star className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
{userCanChangeAccess && (
|
||||
<Tooltip
|
||||
tooltipContent={`${
|
||||
@ -255,7 +258,7 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{userCanDelete && (
|
||||
{userCanDelete && isEditingAllowed && (
|
||||
<CustomMenu.MenuItem onClick={handleDeletePage}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
@ -266,7 +269,7 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{userCanEdit && (
|
||||
{userCanEdit && isEditingAllowed && (
|
||||
<CustomMenu.MenuItem onClick={handleEditPage}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
@ -274,7 +277,7 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{userCanArchive && (
|
||||
{userCanArchive && isEditingAllowed && (
|
||||
<CustomMenu.MenuItem onClick={handleArchivePage}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Archive className="h-3 w-3" />
|
||||
|
@ -54,7 +54,14 @@ export const ProjectViewForm: React.FC<Props> = observer(({ handleFormSubmit, ha
|
||||
|
||||
// for removing filters from a key
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!value) return;
|
||||
// If value is null then remove all the filters of that key
|
||||
if (!value) {
|
||||
setValue("query_data", {
|
||||
...selectedFilters,
|
||||
[key]: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newValues = selectedFilters?.[key] ?? [];
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
// ui
|
||||
import { ChevronDown, PenSquare, Search } from "lucide-react";
|
||||
import { ChevronUp, PenSquare, Search } from "lucide-react";
|
||||
// hooks
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// components
|
||||
@ -37,7 +37,6 @@ export const WorkspaceSidebarQuickAction = observer(() => {
|
||||
}}
|
||||
fieldsToShow={["all"]}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`mt-4 flex w-full cursor-pointer items-center justify-between px-4 ${
|
||||
isSidebarCollapsed ? "flex-col gap-1" : "gap-2"
|
||||
@ -74,10 +73,7 @@ export const WorkspaceSidebarQuickAction = observer(() => {
|
||||
isSidebarCollapsed ? "hidden" : "block"
|
||||
}`}
|
||||
>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className="rotate-0 transform !text-custom-sidebar-text-300 transition-transform duration-300 group-hover:rotate-180"
|
||||
/>
|
||||
<ChevronUp className="h-4 w-4 rotate-180 transform !text-custom-sidebar-text-300 transition-transform duration-300 group-hover:rotate-0" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { TIssueOrderByOptions } from "types";
|
||||
import { LayersIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
||||
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip } from "lucide-react";
|
||||
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarClock, CalendarCheck } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
import { ISvgIcons } from "@plane/ui/src/icons/type";
|
||||
|
||||
@ -36,7 +36,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
ascendingOrderTitle: "New",
|
||||
descendingOrderKey: "target_date",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: CalendarDays,
|
||||
icon: CalendarCheck,
|
||||
},
|
||||
estimate: {
|
||||
title: "Estimate",
|
||||
@ -68,7 +68,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
ascendingOrderTitle: "New",
|
||||
descendingOrderKey: "start_date",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: CalendarDays,
|
||||
icon: CalendarClock,
|
||||
},
|
||||
state: {
|
||||
title: "State",
|
||||
|
@ -35,7 +35,7 @@
|
||||
"dotenv": "^16.0.3",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.293.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react-lite": "^4.0.3",
|
||||
"next": "^14.0.3",
|
||||
|
@ -100,7 +100,9 @@ const ProfileActivityPage: NextPageWithLayout = () => {
|
||||
activityItem.field !== "estimate" ? (
|
||||
<span className="text-custom-text-200">
|
||||
created{" "}
|
||||
<Link href={`/${workspaceSlug}/projects/${activityItem.project}/issues/${activityItem.issue}`}>
|
||||
<Link
|
||||
href={`/${activityItem.workspace_detail.slug}/projects/${activityItem.project}/issues/${activityItem.issue}`}
|
||||
>
|
||||
<span className="inline-flex items-center hover:underline">
|
||||
this issue. <ExternalLinkIcon className="ml-1 h-3.5 w-3.5" />
|
||||
</span>
|
||||
@ -156,7 +158,9 @@ const ProfileActivityPage: NextPageWithLayout = () => {
|
||||
{activityItem.actor_detail.first_name} Bot
|
||||
</span>
|
||||
) : (
|
||||
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
|
||||
<Link
|
||||
href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`}
|
||||
>
|
||||
<span className="text-gray font-medium">
|
||||
{activityItem.actor_detail.display_name}
|
||||
</span>
|
||||
|
@ -33,7 +33,7 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
|
||||
const [isPageLoading, setIsPageLoading] = useState(true);
|
||||
|
||||
const {
|
||||
appConfig: { envConfig },
|
||||
user: { currentUser },
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
@ -74,20 +74,11 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!envConfig) return;
|
||||
if (!currentUser) return;
|
||||
|
||||
const enableEmailPassword =
|
||||
envConfig?.email_password_login ||
|
||||
!(
|
||||
envConfig?.email_password_login ||
|
||||
envConfig?.magic_login ||
|
||||
envConfig?.google_client_id ||
|
||||
envConfig?.github_client_id
|
||||
);
|
||||
|
||||
if (!enableEmailPassword) router.push("/profile");
|
||||
if (currentUser.is_password_autoset) router.push("/profile");
|
||||
else setIsPageLoading(false);
|
||||
}, [envConfig, router]);
|
||||
}, [currentUser, router]);
|
||||
|
||||
if (isPageLoading)
|
||||
return (
|
||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 13 MiB |
BIN
web/public/instance-setup-done.webp
Normal file
BIN
web/public/instance-setup-done.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 88 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user