Compare commits

..
Author SHA1 Message Date
sriram veeraghanta 936452758a sync: preview to master 2023-12-12 17:51:09 +05:30
sriram veeraghantaandGitHub 4fdac437e9 Merge pull request #3097 from makeplane/develop
promote: develop to preview
2023-12-12 17:41:46 +05:30
Aaryan KhandelwalandGitHub 48fe6f9b9a chore: update self hosted sign in form cta in the web and space apps (#3096)
* chore: update self hosted sign in form cta

* chore: update self hosted sign in form cta in the space app
2023-12-12 17:40:02 +05:30
sriram veeraghantaandGitHub 05d675c138 Merge pull request #3086 from makeplane/fix/issue_layout_permission_validation
chore: issue layout quick add issue button permission validation
2023-12-12 17:36:07 +05:30
sriram veeraghantaandGitHub 09b4f6dedd Merge pull request #3091 from makeplane/chore/project-member-leave
chore: guest and viewer can leave project
2023-12-12 17:35:14 +05:30
sriram veeraghantaandGitHub 449ac06fd7 Merge pull request #3093 from makeplane/develop
promote: develop to preview
2023-12-12 17:01:57 +05:30
LAKHAN BAHETI c7d50bb7ce fix: member role project leave 2023-12-12 17:00:59 +05:30
NarayanBavisetti 90de11c08d chore: guest and viewer can leave project 2023-12-12 16:22:33 +05:30
Anmol Singh BhatiaandGitHub 24c02495aa chore: instance setup done image update (#3090) 2023-12-12 16:05:59 +05:30
NikhilandGitHub 1795916042 fix: debug toolbar import error when running on production setting with DEBUG on (#3085) 2023-12-12 11:54:52 +05:30
Anmol Singh Bhatia 43cbe44a35 chore: issue layout quick add issue button permission validation 2023-12-12 11:49:43 +05:30
Aaryan KhandelwalandGitHub 361ee16567 fix: modal size not changing when switching between modes (#3082) 2023-12-12 00:44:58 +05:30
NikhilandGitHub de24b02a0a fix: user login after account deactivation (#3073) 2023-12-11 22:44:37 +05:30
Anmol Singh BhatiaandGitHub 1d3745157d style: cycle and module sidebar improvement (#3076)
* style: cycle and module sidebar improvement

* chore: issue attachment error message updated
2023-12-11 22:27:28 +05:30
Aaryan KhandelwalandGitHub 07c15fcc1f fix: gantt issues not getting updated (#3075) 2023-12-11 22:26:57 +05:30
Anmol Singh BhatiaandGitHub c33cfeb227 chore: issue link mobx integration (#3072)
* chore: issue link mobx integration

* chore: issue link code refactor and build fix
2023-12-11 22:26:29 +05:30
Aaryan KhandelwalandGitHub f119d702c7 chore: set an expiry of 30 days for the access and refresh tokens (#3071) 2023-12-11 22:25:48 +05:30
Aaryan KhandelwalandGitHub 0f752f93b6 fix: pre-fill cycle and module details when editing an issue (#3070)
* fix: prefill cycle and module when editing an issue

* chore: remove console.log
2023-12-11 22:25:14 +05:30
Lakhan BahetiandGitHub ae2e1a4b64 fix: delete issue mutation & toast (#3079)
* fix: delete issue mutation

* fix: toast on delete issue

* fix: activity to issue detail redirection
2023-12-11 22:24:41 +05:30
Lakhan BahetiandGitHub 472a5d8047 fix: spreadsheet layout alignment (#3080) 2023-12-11 22:23:49 +05:30
Prateek ShouryaandGitHub 3b12332704 fix: issue with clear filter option not working for filter groups in create view modal. (#3081) 2023-12-11 22:23:20 +05:30
Aaryan KhandelwalandGitHub c7cad452ab fix: disable label combining for labels inside a group (#3069) 2023-12-11 17:31:22 +05:30
Anmol Singh BhatiaandGitHub 82c0ee00a3 chore: profile setting bug fixes (#3068)
* chore: change password page redirection fix

* chore: profile activity profile redirection fix
2023-12-11 17:30:17 +05:30
Lakhan BahetiandGitHub 8041b23a63 chore: access restriction for lower roles (#3067) 2023-12-11 17:29:43 +05:30
Anmol Singh BhatiaandGitHub f38278f465 chore: user permission related fix (#3066)
* chore: page action user permission validation

* chore: cycle & module action user permission validation

* chore: issue quick action user permission validation

* chore: spreadsheet layout improvement
2023-12-11 17:29:10 +05:30
Lakhan BahetiandGitHub 73b58e91ee fix: spreadsheet layout sub issues overlapping (#3064)
* fix: spreadsheet layout sub issues overlapping

* fix: logs
2023-12-11 17:28:44 +05:30
Prateek ShouryaandGitHub b515c0ffa6 fix: issue with GPT assistant response not updating on new requests. (#3063) 2023-12-11 17:27:52 +05:30
Aaryan KhandelwalandGitHub b629263bc2 fix: icon consistency for issue properties (#3065) 2023-12-11 17:27:29 +05:30
sriram veeraghanta 1bf064df15 sync: preview with develop 2023-12-11 16:26:11 +05:30
NikhilandGitHub 9918d5242c dev: fix oauth configuration check (#3062) 2023-12-11 16:17:11 +05:30
Aaryan KhandelwalandGitHub dd87bd0ee2 style: update OAuth buttons UI (#3061) 2023-12-11 15:52:21 +05:30
Lakhan BahetiandGitHub 26d37fbd38 chore: error handling for user permission while issue drag & drop (#3060)
* filx: gantt chart quick add permission

* fix: permission toast on drag & drop issues
2023-12-11 15:51:40 +05:30
sriram veeraghantaandGitHub 0f28008fa5 Merge pull request #3056 from makeplane/promote/develop-preview
Promote: Develop to Preview
2023-12-10 15:51:41 +05:30
sriram veeraghantaandAaryan Khandelwal 5b0066140f chore: format all files in monorepo (#3054)
* chore: format all files in the project

* fix: removing @types/react from dependencies

* fix: adding prettier and eslint config

* chore: format files

* fix: upgrading turbo version

* chore: ignoring warnings and adding todos

* fix: updated the type of bubble menu item in the document editor

* chore: format files

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-12-10 15:50:45 +05:30
Aaryan Khandelwalandsriram veeraghanta e5ae139178 chore: update lucide-react package to the latest version 2023-12-10 15:50:44 +05:30
Aaryan Khandelwalandsriram veeraghanta 02e2c6f848 style: update the ui of the slash command menu 2023-12-10 15:50:44 +05:30
0ceb9974f6 dev: hub compose file update (#2376) (#2444) (#2445)
* docker-compose-hub modified for envs

* bug:fix recent page hiding last item on scroll #1468 (#2411)

* wip

* fixed the AMD build on ARM

---------

Co-authored-by: Manish Gupta <59428681+manishg3@users.noreply.github.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2023-10-16 13:00:58 +05:30
Prashant IndurkarandGitHub e0fcc0c876 bug:fix recent page hiding last item on scroll #1468 (#2411) 2023-10-12 16:35:00 +05:30
110 changed files with 911 additions and 844 deletions
-6
View File
@@ -21,7 +21,6 @@ from plane.app.views import (
IssueArchiveViewSet,
IssueRelationViewSet,
IssueDraftViewSet,
TransferProjectIssueEndpoint,
)
@@ -313,9 +312,4 @@ urlpatterns = [
),
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/transfer-issues/",
TransferProjectIssueEndpoint.as_view(),
name="transfer-project-issue",
),
]
-1
View File
@@ -80,7 +80,6 @@ from .issue import (
IssueReactionViewSet,
IssueRelationViewSet,
IssueDraftViewSet,
TransferProjectIssueEndpoint,
)
from .auth_extended import (
@@ -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,
+4 -67
View File
@@ -217,9 +217,7 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(
issue_queryset, many=True, fields=fields if fields else None
).data
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
@@ -1064,9 +1062,7 @@ class IssueArchiveViewSet(BaseViewSet):
else issue_queryset.filter(parent__isnull=True)
)
issues = IssueLiteSerializer(
issue_queryset, many=True, fields=fields if fields else None
).data
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
@@ -1551,9 +1547,7 @@ class IssueDraftViewSet(BaseViewSet):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(
issue_queryset, many=True, fields=fields if fields else None
).data
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
@@ -1632,61 +1626,4 @@ class IssueDraftViewSet(BaseViewSet):
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
)
return Response(status=status.HTTP_204_NO_CONTENT)
class TransferProjectIssueEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id, issue_id):
transfer_project = request.GET.get("transfer_project", False)
if not transfer_project:
return Response(
{"error": "Transfer project id is required"},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the issue
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=issue_id
)
# Get the transfered state
state = State.objects.filter(
workspace__slug=slug, project_id=transfer_project, group=issue.state.group
).first()
# Get all the labels attached to issues
label_names = [label.name.lower() for label in issue.labels.all()]
query = Q()
for name in label_names:
query |= Q(name__iexact=name)
matching_labels = (
Label.objects.filter(workspace__slug=slug, project_id=transfer_project)
.filter(query)
.values("name", "color")
)
# assignees
assignee_ids = [assignee.id for assignee in issue.assignees.all()]
assignees = ProjectMember.objects.filter(
workspace__slug=slug, project_id=project_id, member_id__in=assignee_ids
).values("member__display_name", "member__id", "member__avatar")
return Response(
{
"state": {
"name": state.name,
"id": state.id,
"group": state.group,
},
"lables": matching_labels,
"members": assignees,
}
)
return Response(status=status.HTTP_204_NO_CONTENT)
-6
View File
@@ -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(
{
+13
View File
@@ -42,6 +42,7 @@ from plane.app.permissions import (
WorkspaceUserPermission,
ProjectBasePermission,
ProjectMemberPermission,
ProjectLitePermission,
)
from plane.db.models import (
@@ -602,6 +603,18 @@ class ProjectMemberViewSet(BaseViewSet):
ProjectMemberPermission,
]
def get_permissions(self):
if self.action == "leave":
self.permission_classes = [
ProjectLitePermission,
]
else:
self.permission_classes = [
ProjectMemberPermission,
]
return super(ProjectMemberViewSet, self).get_permissions()
search_fields = [
"member__display_name",
"member__first_name",
+7 -4
View File
@@ -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
@@ -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} />
</>
);
};
+2 -2
View File
@@ -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>
</>
@@ -130,7 +130,7 @@ export const SelfHostedSignInForm: React.FC<Props> = (props) => {
/>
</div>
<Button type="submit" variant="primary" className="w-full" size="xl" loading={isSubmitting}>
Go to board
Continue
</Button>
<p className="text-xs text-onboarding-text-200">
When you click the button above, you agree with our{" "}
@@ -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}
@@ -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} />
</>
);
};
+2 -2
View File
@@ -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>
</>
@@ -130,7 +130,7 @@ export const SelfHostedSignInForm: React.FC<Props> = (props) => {
/>
</div>
<Button type="submit" variant="primary" className="w-full" size="xl" loading={isSubmitting}>
Go to workspace
Continue
</Button>
<p className="text-xs text-onboarding-text-200">
When you click the button above, you agree with our{" "}
@@ -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>
)}
+5 -5
View File
@@ -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>
+18 -12
View File
@@ -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">
+16 -11
View File
@@ -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">
+3 -9
View File
@@ -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>
+4 -4
View File
@@ -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.",
});
});
};
+5 -5
View File
@@ -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>
) : (
+12 -2
View File
@@ -27,11 +27,21 @@ type Props = {
viewId?: string
) => Promise<IIssue | undefined>;
viewId?: string;
disableIssueCreation?: boolean;
};
export const IssueGanttSidebar: React.FC<Props> = (props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { title, blockUpdateHandler, blocks, enableReorder, enableQuickIssueCreate, quickAddCallback, viewId } = props;
const {
title,
blockUpdateHandler,
blocks,
enableReorder,
enableQuickIssueCreate,
quickAddCallback,
viewId,
disableIssueCreation,
} = props;
const router = useRouter();
const { cycleId } = router.query;
@@ -160,7 +170,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
)}
{droppableProvided.placeholder}
</>
{enableQuickIssueCreate && (
{enableQuickIssueCreate && !disableIssueCreation && (
<GanttInlineCreateIssueForm quickAddCallback={quickAddCallback} viewId={viewId} />
)}
</div>
+17 -10
View File
@@ -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"
+20 -13
View File
@@ -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>
);
});
+17 -10
View File
@@ -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"
+19 -9
View File
@@ -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>
);
+17 -10
View File
@@ -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>
</>
+18 -11
View File
@@ -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>
);
+1 -1
View File
@@ -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
+19 -1
View File
@@ -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));
};
+3 -4
View File
@@ -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(
@@ -16,6 +16,8 @@ import {
IProjectIssuesFilterStore,
IViewIssuesFilterStore,
} from "store/issues";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type Props = {
issuesFilterStore:
@@ -41,7 +43,14 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
const { issuesFilterStore, issues, groupedIssueIds, layout, showWeekends, quickActions, quickAddCallback, viewId } =
props;
const { calendar: calendarStore } = useMobxStore();
const {
calendar: calendarStore,
projectIssues: issueStore,
user: { currentProjectRole },
} = useMobxStore();
const { enableIssueCreation } = issueStore?.viewFlags || {};
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const calendarPayload = calendarStore.calendarPayload;
@@ -71,6 +80,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
issues={issues}
groupedIssueIds={groupedIssueIds}
enableQuickIssueCreate
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
quickActions={quickActions}
quickAddCallback={quickAddCallback}
viewId={viewId}
@@ -85,6 +95,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
issues={issues}
groupedIssueIds={groupedIssueIds}
enableQuickIssueCreate
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
quickActions={quickActions}
quickAddCallback={quickAddCallback}
viewId={viewId}
@@ -26,6 +26,7 @@ type Props = {
groupedIssueIds: IGroupedIssues;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
enableQuickIssueCreate?: boolean;
disableIssueCreation?: boolean;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
@@ -43,6 +44,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
groupedIssueIds,
quickActions,
enableQuickIssueCreate,
disableIssueCreation,
quickAddCallback,
viewId,
} = props;
@@ -86,7 +88,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
ref={provided.innerRef}
>
<CalendarIssueBlocks issues={issues} issueIdList={issueIdList} quickActions={quickActions} />
{enableQuickIssueCreate && (
{enableQuickIssueCreate && !disableIssueCreation && (
<div className="px-2 py-1">
<CalendarQuickAddIssueForm
formKey="target_date"
@@ -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(),
@@ -25,6 +25,7 @@ type Props = {
week: ICalendarWeek | undefined;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
enableQuickIssueCreate?: boolean;
disableIssueCreation?: boolean;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
@@ -42,6 +43,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
week,
quickActions,
enableQuickIssueCreate,
disableIssueCreation,
quickAddCallback,
viewId,
} = props;
@@ -69,6 +71,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
groupedIssueIds={groupedIssueIds}
quickActions={quickActions}
enableQuickIssueCreate={enableQuickIssueCreate}
disableIssueCreation={disableIssueCreation}
quickAddCallback={quickAddCallback}
viewId={viewId}
/>
@@ -51,23 +51,23 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
const issuesResponse = issueStore.getIssues;
const issueIds = (issueStore.getIssuesIds ?? []) as TUnGroupedIssues;
const { enableIssueCreation } = issueStore?.viewFlags || {};
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 +80,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
@@ -88,6 +88,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
quickAddCallback={issueStore.quickAddIssue}
viewId={viewId}
enableQuickIssueCreate
disableIssueCreation={!enableIssueCreation || !isAllowed}
/>
)}
enableBlockLeftResize={isAllowed}
@@ -102,8 +103,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) => {
@@ -335,7 +330,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
projects={workspaceProjects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted}
disableIssueCreation={true}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
enableQuickIssueCreate={enableQuickAdd}
currentStore={currentStore}
quickAddCallback={issueStore?.quickAddIssue}
@@ -149,7 +149,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
</Droppable>
<div className="sticky bottom-0 z-[0] w-full flex-shrink-0 bg-custom-background-90 py-1">
{enableQuickIssueCreate && (
{enableQuickIssueCreate && !disableIssueCreation && (
<KanBanQuickAddIssueForm
formKey="name"
groupId={getValueFromObject(_list, listKey) as 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"
/>
)}
@@ -109,7 +109,7 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
/>
)}
{enableIssueQuickAdd && (
{enableIssueQuickAdd && !disableIssueCreation && (
<div className="sticky bottom-0 z-[1] w-full flex-shrink-0">
<ListQuickAddIssueForm
prePopulatedData={prePopulateQuickAddData(group_by, getValueFromObject(_list, listKey))}
@@ -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>
</>
);
@@ -50,7 +50,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
user: userStore,
} = useMobxStore();
const { enableInlineEditing, enableQuickAdd } = issueStore?.viewFlags || {};
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {};
const { currentProjectRole } = userStore;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
@@ -120,6 +120,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
quickAddCallback={issueStore.quickAddIssue}
viewId={viewId}
enableQuickCreateIssue={enableQuickAdd}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
/>
);
});
@@ -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"
}`}
>
@@ -33,6 +33,7 @@ type Props = {
viewId?: string;
canEditProperties: (projectId: string | undefined) => boolean;
enableQuickCreateIssue?: boolean;
disableIssueCreation?: boolean;
};
export const SpreadsheetView: React.FC<Props> = observer((props) => {
@@ -50,6 +51,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
viewId,
canEditProperties,
enableQuickCreateIssue,
disableIssueCreation,
} = props;
// states
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
@@ -77,6 +79,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 +93,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,17 +143,13 @@ 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>
<div className="border-t border-custom-border-100">
<div className="z-5 sticky bottom-0 left-0 mb-3">
{enableQuickCreateIssue && (
{enableQuickCreateIssue && !disableIssueCreation && (
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
)}
</div>
+6 -6
View File
@@ -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,
+14 -1
View File
@@ -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}
+13 -1
View File
@@ -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>
+7 -11
View File
@@ -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>
+13 -73
View File
@@ -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 (
+4 -4
View File
@@ -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",
+35 -24
View File
@@ -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" />
+32 -22
View File
@@ -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" />
+10 -6
View File
@@ -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",
+17 -14
View File
@@ -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" />
+1 -1
View File
@@ -168,7 +168,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
);
})}
</CustomSelect>
{isAdmin && (
{(isAdmin || memberDetails.id === currentProjectMemberInfo?.member.id) && (
<Tooltip
tooltipContent={
memberDetails.id === currentProjectMemberInfo?.member.id ? "Leave project" : "Remove member"
+8 -1
View File
@@ -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
+3 -3
View File
@@ -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",
+6 -2
View File
@@ -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>
+4 -13
View File
@@ -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

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