Compare commits

...
20 Commits
Author SHA1 Message Date
sriram veeraghantaandGitHub c6e3f1b932 Merge pull request #3535 from makeplane/preview
release: 0.15-dev
2024-02-01 15:01:49 +05:30
7d08a57be6 chore: remove build warnings (#3534)
* chore: remove build warnings

* fix: posthog wrapper fixes

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-02-01 14:53:08 +05:30
Anmol Singh BhatiaandGitHub b0ad48e35a chore: enhance loading state for setting page updates (#3533) 2024-02-01 13:35:01 +05:30
Anmol Singh BhatiaandGitHub d68669df51 fix: resolved issue with resetting the draft issue form (#3532) 2024-02-01 13:33:08 +05:30
Anmol Singh BhatiaandGitHub 4e600e4e9b fix: update cycle error (#3530)
* fix: update cycle response and implement required changes

* chore: update cycle response
2024-02-01 13:32:37 +05:30
Lakhan BahetiandGitHub 4fc4da7982 fix: email-template heading overview (#3525)
* fix: email-template heading

* fix: typo
2024-01-31 20:16:47 +05:30
M. PalanikannanandGitHub 6f210e1f4b chore: removing unnecessary code (#3524)
* fix: adding back enter key extension with mentions

* removed unncessary code
2024-01-31 19:38:37 +05:30
Aaryan KhandelwalandGitHub f7803dab56 chore: added validation to cloud hostname field (#3523) 2024-01-31 18:06:57 +05:30
M. PalanikannanandGitHub 70172f8e3d fix: adding back enter key extension with mentions (#3499) 2024-01-31 18:06:12 +05:30
M. PalanikannanandGitHub 21bc668a56 fix: horizontal rule no more causes issues on last node (#3507) 2024-01-31 18:05:06 +05:30
Anmol Singh BhatiaandGitHub dc5a5f4a91 fix/draft issue modal focus issue fix (#3522) 2024-01-31 17:47:47 +05:30
Anmol Singh BhatiaandGitHub 2c67aced15 fix: cycle and module sidebar mutation fix (#3521)
* fix: cycle and module sidebar mutation

* fix: cycle and module calendar drag n drop fix
2024-01-31 17:09:24 +05:30
Ramesh Kumar ChandraandGitHub 3a4c893368 Style/workspace project settings layout (#3520)
* style: project settings layout for mobile screens

* style: workspace settings layout for mobile screens
2024-01-31 15:42:20 +05:30
0d036e6bf5 refactor: dropdown button components (#3508)
* refactor: dropdown button components

* chore: dropdowns accessibility improvement

* chore: update module dropdown

* chore: update option content

* chore: hide icon from the peek overview

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2024-01-31 15:36:55 +05:30
rahulrameshaandGitHub 3ef0570f6a Fix all issues custom views by defining list layout for my-issue (#3517) 2024-01-31 13:59:46 +05:30
Prateek ShouryaandGitHub c9d2ea36b8 chore: allow guests/ viewers to comment. (#3515) 2024-01-30 20:13:28 +05:30
f0836ceb10 chore: email-template changes overview (#3494)
* style: template design

* fix: project url and actors

* fix: heading jinja

* fix: typo in workspace

* fix: issue title change

* fix: top project, issue redirection

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-01-30 20:12:59 +05:30
Anmol Singh BhatiaandGitHub 888665783e feat: issue highlighting (#3514)
* chore: kanban issue highlight

* chore: issue highlighting added
2024-01-30 20:12:39 +05:30
Prateek ShouryaandGitHub 817737b2c0 improvement: add hide/ unhide icon for all password field in the platform. (#3513) 2024-01-30 20:11:16 +05:30
rahulrameshaandGitHub 638c1e21c9 fix quick add issue creation in cycles (#3511) 2024-01-30 18:03:49 +05:30
91 changed files with 2592 additions and 3525 deletions
-1
View File
@@ -33,7 +33,6 @@ class CycleWriteSerializer(BaseSerializer):
class CycleSerializer(BaseSerializer):
owned_by = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
cancelled_issues = serializers.IntegerField(read_only=True)
@@ -148,10 +148,12 @@ def send_email_notification(
template_data = []
total_changes = 0
comments = []
actors_involved = []
for actor_id, changes in data.items():
actor = User.objects.get(pk=actor_id)
total_changes = total_changes + len(changes)
comment = changes.pop("comment", False)
actors_involved.append(actor_id)
if comment:
comments.append(
{
@@ -184,13 +186,14 @@ def send_email_notification(
}
)
summary = "updates were made to the issue by"
summary = "Updates were made to the issue by"
# Send the mail
subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
context = {
"data": template_data,
"summary": summary,
"actors_involved": len(set(actors_involved)),
"issue": {
"issue_identifier": f"{str(issue.project.identifier)}-{str(issue.sequence_id)}",
"name": issue.name,
@@ -200,6 +203,9 @@ def send_email_notification(
"email": receiver.email,
},
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
"project_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/",
"workspace":str(issue.project.workspace.slug),
"project": str(issue.project.name),
"user_preference": f"{base_api}/profile/preferences/email",
"comments": comments,
}
File diff suppressed because it is too large Load Diff
@@ -1,109 +0,0 @@
import { TextSelection } from "prosemirror-state";
import { InputRule, mergeAttributes, Node, nodeInputRule, wrappingInputRule } from "@tiptap/core";
/**
* Extension based on:
* - Tiptap HorizontalRule extension (https://tiptap.dev/api/nodes/horizontal-rule)
*/
export interface HorizontalRuleOptions {
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
horizontalRule: {
/**
* Add a horizontal rule
*/
setHorizontalRule: () => ReturnType;
};
}
}
export const HorizontalRule = Node.create<HorizontalRuleOptions>({
name: "horizontalRule",
addOptions() {
return {
HTMLAttributes: {},
};
},
group: "block",
addAttributes() {
return {
color: {
default: "#dddddd",
},
};
},
parseHTML() {
return [
{
tag: `div[data-type="${this.name}"]`,
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"div",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
"data-type": this.name,
}),
["div", {}],
];
},
addCommands() {
return {
setHorizontalRule:
() =>
({ chain }) => {
return (
chain()
.insertContent({ type: this.name })
// set cursor after horizontal rule
.command(({ tr, dispatch }) => {
if (dispatch) {
const { $to } = tr.selection;
const posAfter = $to.end();
if ($to.nodeAfter) {
tr.setSelection(TextSelection.create(tr.doc, $to.pos));
} else {
// add node after horizontal rule if its the end of the document
const node = $to.parent.type.contentMatch.defaultType?.create();
if (node) {
tr.insert(posAfter, node);
tr.setSelection(TextSelection.create(tr.doc, posAfter));
}
}
tr.scrollIntoView();
}
return true;
})
.run()
);
},
};
},
addInputRules() {
return [
new InputRule({
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
handler: ({ state, range, match }) => {
state.tr.replaceRangeWith(range.from, range.to, this.type.create());
},
}),
];
},
});
@@ -1,26 +1,25 @@
import StarterKit from "@tiptap/starter-kit";
import TiptapUnderline from "@tiptap/extension-underline";
import TextStyle from "@tiptap/extension-text-style";
import { Color } from "@tiptap/extension-color";
import TaskItem from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list";
import TextStyle from "@tiptap/extension-text-style";
import TiptapUnderline from "@tiptap/extension-underline";
import StarterKit from "@tiptap/starter-kit";
import { Markdown } from "tiptap-markdown";
import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { Table } from "src/ui/extensions/table/table";
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { TableRow } from "src/ui/extensions/table/table-row/table-row";
import { HorizontalRule } from "src/ui/extensions/horizontal-rule";
import { ImageExtension } from "src/ui/extensions/image";
import { isValidHttpUrl } from "src/lib/utils";
import { Mentions } from "src/ui/mentions";
import { CustomKeymap } from "src/ui/extensions/keymap";
import { CustomCodeBlockExtension } from "src/ui/extensions/code";
import { CustomQuoteExtension } from "src/ui/extensions/quote";
import { ListKeymap } from "src/ui/extensions/custom-list-keymap";
import { CustomKeymap } from "src/ui/extensions/keymap";
import { CustomQuoteExtension } from "src/ui/extensions/quote";
import { DeleteImage } from "src/types/delete-image";
import { IMentionSuggestion } from "src/types/mention-suggestion";
@@ -55,7 +54,9 @@ export const CoreEditorExtensions = (
},
code: false,
codeBlock: false,
horizontalRule: false,
horizontalRule: {
HTMLAttributes: { class: "mt-4 mb-4" },
},
blockquote: false,
dropcursor: {
color: "rgba(var(--color-text-100))",
@@ -104,7 +105,6 @@ export const CoreEditorExtensions = (
transformCopiedText: true,
transformPastedText: true,
}),
HorizontalRule,
Table,
TableHeader,
TableCell,
@@ -10,6 +10,11 @@ export interface CustomMentionOptions extends MentionOptions {
}
export const CustomMention = Mention.extend<CustomMentionOptions>({
addStorage(this) {
return {
mentionsOpen: false,
};
},
addAttributes() {
return {
id: {
@@ -14,6 +14,7 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
props.editor.storage.mentionsOpen = true;
reactRenderer = new ReactRenderer(MentionList, {
props,
editor: props.editor,
@@ -45,10 +46,18 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
return true;
}
// @ts-ignore
return reactRenderer?.ref?.onKeyDown(props);
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
if (navigationKeys.includes(props.event.key)) {
// @ts-ignore
reactRenderer?.ref?.onKeyDown(props);
event?.stopPropagation();
return true;
}
return false;
},
onExit: () => {
onExit: (props: { editor: Editor; event: KeyboardEvent }) => {
props.editor.storage.mentionsOpen = false;
popup?.[0].destroy();
reactRenderer?.destroy();
},
@@ -11,7 +11,6 @@ import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { Table } from "src/ui/extensions/table/table";
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
import { TableRow } from "src/ui/extensions/table/table-row/table-row";
import { HorizontalRule } from "src/ui/extensions/horizontal-rule";
import { ReadOnlyImageExtension } from "src/ui/extensions/image/read-only-image";
import { isValidHttpUrl } from "src/lib/utils";
@@ -51,7 +50,9 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
},
},
codeBlock: false,
horizontalRule: false,
horizontalRule: {
HTMLAttributes: { class: "mt-4 mb-4" },
},
dropcursor: {
color: "rgba(var(--color-text-100))",
width: 2,
@@ -72,7 +73,6 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
class: "rounded-lg border border-custom-border-300",
},
}),
HorizontalRule,
TiptapUnderline,
TextStyle,
Color,
@@ -4,13 +4,16 @@ export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
Extension.create({
name: "enterKey",
addKeyboardShortcuts() {
addKeyboardShortcuts(this) {
return {
Enter: () => {
if (onEnterKeyPress) {
onEnterKeyPress();
if (!this.editor.storage.mentionsOpen) {
if (onEnterKeyPress) {
onEnterKeyPress();
}
return true;
}
return true;
return false;
},
"Shift-Enter": ({ editor }) =>
editor.commands.first(({ commands }) => [
@@ -1,5 +1,3 @@
import { EnterKeyExtension } from "src/ui/extensions/enter-key-extension";
export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [
// EnterKeyExtension(onEnterKeyPress),
];
export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [EnterKeyExtension(onEnterKeyPress)];
+1 -1
View File
@@ -30,7 +30,7 @@ export interface ICycle {
is_favorite: boolean;
issue: string;
name: string;
owned_by: IUser;
owned_by: string;
project: string;
project_detail: IProjectLite;
status: TCycleGroups;
@@ -31,7 +31,6 @@ export const GoogleSignInButton: FC<Props> = (props) => {
size: "large",
logo_alignment: "center",
text: type === "sign_in" ? "signin_with" : "signup_with",
width: 384,
} as GsiButtonConfiguration // customization attributes
);
} catch (err) {
@@ -8,6 +8,8 @@ import useToast from "hooks/use-toast";
import { Button, Input } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
// icons
import { Eye, EyeOff } from "lucide-react";
type Props = {
email: string;
@@ -31,6 +33,7 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
const { email, handleSignInRedirection } = props;
// states
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
const [showPassword, setShowPassword] = useState(false);
// toast alert
const { setToastAlert } = useToast();
// form info
@@ -114,17 +117,30 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
required: "Password is required",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
type="password"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
autoFocus
/>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
autoFocus
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
)}
/>
<p className="text-onboarding-text-200 text-xs mt-2 pb-3">
@@ -2,7 +2,7 @@ import React, { useState } from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
import { XCircle } from "lucide-react";
import { Eye, EyeOff, XCircle } from "lucide-react";
// services
import { AuthService } from "services/auth.service";
// hooks
@@ -40,6 +40,7 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
const { email, handleStepChange, handleEmailClear, onSubmit } = props;
// states
const [isSendingUniqueCode, setIsSendingUniqueCode] = useState(false);
const [showPassword, setShowPassword] = useState(false);
// toast alert
const { setToastAlert } = useToast();
const {
@@ -157,15 +158,28 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
required: "Password is required",
}}
render={({ field: { value, onChange } }) => (
<Input
type="password"
value={value}
onChange={onChange}
hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
autoFocus
/>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
autoFocus
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
)}
/>
<div className="w-full text-right mt-2 pb-3">
@@ -10,6 +10,8 @@ import { Button, Input } from "@plane/ui";
import { checkEmailValidity } from "helpers/string.helper";
// constants
import { ESignUpSteps } from "components/account";
// icons
import { Eye, EyeOff } from "lucide-react";
type Props = {
email: string;
@@ -34,6 +36,7 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
const { email, handleSignInRedirection } = props;
// states
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
const [showPassword, setShowPassword] = useState(false);
// toast alert
const { setToastAlert } = useToast();
// form info
@@ -119,16 +122,29 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
required: "Password is required",
}}
render={({ field: { value, onChange } }) => (
<Input
type="password"
value={value}
onChange={onChange}
hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
autoFocus
/>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
autoFocus
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
)}
/>
<p className="text-onboarding-text-200 text-xs mt-2 pb-3">
@@ -1,8 +1,8 @@
import React from "react";
import React, { useState } from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
import { XCircle } from "lucide-react";
import { Eye, EyeOff, XCircle } from "lucide-react";
// services
import { AuthService } from "services/auth.service";
// hooks
@@ -32,6 +32,8 @@ const authService = new AuthService();
export const SignUpPasswordForm: React.FC<Props> = observer((props) => {
const { onSubmit } = props;
// states
const [showPassword, setShowPassword] = useState(false);
// toast alert
const { setToastAlert } = useToast();
// form info
@@ -112,15 +114,28 @@ export const SignUpPasswordForm: React.FC<Props> = observer((props) => {
required: "Password is required",
}}
render={({ field: { value, onChange } }) => (
<Input
type="password"
value={value}
onChange={onChange}
hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
autoFocus
/>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
autoFocus
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
)}
/>
<p className="text-onboarding-text-200 text-xs mt-2 pb-3">
@@ -1,7 +1,7 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import { useCycle, useModule, useProject } from "hooks/store";
import { useCycle, useMember, useModule, useProject } from "hooks/store";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
import { renderFormattedDate } from "helpers/date-time.helper";
@@ -15,10 +15,12 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
const { getProjectById } = useProject();
const { getCycleById } = useCycle();
const { getModuleById } = useModule();
const { getUserDetails } = useMember();
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
const projectDetails = projectId ? getProjectById(projectId.toString()) : undefined;
const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by) : undefined;
return (
<>
@@ -29,7 +31,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
<div className="mt-4 space-y-4">
<div className="flex items-center gap-2 text-xs">
<h6 className="text-custom-text-200">Lead</h6>
<span>{cycleDetails.owned_by?.display_name}</span>
<span>{cycleOwnerDetails?.display_name}</span>
</div>
<div className="flex items-center gap-2 text-xs">
<h6 className="text-custom-text-200">Start Date</h6>
+17 -5
View File
@@ -130,17 +130,29 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
onChange(unsplashImages[0].urls.regular);
}, [value, onChange, unsplashImages]);
const openDropdown = () => setIsOpen(true);
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
const handleClose = () => {
if (isOpen) setIsOpen(false);
};
useOutsideClickDetector(ref, closeDropdown);
const toggleDropdown = () => {
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(ref, handleClose);
return (
<Popover className="relative z-[2]" ref={ref} tabIndex={tabIndex} onKeyDown={handleKeyDown}>
<Popover.Button
className="rounded border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
onClick={openDropdown}
onClick={handleOnClick}
disabled={disabled}
>
{label}
@@ -157,6 +157,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
window.removeEventListener("keydown", handleEnterKeyPress);
window.removeEventListener("keydown", handleEscapeKeyPress);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, handleSubmit, onClose]);
const responseActionButton = response !== "" && (
@@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { useTheme } from "next-themes";
// hooks
import { useCycle, useIssues, useProject, useUser } from "hooks/store";
import { useCycle, useIssues, useMember, useProject, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// ui
import { SingleProgressStats } from "components/core";
@@ -58,6 +58,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
removeCycleFromFavorites,
} = useCycle();
const { currentProjectDetails } = useProject();
const { getUserDetails } = useMember();
// toast alert
const { setToastAlert } = useToast();
@@ -67,6 +68,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
);
const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
const cycleOwnerDetails = activeCycle ? getUserDetails(activeCycle.owned_by) : undefined;
const { data: activeCycleIssues } = useSWR(
workspaceSlug && projectId && currentProjectActiveCycleId
@@ -203,20 +205,20 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
<div className="flex items-center gap-4">
<div className="flex items-center gap-2.5 text-custom-text-200">
{activeCycle.owned_by.avatar && activeCycle.owned_by.avatar !== "" ? (
{cycleOwnerDetails?.avatar && cycleOwnerDetails?.avatar !== "" ? (
<img
src={activeCycle.owned_by.avatar}
src={cycleOwnerDetails?.avatar}
height={16}
width={16}
className="rounded-full"
alt={activeCycle.owned_by.display_name}
alt={cycleOwnerDetails?.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize">
{activeCycle.owned_by.display_name.charAt(0)}
{cycleOwnerDetails?.display_name.charAt(0)}
</span>
)}
<span className="text-custom-text-200">{activeCycle.owned_by.display_name}</span>
<span className="text-custom-text-200">{cycleOwnerDetails?.display_name}</span>
</div>
{activeCycle.assignees.length > 0 && (
+15 -11
View File
@@ -6,7 +6,7 @@ import { Disclosure, Popover, Transition } from "@headlessui/react";
// services
import { CycleService } from "services/cycle.service";
// hooks
import { useApplication, useCycle, useUser } from "hooks/store";
import { useApplication, useCycle, useMember, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { SidebarProgressStats } from "components/core";
@@ -73,8 +73,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
membership: { currentProjectRole },
} = useUser();
const { getCycleById, updateCycleDetails } = useCycle();
const { getUserDetails } = useMember();
const cycleDetails = getCycleById(cycleId);
const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by) : undefined;
const { setToastAlert } = useToast();
@@ -518,8 +520,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
</div>
<div className="flex w-1/2 items-center rounded-sm">
<div className="flex items-center gap-2.5">
<Avatar name={cycleDetails.owned_by.display_name} src={cycleDetails.owned_by.avatar} />
<span className="text-sm text-custom-text-200">{cycleDetails.owned_by.display_name}</span>
<Avatar name={cycleOwnerDetails?.display_name} src={cycleOwnerDetails?.avatar} />
<span className="text-sm text-custom-text-200">{cycleOwnerDetails?.display_name}</span>
</div>
</div>
</div>
@@ -587,14 +589,16 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
</div>
</div>
</div>
<div className="relative h-40 w-80">
<ProgressChart
distribution={cycleDetails.distribution?.completion_chart ?? {}}
startDate={cycleDetails.start_date ?? ""}
endDate={cycleDetails.end_date ?? ""}
totalIssues={cycleDetails.total_issues}
/>
</div>
{cycleDetails && cycleDetails.distribution && (
<div className="relative h-40 w-80">
<ProgressChart
distribution={cycleDetails.distribution?.completion_chart ?? {}}
startDate={cycleDetails.start_date ?? ""}
endDate={cycleDetails.end_date ?? ""}
totalIssues={cycleDetails.total_issues}
/>
</div>
)}
</div>
) : (
""
@@ -77,7 +77,7 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
})}
>
Issues
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium py-1 px-1.5 rounded-xl h-4 min-w-6 flex items-center text-center justify-center">
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium rounded-xl h-4 min-w-6 flex items-center text-center justify-center">
{totalIssues}
</span>
</h6>
@@ -72,14 +72,14 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
startedCount > 0
? "started"
: unStartedCount > 0
? "unstarted"
: backlogCount > 0
? "backlog"
: completedCount > 0
? "completed"
: canceledCount > 0
? "cancelled"
: null;
? "unstarted"
: backlogCount > 0
? "backlog"
: completedCount > 0
? "completed"
: canceledCount > 0
? "cancelled"
: null;
setActiveStateGroup(stateGroup);
setDefaultStateGroup(stateGroup);
@@ -151,13 +151,13 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
/>
</div>
{totalCount > 0 ? (
<div className="flex items-center pl-20 md:pl-11 lg:pl-14 pr-11 mt-11">
<div className="flex md:flex-col lg:flex-row items-center gap-x-10 gap-y-8 w-full">
<div className="w-full flex justify-center">
<div className="flex items-center pl-10 md:pl-11 lg:pl-14 pr-11 mt-11">
<div className="flex flex-col sm:flex-row md:flex-row lg:flex-row items-center justify-evenly gap-x-10 gap-y-8 w-full">
<div>
<PieGraph
data={chartData}
height="220px"
width="220px"
width="200px"
innerRadius={0.6}
cornerRadius={5}
colors={(datum) => datum.data.color}
@@ -189,7 +189,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
layers={["arcs", CenteredMetric]}
/>
</div>
<div className="justify-self-end space-y-6 w-min whitespace-nowrap">
<div className="space-y-6 w-min whitespace-nowrap">
{chartData.map((item) => (
<div key={item.id} className="flex items-center justify-between gap-6">
<div className="flex items-center gap-2.5 w-24">
@@ -7,7 +7,6 @@ import { useDashboard } from "hooks/store";
import { WidgetLoader } from "components/dashboard/widgets";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { TOverviewStatsWidgetResponse } from "@plane/types";
+101
View File
@@ -0,0 +1,101 @@
// helpers
import { cn } from "helpers/common.helper";
// types
import { TButtonVariants } from "./types";
// constants
import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS } from "./constants";
import { Tooltip } from "@plane/ui";
export type DropdownButtonProps = {
children: React.ReactNode;
className?: string;
isActive: boolean;
tooltipContent: string | React.ReactNode;
tooltipHeading: string;
showTooltip: boolean;
variant: TButtonVariants;
};
type ButtonProps = {
children: React.ReactNode;
className?: string;
isActive: boolean;
tooltipContent: string | React.ReactNode;
tooltipHeading: string;
showTooltip: boolean;
};
export const DropdownButton: React.FC<DropdownButtonProps> = (props) => {
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip, variant } = props;
const ButtonToRender: React.FC<ButtonProps> = BORDER_BUTTON_VARIANTS.includes(variant)
? BorderButton
: BACKGROUND_BUTTON_VARIANTS.includes(variant)
? BackgroundButton
: TransparentButton;
return (
<ButtonToRender
className={className}
isActive={isActive}
tooltipContent={tooltipContent}
tooltipHeading={tooltipHeading}
showTooltip={showTooltip}
>
{children}
</ButtonToRender>
);
};
const BorderButton: React.FC<ButtonProps> = (props) => {
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
return (
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
{ "bg-custom-background-80": isActive },
className
)}
>
{children}
</div>
</Tooltip>
);
};
const BackgroundButton: React.FC<ButtonProps> = (props) => {
const { children, className, tooltipContent, tooltipHeading, showTooltip } = props;
return (
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
className
)}
>
{children}
</div>
</Tooltip>
);
};
const TransparentButton: React.FC<ButtonProps> = (props) => {
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
return (
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
{ "bg-custom-background-80": isActive },
className
)}
>
{children}
</div>
</Tooltip>
);
};
+20
View File
@@ -0,0 +1,20 @@
// types
import { TButtonVariants } from "./types";
export const BORDER_BUTTON_VARIANTS: TButtonVariants[] = ["border-with-text", "border-without-text"];
export const BACKGROUND_BUTTON_VARIANTS: TButtonVariants[] = ["background-with-text", "background-without-text"];
export const TRANSPARENT_BUTTON_VARIANTS: TButtonVariants[] = ["transparent-with-text", "transparent-without-text"];
export const BUTTON_VARIANTS_WITHOUT_TEXT: TButtonVariants[] = [
"border-without-text",
"background-without-text",
"transparent-without-text",
];
export const BUTTON_VARIANTS_WITH_TEXT: TButtonVariants[] = [
"border-with-text",
"background-with-text",
"transparent-with-text",
];
+51 -187
View File
@@ -7,13 +7,16 @@ import { Check, ChevronDown, Search } from "lucide-react";
import { useApplication, useCycle } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { DropdownButton } from "./buttons";
// icons
import { ContrastIcon, Tooltip } from "@plane/ui";
import { ContrastIcon } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
import { ICycle } from "@plane/types";
import { TDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & {
button?: ReactNode;
@@ -24,18 +27,6 @@ type Props = TDropdownProps & {
value: string | null;
};
type ButtonProps = {
className?: string;
cycle: ICycle | null;
hideIcon: boolean;
hideText?: boolean;
dropdownArrow: boolean;
isActive?: boolean;
dropdownArrowClassName: string;
placeholder: string;
tooltip: boolean;
};
type DropdownOptions =
| {
value: string | null;
@@ -44,100 +35,6 @@ type DropdownOptions =
}[]
| undefined;
const BorderButton = (props: ButtonProps) => {
const {
className,
cycle,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
placeholder,
tooltip,
} = props;
return (
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? placeholder} disabled={!tooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
{ "bg-custom-background-80": isActive },
className
)}
>
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}{" "}
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
const BackgroundButton = (props: ButtonProps) => {
const {
className,
cycle,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
placeholder,
tooltip,
} = props;
return (
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? placeholder} disabled={!tooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
className
)}
>
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
const TransparentButton = (props: ButtonProps) => {
const {
className,
cycle,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
placeholder,
tooltip,
} = props;
return (
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? placeholder} disabled={!tooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
{ "bg-custom-background-80": isActive },
className
)}
>
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
export const CycleDropdown: React.FC<Props> = observer((props) => {
const {
button,
@@ -153,8 +50,8 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
placeholder = "Cycle",
placement,
projectId,
showTooltip = false,
tabIndex,
tooltip = false,
value,
} = props;
// states
@@ -221,13 +118,34 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
const selectedCycle = value ? getCycleById(value) : null;
const openDropdown = () => {
setIsOpen(true);
const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const handleClose = () => {
if (isOpen) setIsOpen(false);
if (referenceElement) referenceElement.blur();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: string | null) => {
onChange(val);
handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
return (
<Combobox
@@ -236,7 +154,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
tabIndex={tabIndex}
className={cn("h-full", className)}
value={value}
onChange={onChange}
onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
>
@@ -246,7 +164,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown}
onClick={handleOnClick}
>
{button}
</button>
@@ -262,77 +180,24 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
},
buttonContainerClassName
)}
onClick={openDropdown}
onClick={handleOnClick}
>
{/* TODO: move button components to a single file for each dropdown */}
{buttonVariant === "border-with-text" ? (
<BorderButton
cycle={selectedCycle}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
<BorderButton
cycle={selectedCycle}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "background-with-text" ? (
<BackgroundButton
cycle={selectedCycle}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "background-without-text" ? (
<BackgroundButton
cycle={selectedCycle}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-with-text" ? (
<TransparentButton
cycle={selectedCycle}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
<TransparentButton
cycle={selectedCycle}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : null}
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Cycle"
tooltipContent={selectedCycle?.name ?? placeholder}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate">{selectedCycle?.name ?? placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
@@ -366,7 +231,6 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={closeDropdown}
>
{({ selected }) => (
<>
+57 -249
View File
@@ -2,17 +2,19 @@ import React, { useRef, useState } from "react";
import { Combobox } from "@headlessui/react";
import DatePicker from "react-datepicker";
import { usePopper } from "react-popper";
import { Calendar, CalendarDays, X } from "lucide-react";
import { CalendarDays, X } from "lucide-react";
// hooks
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// ui
import { Tooltip } from "@plane/ui";
// components
import { DropdownButton } from "./buttons";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { TDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & {
clearIconClassName?: string;
@@ -23,157 +25,6 @@ type Props = TDropdownProps & {
onChange: (val: Date | null) => void;
value: Date | string | null;
closeOnSelect?: boolean;
showPlaceholderIcon?: boolean;
};
type ButtonProps = {
className?: string;
clearIconClassName: string;
date: string | Date | null;
icon: React.ReactNode;
isClearable: boolean;
hideIcon?: boolean;
hideText?: boolean;
isActive?: boolean;
onClear: () => void;
placeholder: string;
tooltip: boolean;
showPlaceholderIcon?: boolean;
};
const BorderButton = (props: ButtonProps) => {
const {
className,
clearIconClassName,
date,
icon,
isClearable,
hideIcon = false,
hideText = false,
isActive = false,
onClear,
placeholder,
tooltip,
} = props;
return (
<Tooltip
tooltipHeading={placeholder}
tooltipContent={date ? renderFormattedDate(date) : "None"}
disabled={!tooltip}
>
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
{ "bg-custom-background-80": isActive },
className
)}
>
{!hideIcon && icon}
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
{isClearable && (
<X
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
onClick={(e) => {
e.stopPropagation();
onClear();
}}
/>
)}
</div>
</Tooltip>
);
};
const BackgroundButton = (props: ButtonProps) => {
const {
className,
clearIconClassName,
date,
icon,
isClearable,
hideIcon = false,
hideText = false,
onClear,
placeholder,
tooltip,
} = props;
return (
<Tooltip
tooltipHeading={placeholder}
tooltipContent={date ? renderFormattedDate(date) : "None"}
disabled={!tooltip}
>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
className
)}
>
{!hideIcon && icon}
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
{isClearable && (
<X
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
onClick={(e) => {
e.stopPropagation();
onClear();
}}
/>
)}
</div>
</Tooltip>
);
};
const TransparentButton = (props: ButtonProps) => {
const {
className,
clearIconClassName,
date,
icon,
isClearable,
hideIcon = false,
hideText = false,
isActive = false,
onClear,
placeholder,
tooltip,
showPlaceholderIcon = false,
} = props;
return (
<Tooltip
tooltipHeading={placeholder}
tooltipContent={date ? renderFormattedDate(date) : "None"}
disabled={!tooltip}
>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
{ "bg-custom-background-80": isActive },
className
)}
>
{!hideIcon && icon}
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
{showPlaceholderIcon && !date && (
<Calendar className="h-2.5 w-2.5 flex-shrink-0 hidden group-hover:inline text-custom-text-400" />
)}
{isClearable && (
<X
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
onClick={(e) => {
e.stopPropagation();
onClear();
}}
/>
)}
</div>
</Tooltip>
);
};
export const DateDropdown: React.FC<Props> = (props) => {
@@ -193,9 +44,8 @@ export const DateDropdown: React.FC<Props> = (props) => {
onChange,
placeholder = "Date",
placement,
showTooltip = false,
tabIndex,
tooltip = false,
showPlaceholderIcon = false,
value,
} = props;
const [isOpen, setIsOpen] = useState(false);
@@ -217,15 +67,36 @@ export const DateDropdown: React.FC<Props> = (props) => {
],
});
const isDateSelected = value !== null && value !== undefined && value.toString().trim() !== "";
const isDateSelected = value && value.toString().trim() !== "";
const openDropdown = () => {
setIsOpen(true);
const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const handleClose = () => {
if (isOpen) setIsOpen(false);
if (referenceElement) referenceElement.blur();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: Date | null) => {
onChange(val);
if (closeOnSelect) handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
return (
<Combobox
@@ -247,90 +118,30 @@ export const DateDropdown: React.FC<Props> = (props) => {
},
buttonContainerClassName
)}
onClick={openDropdown}
onClick={handleOnClick}
>
{buttonVariant === "border-with-text" ? (
<BorderButton
date={value}
className={buttonClassName}
clearIconClassName={clearIconClassName}
hideIcon={hideIcon}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
<BorderButton
date={value}
className={buttonClassName}
clearIconClassName={clearIconClassName}
hideIcon={hideIcon}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
isActive={isOpen}
tooltip={tooltip}
hideText
/>
) : buttonVariant === "background-with-text" ? (
<BackgroundButton
date={value}
className={buttonClassName}
clearIconClassName={clearIconClassName}
hideIcon={hideIcon}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
tooltip={tooltip}
/>
) : buttonVariant === "background-without-text" ? (
<BackgroundButton
date={value}
className={buttonClassName}
clearIconClassName={clearIconClassName}
hideIcon={hideIcon}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
tooltip={tooltip}
hideText
/>
) : buttonVariant === "transparent-with-text" ? (
<TransparentButton
date={value}
className={buttonClassName}
clearIconClassName={clearIconClassName}
hideIcon={hideIcon}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
isActive={isOpen}
tooltip={tooltip}
showPlaceholderIcon={showPlaceholderIcon}
/>
) : buttonVariant === "transparent-without-text" ? (
<TransparentButton
date={value}
className={buttonClassName}
clearIconClassName={clearIconClassName}
hideIcon={hideIcon}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
isActive={isOpen}
tooltip={tooltip}
hideText
showPlaceholderIcon={showPlaceholderIcon}
/>
) : null}
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading={placeholder}
tooltipContent={value ? renderFormattedDate(value) : "None"}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && icon}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate">{value ? renderFormattedDate(value) : placeholder}</span>
)}
{isClearable && isDateSelected && (
<X
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
onClick={(e) => {
e.stopPropagation();
onChange(null);
}}
/>
)}
</DropdownButton>
</button>
</Combobox.Button>
{isOpen && (
@@ -338,10 +149,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
<div className="my-1" ref={setPopperElement} style={styles.popper} {...attributes.popper}>
<DatePicker
selected={value ? new Date(value) : null}
onChange={(val) => {
onChange(val);
if (closeOnSelect) closeDropdown();
}}
onChange={dropdownOnChange}
dateFormat="dd-MM-yyyy"
minDate={minDate}
maxDate={maxDate}
+50 -205
View File
@@ -8,12 +8,14 @@ import sortBy from "lodash/sortBy";
import { useApplication, useEstimate } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// ui
import { Tooltip } from "@plane/ui";
// components
import { DropdownButton } from "./buttons";
// helpers
import { cn } from "helpers/common.helper";
// types
import { TDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & {
button?: ReactNode;
@@ -24,18 +26,6 @@ type Props = TDropdownProps & {
value: number | null;
};
type ButtonProps = {
className?: string;
estimatePoint: string | null;
dropdownArrow: boolean;
dropdownArrowClassName: string;
hideIcon?: boolean;
hideText?: boolean;
isActive?: boolean;
placeholder: string;
tooltip: boolean;
};
type DropdownOptions =
| {
value: number | null;
@@ -44,118 +34,6 @@ type DropdownOptions =
}[]
| undefined;
const BorderButton = (props: ButtonProps) => {
const {
className,
estimatePoint,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
placeholder,
tooltip,
} = props;
return (
<Tooltip
tooltipHeading="Estimate"
tooltipContent={estimatePoint !== null ? estimatePoint : placeholder}
disabled={!tooltip}
>
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
{ "bg-custom-background-80": isActive },
className
)}
>
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
{!hideText && (
<span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
const BackgroundButton = (props: ButtonProps) => {
const {
className,
estimatePoint,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
placeholder,
tooltip,
} = props;
return (
<Tooltip
tooltipHeading="Estimate"
tooltipContent={estimatePoint !== null ? estimatePoint : placeholder}
disabled={!tooltip}
>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
className
)}
>
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
{!hideText && (
<span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
const TransparentButton = (props: ButtonProps) => {
const {
className,
estimatePoint,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
placeholder,
tooltip,
} = props;
return (
<Tooltip
tooltipHeading="Estimate"
tooltipContent={estimatePoint !== null ? estimatePoint : placeholder}
disabled={!tooltip}
>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
{ "bg-custom-background-80": isActive },
className
)}
>
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
{!hideText && (
<span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
export const EstimateDropdown: React.FC<Props> = observer((props) => {
const {
button,
@@ -171,8 +49,8 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
placeholder = "Estimate",
placement,
projectId,
showTooltip = false,
tabIndex,
tooltip = false,
value,
} = props;
// states
@@ -228,15 +106,35 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null;
const openDropdown = () => {
setIsOpen(true);
const onOpen = () => {
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const handleClose = () => {
if (isOpen) setIsOpen(false);
if (referenceElement) referenceElement.blur();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: number | null) => {
onChange(val);
handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
return (
<Combobox
@@ -245,7 +143,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
tabIndex={tabIndex}
className={cn("h-full w-full", className)}
value={value}
onChange={onChange}
onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
>
@@ -255,7 +153,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown}
onClick={handleOnClick}
>
{button}
</button>
@@ -271,76 +169,24 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
},
buttonContainerClassName
)}
onClick={openDropdown}
onClick={handleOnClick}
>
{buttonVariant === "border-with-text" ? (
<BorderButton
estimatePoint={selectedEstimate}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
<BorderButton
estimatePoint={selectedEstimate}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "background-with-text" ? (
<BackgroundButton
estimatePoint={selectedEstimate}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "background-without-text" ? (
<BackgroundButton
estimatePoint={selectedEstimate}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-with-text" ? (
<TransparentButton
estimatePoint={selectedEstimate}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
<TransparentButton
estimatePoint={selectedEstimate}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : null}
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Estimate"
tooltipContent={selectedEstimate !== null ? selectedEstimate : placeholder}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate">{selectedEstimate !== null ? selectedEstimate : placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
@@ -374,7 +220,6 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={closeDropdown}
>
{({ selected }) => (
<>
-1
View File
@@ -3,7 +3,6 @@ export * from "./cycle";
export * from "./date";
export * from "./estimate";
export * from "./module";
export * from "./module-select";
export * from "./priority";
export * from "./project";
export * from "./state";
@@ -0,0 +1,37 @@
import { observer } from "mobx-react-lite";
// hooks
import { useMember } from "hooks/store";
// ui
import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui";
type AvatarProps = {
showTooltip: boolean;
userIds: string | string[] | null;
};
export const ButtonAvatars: React.FC<AvatarProps> = observer((props) => {
const { showTooltip, userIds } = props;
// store hooks
const { getUserDetails } = useMember();
if (Array.isArray(userIds)) {
if (userIds.length > 0)
return (
<AvatarGroup size="md" showTooltip={!showTooltip}>
{userIds.map((userId) => {
const userDetails = getUserDetails(userId);
if (!userDetails) return;
return <Avatar key={userId} src={userDetails.avatar} name={userDetails.display_name} />;
})}
</AvatarGroup>
);
} else {
if (userIds) {
const userDetails = getUserDetails(userIds);
return <Avatar src={userDetails?.avatar} name={userDetails?.display_name} size="md" showTooltip={!showTooltip} />;
}
}
return <UserGroupIcon className="h-3 w-3 flex-shrink-0" />;
});
-187
View File
@@ -1,187 +0,0 @@
import { observer } from "mobx-react-lite";
import { ChevronDown } from "lucide-react";
// hooks
import { useMember } from "hooks/store";
// ui
import { Avatar, AvatarGroup, Tooltip, UserGroupIcon } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
type ButtonProps = {
className?: string;
dropdownArrow: boolean;
dropdownArrowClassName: string;
placeholder: string;
hideIcon?: boolean;
hideText?: boolean;
isActive?: boolean;
tooltip: boolean;
userIds: string | string[] | null;
};
const ButtonAvatars = observer(({ tooltip, userIds }: { tooltip: boolean; userIds: string | string[] | null }) => {
const { getUserDetails } = useMember();
if (Array.isArray(userIds)) {
if (userIds.length > 0)
return (
<AvatarGroup size="md" showTooltip={!tooltip}>
{userIds.map((userId) => {
const userDetails = getUserDetails(userId);
if (!userDetails) return;
return <Avatar key={userId} src={userDetails.avatar} name={userDetails.display_name} />;
})}
</AvatarGroup>
);
} else {
if (userIds) {
const userDetails = getUserDetails(userIds);
return <Avatar src={userDetails?.avatar} name={userDetails?.display_name} size="md" showTooltip={!tooltip} />;
}
}
return <UserGroupIcon className="h-3 w-3 flex-shrink-0" />;
});
export const BorderButton = observer((props: ButtonProps) => {
const {
className,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
placeholder,
userIds,
tooltip,
} = props;
// store hooks
const { getUserDetails } = useMember();
const isArray = Array.isArray(userIds);
return (
<Tooltip
tooltipHeading={placeholder}
tooltipContent={`${userIds?.length ?? 0} assignee${userIds?.length !== 1 ? "s" : ""}`}
disabled={!tooltip}
>
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
{ "bg-custom-background-80": isActive },
className
)}
>
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
{!hideText && (
<span className="flex-grow truncate text-sm leading-5">
{isArray && userIds.length > 0
? userIds.length === 1
? getUserDetails(userIds[0])?.display_name
: ""
: placeholder}
</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
});
export const BackgroundButton = observer((props: ButtonProps) => {
const {
className,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
placeholder,
userIds,
tooltip,
} = props;
// store hooks
const { getUserDetails } = useMember();
const isArray = Array.isArray(userIds);
return (
<Tooltip
tooltipHeading={placeholder}
tooltipContent={`${userIds?.length ?? 0} assignee${userIds?.length !== 1 ? "s" : ""}`}
disabled={!tooltip}
>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
className
)}
>
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
{!hideText && (
<span className="flex-grow truncate text-sm leading-5">
{isArray && userIds.length > 0
? userIds.length === 1
? getUserDetails(userIds[0])?.display_name
: ""
: placeholder}
</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
});
export const TransparentButton = observer((props: ButtonProps) => {
const {
className,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
placeholder,
userIds,
tooltip,
} = props;
// store hooks
const { getUserDetails } = useMember();
const isArray = Array.isArray(userIds);
return (
<Tooltip
tooltipHeading={placeholder}
tooltipContent={`${userIds?.length ?? 0} assignee${userIds?.length !== 1 ? "s" : ""}`}
disabled={!tooltip}
>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
{ "bg-custom-background-80": isActive },
className
)}
>
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
{!hideText && (
<span className="flex-grow truncate text-sm leading-5">
{isArray && userIds.length > 0
? userIds.length === 1
? getUserDetails(userIds[0])?.display_name
: ""
: placeholder}
</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
});
-1
View File
@@ -1,3 +1,2 @@
export * from "./buttons";
export * from "./project-member";
export * from "./workspace-member";
@@ -2,19 +2,22 @@ import { Fragment, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Check, Search } from "lucide-react";
import { Check, ChevronDown, Search } from "lucide-react";
// hooks
import { useApplication, useMember, useUser } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns";
import { ButtonAvatars } from "./avatar";
import { DropdownButton } from "../buttons";
// icons
import { Avatar } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
import { MemberDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
type Props = {
projectId: string;
@@ -36,8 +39,8 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
placeholder = "Members",
placement,
projectId,
showTooltip = false,
tabIndex,
tooltip = false,
value,
} = props;
// states
@@ -96,15 +99,35 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
};
if (multiple) comboboxProps.multiple = true;
const openDropdown = () => {
setIsOpen(true);
const onOpen = () => {
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const handleClose = () => {
if (isOpen) setIsOpen(false);
if (referenceElement) referenceElement.blur();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: string & string[]) => {
onChange(val);
if (!multiple) handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
return (
<Combobox
@@ -112,6 +135,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full", className)}
onChange={dropdownOnChange}
onKeyDown={handleKeyDown}
{...comboboxProps}
>
@@ -121,7 +145,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown}
onClick={handleOnClick}
>
{button}
</button>
@@ -137,76 +161,30 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
},
buttonContainerClassName
)}
onClick={openDropdown}
onClick={handleOnClick}
>
{buttonVariant === "border-with-text" ? (
<BorderButton
userIds={value}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
<BorderButton
userIds={value}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
hideText
/>
) : buttonVariant === "background-with-text" ? (
<BackgroundButton
userIds={value}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "background-without-text" ? (
<BackgroundButton
userIds={value}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
hideText
/>
) : buttonVariant === "transparent-with-text" ? (
<TransparentButton
userIds={value}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
<TransparentButton
userIds={value}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
hideText
/>
) : null}
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading={placeholder}
tooltipContent={`${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate text-sm leading-5">
{Array.isArray(value) && value.length > 0
? value.length === 1
? getUserDetails(value[0])?.display_name
: ""
: placeholder}
</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
@@ -240,9 +218,6 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={() => {
if (!multiple) closeDropdown();
}}
>
{({ selected }) => (
<>
@@ -2,19 +2,22 @@ import { Fragment, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Check, Search } from "lucide-react";
import { Check, ChevronDown, Search } from "lucide-react";
// hooks
import { useMember, useUser } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns";
import { ButtonAvatars } from "./avatar";
import { DropdownButton } from "../buttons";
// icons
import { Avatar } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
import { MemberDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((props) => {
const {
@@ -31,13 +34,13 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
onChange,
placeholder = "Members",
placement,
showTooltip = false,
tabIndex,
tooltip = false,
value,
} = props;
// states
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [isOpen, setIsOpen] = useState<boolean>(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
// popper-js refs
@@ -87,13 +90,34 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
};
if (multiple) comboboxProps.multiple = true;
const openDropdown = () => {
setIsOpen(true);
const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const handleClose = () => {
if (isOpen) setIsOpen(false);
if (referenceElement) referenceElement.blur();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: string & string[]) => {
onChange(val);
if (!multiple) handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
return (
<Combobox
@@ -103,6 +127,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
className={cn("h-full", className)}
{...comboboxProps}
handleKeyDown={handleKeyDown}
onChange={dropdownOnChange}
>
<Combobox.Button as={Fragment}>
{button ? (
@@ -110,6 +135,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
@@ -125,124 +151,82 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
{buttonVariant === "border-with-text" ? (
<BorderButton
userIds={value}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
<BorderButton
userIds={value}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
hideText
/>
) : buttonVariant === "background-with-text" ? (
<BackgroundButton
userIds={value}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "background-without-text" ? (
<BackgroundButton
userIds={value}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
hideText
/>
) : buttonVariant === "transparent-with-text" ? (
<TransparentButton
userIds={value}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
<TransparentButton
userIds={value}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
hideText
/>
) : null}
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading={placeholder}
tooltipContent={`${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate text-sm leading-5">
{Array.isArray(value) && value.length > 0
? value.length === 1
? getUserDetails(value[0])?.display_name
: ""
: placeholder}
</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
<Combobox.Options className="fixed z-10">
<div
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={() => {
if (!multiple) closeDropdown();
}}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
)
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
)
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
)}
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
)}
</div>
</div>
</div>
</Combobox.Options>
</Combobox.Options>
)}
</Combobox>
);
});
@@ -1,114 +0,0 @@
import { FC } from "react";
import { twMerge } from "tailwind-merge";
import { observer } from "mobx-react-lite";
import { ChevronDown, X } from "lucide-react";
// hooks
import { useModule } from "hooks/store";
// ui and components
import { DiceIcon, Tooltip } from "@plane/ui";
// types
import { TModuleSelectButton } from "./types";
export const ModuleSelectButton: FC<TModuleSelectButton> = observer((props) => {
const {
value,
onChange,
placeholder,
buttonClassName,
buttonVariant,
hideIcon,
hideText,
dropdownArrow,
dropdownArrowClassName,
showTooltip,
showCount,
} = props;
// hooks
const { getModuleById } = useModule();
return (
<div
className={twMerge(
`w-full h-full relative overflow-hidden flex justify-between items-center gap-1 rounded text-sm px-2`,
buttonVariant === "border-with-text"
? `border-[0.5px] border-custom-border-300 hover:bg-custom-background-80`
: ``,
buttonVariant === "border-without-text"
? `border-[0.5px] border-custom-border-300 hover:bg-custom-background-80`
: ``,
buttonVariant === "background-with-text" ? `bg-custom-background-80` : ``,
buttonVariant === "background-without-text" ? `bg-custom-background-80` : ``,
buttonVariant === "transparent-with-text" ? `hover:bg-custom-background-80` : ``,
buttonVariant === "transparent-without-text" ? `hover:bg-custom-background-80` : ``,
buttonClassName
)}
>
<div className="relative overflow-hidden h-full flex flex-wrap items-center gap-1">
{value && typeof value === "string" ? (
<div className="relative overflow-hidden flex items-center gap-1.5">
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && (
<span className="w-full overflow-hidden truncate inline-block line-clamp-1 capitalize">
{getModuleById(value)?.name || placeholder}
</span>
)}
</div>
) : value && Array.isArray(value) && value.length > 0 ? (
showCount ? (
<div className="relative overflow-hidden flex items-center gap-1.5">
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && (
<span className="w-full overflow-hidden truncate inline-block line-clamp-1 capitalize">
{value.length} Modules
</span>
)}
</div>
) : (
value.map((moduleId) => {
const _module = getModuleById(moduleId);
if (!_module) return <></>;
return (
<div className="relative flex justify-between items-center gap-1 min-w-[60px] max-w-[84px] overflow-hidden bg-custom-background-80 px-1.5 py-1 rounded">
<Tooltip tooltipContent={_module?.name} disabled={!showTooltip}>
<div className="relative overflow-hidden flex items-center gap-1.5">
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && (
<span className="w-full truncate inline-block line-clamp-1 capitalize">{_module?.name}</span>
)}
</div>
</Tooltip>
<Tooltip tooltipContent="Remove" disabled={!showTooltip}>
<span
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onChange(_module.id);
}}
>
<X className="h-2.5 w-2.5 text-custom-text-300 hover:text-red-500" />
</span>
</Tooltip>
</div>
);
})
)
) : (
!hideText && (
<div className="relative overflow-hidden flex items-center gap-1.5">
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && (
<span className="w-full overflow-hidden truncate inline-block line-clamp-1 capitalize">
{placeholder}
</span>
)}
</div>
)
)}
</div>
{dropdownArrow && (
<ChevronDown className={twMerge("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
);
});
@@ -1,2 +0,0 @@
export * from "./button";
export * from "./select";
@@ -1,227 +0,0 @@
import { FC, useEffect, useRef, useState, Fragment } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Check, Search } from "lucide-react";
import { twMerge } from "tailwind-merge";
// hooks
import { useModule } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { ModuleSelectButton } from "./";
// types
import { TModuleSelectDropdown, TModuleSelectDropdownOption } from "./types";
import { DiceIcon } from "@plane/ui";
export const ModuleSelectDropdown: FC<TModuleSelectDropdown> = observer((props) => {
// props
const {
workspaceSlug,
projectId,
value = undefined,
onChange,
placeholder = "Module",
multiple = false,
disabled = false,
className = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonVariant = "transparent-with-text",
hideIcon = false,
dropdownArrow = false,
dropdownArrowClassName = "",
showTooltip = false,
showCount = false,
placement,
tabIndex,
button,
} = props;
// states
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
// store hooks
const { getProjectModuleIds, fetchModules, getModuleById } = useModule();
const moduleIds = getProjectModuleIds(projectId);
const options: TModuleSelectDropdownOption[] | undefined = moduleIds?.map((moduleId) => {
const moduleDetails = getModuleById(moduleId);
return {
value: moduleId,
query: `${moduleDetails?.name}`,
content: (
<div className="flex items-center gap-2">
<DiceIcon className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">{moduleDetails?.name}</span>
</div>
),
};
});
!multiple &&
options?.unshift({
value: undefined,
query: "No module",
content: (
<div className="flex items-center gap-2">
<DiceIcon className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">No module</span>
</div>
),
});
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
// fetch modules of the project if not already present in the store
useEffect(() => {
if (!workspaceSlug) return;
if (!moduleIds) fetchModules(workspaceSlug, projectId);
}, [moduleIds, fetchModules, projectId, workspaceSlug]);
const openDropdown = () => {
if (isOpen) closeDropdown();
else {
setIsOpen(true);
if (referenceElement) referenceElement.focus();
}
};
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const comboboxProps: any = {};
if (multiple) comboboxProps.multiple = true;
return (
<Combobox
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={twMerge("h-full", className)}
value={value}
onChange={onChange}
disabled={disabled}
onKeyDown={handleKeyDown}
{...comboboxProps}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={twMerge(
"block h-full max-w-full outline-none",
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer",
buttonContainerClassName
)}
onClick={openDropdown}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={twMerge(
"block h-full max-w-full outline-none ",
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer",
buttonContainerClassName
)}
onClick={openDropdown}
>
<ModuleSelectButton
value={value}
onChange={onChange}
placeholder={placeholder}
disabled={disabled}
buttonClassName={buttonClassName}
buttonVariant={buttonVariant}
hideIcon={hideIcon}
hideText={["border-without-text", "background-without-text", "transparent-without-text"].includes(
buttonVariant
)}
dropdownArrow={dropdownArrow}
dropdownArrowClassName={dropdownArrowClassName}
showTooltip={showTooltip}
showCount={showCount}
/>
</button>
)}
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(moduleIds: any) => {
const displayValueOptions: TModuleSelectDropdownOption[] | undefined = options?.filter((_module) =>
moduleIds.includes(_module.value)
);
return displayValueOptions?.map((_option) => _option.query).join(", ") || "Select Module";
}}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={() => !multiple && closeDropdown()}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
)
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
)}
</Combobox>
);
});
-50
View File
@@ -1,50 +0,0 @@
import { ReactNode } from "react";
import { Placement } from "@popperjs/core";
import { TDropdownProps, TButtonVariants } from "../types";
type TModuleSelectDropdownRoot = Omit<
TDropdownProps,
"buttonClassName",
"buttonContainerClassName",
"buttonContainerClassName",
"className",
"disabled",
"hideIcon",
"placeholder",
"placement",
"tabIndex",
"tooltip"
>;
export type TModuleSelectDropdownBase = {
value: string | string[] | undefined;
onChange: (moduleIds: undefined | string | (string | undefined)[]) => void;
placeholder?: string;
disabled?: boolean;
buttonClassName?: string;
buttonVariant?: TButtonVariants;
hideIcon?: boolean;
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
showTooltip?: boolean;
showCount?: boolean;
};
export type TModuleSelectButton = TModuleSelectDropdownBase & { hideText?: boolean };
export type TModuleSelectDropdown = TModuleSelectDropdownBase & {
workspaceSlug: string;
projectId: string;
multiple?: boolean;
className?: string;
buttonContainerClassName?: string;
placement?: Placement;
tabIndex?: number;
button?: ReactNode;
};
export type TModuleSelectDropdownOption = {
value: string | undefined;
query: string;
content: JSX.Element;
};
+167 -182
View File
@@ -2,27 +2,40 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Check, ChevronDown, Search } from "lucide-react";
import { Check, ChevronDown, Search, X } from "lucide-react";
// hooks
import { useApplication, useModule } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { DropdownButton } from "./buttons";
// icons
import { DiceIcon, Tooltip } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
import { IModule } from "@plane/types";
import { TDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants";
type Props = TDropdownProps & {
button?: ReactNode;
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
onChange: (val: string | null) => void;
projectId: string;
value: string | null;
};
showCount?: boolean;
} & (
| {
multiple: false;
onChange: (val: string | null) => void;
value: string | null;
}
| {
multiple: true;
onChange: (val: string[]) => void;
value: string[];
}
);
type DropdownOptions =
| {
@@ -32,110 +45,97 @@ type DropdownOptions =
}[]
| undefined;
type ButtonProps = {
className?: string;
type ButtonContentProps = {
disabled: boolean;
dropdownArrow: boolean;
dropdownArrowClassName: string;
hideIcon?: boolean;
hideText?: boolean;
isActive?: boolean;
module: IModule | null;
hideIcon: boolean;
hideText: boolean;
onChange: (moduleIds: string[]) => void;
placeholder: string;
tooltip: boolean;
showCount: boolean;
value: string | string[] | null;
};
const BorderButton = (props: ButtonProps) => {
const ButtonContent: React.FC<ButtonContentProps> = (props) => {
const {
className,
disabled,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
module,
hideIcon,
hideText,
onChange,
placeholder,
tooltip,
showCount,
value,
} = props;
// store hooks
const { getModuleById } = useModule();
return (
<Tooltip tooltipHeading="Module" tooltipContent={module?.name ?? placeholder} disabled={!tooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
{ "bg-custom-background-80": isActive },
className
if (Array.isArray(value))
return (
<>
{showCount ? (
<>
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
<span className="flex-grow truncate text-left">
{value.length > 0 ? `${value.length} Module${value.length === 1 ? "" : "s"}` : placeholder}
</span>
</>
) : value.length > 0 ? (
<div className="flex items-center gap-2 py-0.5 flex-wrap">
{value.map((moduleId) => {
const moduleDetails = getModuleById(moduleId);
return (
<div
key={moduleId}
className="flex items-center gap-1 bg-custom-background-80 text-custom-text-200 rounded px-1.5 py-1"
>
{!hideIcon && <DiceIcon className="h-2.5 w-2.5 flex-shrink-0" />}
{!hideText && (
<Tooltip tooltipHeading="Title" tooltipContent={moduleDetails?.name}>
<span className="text-xs font-medium flex-grow truncate max-w-40">{moduleDetails?.name}</span>
</Tooltip>
)}
{!disabled && (
<Tooltip tooltipContent="Remove">
<button
type="button"
className="flex-shrink-0"
onClick={() => {
const newModuleIds = value.filter((m) => m !== moduleId);
onChange(newModuleIds);
}}
>
<X className="h-2.5 w-2.5 text-custom-text-300 hover:text-red-500" />
</button>
</Tooltip>
)}
</div>
);
})}
</div>
) : (
<>
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
<span className="flex-grow truncate text-left">{placeholder}</span>
</>
)}
>
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && <span className="flex-grow truncate">{module?.name ?? placeholder}</span>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
const BackgroundButton = (props: ButtonProps) => {
const {
className,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
module,
placeholder,
tooltip,
} = props;
return (
<Tooltip tooltipHeading="Module" tooltipContent={module?.name ?? placeholder} disabled={!tooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
className
)}
>
</>
);
else
return (
<>
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && <span className="flex-grow truncate">{module?.name ?? placeholder}</span>}
{!hideText && <span className="flex-grow truncate text-left">{value ?? placeholder}</span>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
const TransparentButton = (props: ButtonProps) => {
const {
className,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
module,
placeholder,
tooltip,
} = props;
return (
<Tooltip tooltipHeading="Module" tooltipContent={module?.name ?? placeholder} disabled={!tooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
{ "bg-custom-background-80": isActive },
className
)}
>
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
{!hideText && <span className="flex-grow truncate">{module?.name ?? placeholder}</span>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
</>
);
};
export const ModuleDropdown: React.FC<Props> = observer((props) => {
@@ -149,12 +149,14 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
dropdownArrow = false,
dropdownArrowClassName = "",
hideIcon = false,
multiple,
onChange,
placeholder = "Module",
placement,
projectId,
showCount = false,
showTooltip = false,
tabIndex,
tooltip = false,
value,
} = props;
// states
@@ -186,7 +188,6 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
const options: DropdownOptions = moduleIds?.map((moduleId) => {
const moduleDetails = getModuleById(moduleId);
return {
value: moduleId,
query: `${moduleDetails?.name}`,
@@ -198,16 +199,17 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
),
};
});
options?.unshift({
value: null,
query: "No module",
content: (
<div className="flex items-center gap-2">
<DiceIcon className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">No module</span>
</div>
),
});
if (!multiple)
options?.unshift({
value: null,
query: "No module",
content: (
<div className="flex items-center gap-2">
<DiceIcon className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">No module</span>
</div>
),
});
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
@@ -219,15 +221,41 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
if (!moduleIds) fetchModules(workspaceSlug, projectId);
}, [moduleIds, fetchModules, projectId, workspaceSlug]);
const selectedModule = value ? getModuleById(value) : null;
const openDropdown = () => {
setIsOpen(true);
const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const handleClose = () => {
if (isOpen) setIsOpen(false);
if (referenceElement) referenceElement.blur();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: string & string[]) => {
onChange(val);
if (!multiple) handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
const comboboxProps: any = {
value,
onChange: dropdownOnChange,
disabled,
};
if (multiple) comboboxProps.multiple = true;
return (
<Combobox
@@ -235,10 +263,8 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full", className)}
value={value}
onChange={onChange}
disabled={disabled}
onKeyDown={handleKeyDown}
{...comboboxProps}
>
<Combobox.Button as={Fragment}>
{button ? (
@@ -246,7 +272,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown}
onClick={handleOnClick}
>
{button}
</button>
@@ -262,76 +288,31 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
},
buttonContainerClassName
)}
onClick={openDropdown}
onClick={handleOnClick}
>
{buttonVariant === "border-with-text" ? (
<BorderButton
module={selectedModule}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Module"
tooltipContent={
Array.isArray(value) ? `${value?.length ?? 0} module${value?.length !== 1 ? "s" : ""}` : ""
}
showTooltip={showTooltip}
variant={buttonVariant}
>
<ButtonContent
disabled={disabled}
dropdownArrow={dropdownArrow}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
showCount={showCount}
value={value}
// @ts-ignore
onChange={onChange}
/>
) : buttonVariant === "border-without-text" ? (
<BorderButton
module={selectedModule}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "background-with-text" ? (
<BackgroundButton
module={selectedModule}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "background-without-text" ? (
<BackgroundButton
module={selectedModule}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-with-text" ? (
<TransparentButton
module={selectedModule}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
<TransparentButton
module={selectedModule}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : null}
</DropdownButton>
</button>
)}
</Combobox.Button>
@@ -361,11 +342,15 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
cn(
"w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none",
{
"bg-custom-background-80": active,
"text-custom-text-100": selected,
"text-custom-text-200": !selected,
}
)
}
onClick={closeDropdown}
>
{({ selected }) => (
<>
+56 -95
View File
@@ -15,6 +15,7 @@ import { TIssuePriorities } from "@plane/types";
import { TDropdownProps } from "./types";
// constants
import { ISSUE_PRIORITIES } from "constants/issue";
import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants";
type Props = TDropdownProps & {
button?: ReactNode;
@@ -34,7 +35,7 @@ type ButtonProps = {
isActive?: boolean;
highlightUrgent: boolean;
priority: TIssuePriorities;
tooltip: boolean;
showTooltip: boolean;
};
const BorderButton = (props: ButtonProps) => {
@@ -46,7 +47,7 @@ const BorderButton = (props: ButtonProps) => {
hideText = false,
highlightUrgent,
priority,
tooltip,
showTooltip,
} = props;
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
@@ -60,7 +61,7 @@ const BorderButton = (props: ButtonProps) => {
};
return (
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}>
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5",
@@ -115,7 +116,7 @@ const BackgroundButton = (props: ButtonProps) => {
hideText = false,
highlightUrgent,
priority,
tooltip,
showTooltip,
} = props;
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
@@ -129,7 +130,7 @@ const BackgroundButton = (props: ButtonProps) => {
};
return (
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}>
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5",
@@ -185,7 +186,7 @@ const TransparentButton = (props: ButtonProps) => {
isActive = false,
highlightUrgent,
priority,
tooltip,
showTooltip,
} = props;
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
@@ -199,7 +200,7 @@ const TransparentButton = (props: ButtonProps) => {
};
return (
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}>
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
@@ -260,8 +261,8 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
highlightUrgent = true,
onChange,
placement,
showTooltip = false,
tabIndex,
tooltip = false,
value,
} = props;
// states
@@ -302,13 +303,40 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
const filteredOptions =
query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
const openDropdown = () => {
setIsOpen(true);
const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const handleClose = () => {
if (isOpen) setIsOpen(false);
if (referenceElement) referenceElement.blur();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: TIssuePriorities) => {
onChange(val);
handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant)
? BorderButton
: BACKGROUND_BUTTON_VARIANTS.includes(buttonVariant)
? BackgroundButton
: TransparentButton;
return (
<Combobox
@@ -323,7 +351,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
className
)}
value={value}
onChange={onChange}
onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
>
@@ -333,7 +361,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown}
onClick={handleOnClick}
>
{button}
</button>
@@ -349,86 +377,20 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
},
buttonContainerClassName
)}
onClick={openDropdown}
onClick={handleOnClick}
>
{buttonVariant === "border-with-text" ? (
<BorderButton
priority={value}
className={cn(buttonClassName, {
"text-white": resolvedTheme === "dark",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
<BorderButton
priority={value}
className={cn(buttonClassName, {
"text-white": resolvedTheme === "dark",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
tooltip={tooltip}
hideText
/>
) : buttonVariant === "background-with-text" ? (
<BackgroundButton
priority={value}
className={cn(buttonClassName, {
"text-white": resolvedTheme === "dark",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
tooltip={tooltip}
/>
) : buttonVariant === "background-without-text" ? (
<BackgroundButton
priority={value}
className={cn(buttonClassName, {
"text-white": resolvedTheme === "dark",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
tooltip={tooltip}
hideText
/>
) : buttonVariant === "transparent-with-text" ? (
<TransparentButton
priority={value}
className={cn(buttonClassName, {
"text-white": resolvedTheme === "dark",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
<TransparentButton
priority={value}
className={cn(buttonClassName, {
"text-white": resolvedTheme === "dark",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
isActive={isOpen}
tooltip={tooltip}
hideText
/>
) : null}
<ButtonToRender
priority={value}
className={cn(buttonClassName, {
"text-white": resolvedTheme === "dark",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
showTooltip={showTooltip}
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
/>
</button>
)}
</Combobox.Button>
@@ -461,7 +423,6 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={closeDropdown}
>
{({ selected }) => (
<>
+58 -190
View File
@@ -7,14 +7,15 @@ import { Check, ChevronDown, Search } from "lucide-react";
import { useProject } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// ui
import { Tooltip } from "@plane/ui";
// components
import { DropdownButton } from "./buttons";
// helpers
import { cn } from "helpers/common.helper";
import { renderEmoji } from "helpers/emoji.helper";
// types
import { IProject } from "@plane/types";
import { TDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & {
button?: ReactNode;
@@ -24,119 +25,6 @@ type Props = TDropdownProps & {
value: string | null;
};
type ButtonProps = {
className?: string;
dropdownArrow: boolean;
dropdownArrowClassName: string;
hideIcon?: boolean;
hideText?: boolean;
placeholder: string;
project: IProject | null;
tooltip: boolean;
};
const BorderButton = (props: ButtonProps) => {
const {
className,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
placeholder,
project,
tooltip,
} = props;
return (
<Tooltip tooltipHeading="Project" tooltipContent={project?.name ?? placeholder} disabled={!tooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
className
)}
>
{!hideIcon && (
<span className="grid place-items-center flex-shrink-0">
{project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
</span>
)}
{!hideText && <span className="flex-grow truncate">{project?.name ?? placeholder}</span>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
const BackgroundButton = (props: ButtonProps) => {
const {
className,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
placeholder,
project,
tooltip,
} = props;
return (
<Tooltip tooltipHeading="Project" tooltipContent={project?.name ?? placeholder} disabled={!tooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
className
)}
>
{!hideIcon && (
<span className="grid place-items-center flex-shrink-0">
{project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
</span>
)}
{!hideText && <span className="flex-grow truncate">{project?.name ?? placeholder}</span>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
const TransparentButton = (props: ButtonProps) => {
const {
className,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
placeholder,
project,
tooltip,
} = props;
return (
<Tooltip tooltipHeading="Project" tooltipContent={project?.name ?? placeholder} disabled={!tooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
className
)}
>
{!hideIcon && (
<span className="grid place-items-center flex-shrink-0">
{project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
</span>
)}
{!hideText && <span className="flex-grow truncate">{project?.name ?? placeholder}</span>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
export const ProjectDropdown: React.FC<Props> = observer((props) => {
const {
button,
@@ -151,8 +39,8 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
onChange,
placeholder = "Project",
placement,
showTooltip = false,
tabIndex,
tooltip = false,
value,
} = props;
// states
@@ -204,13 +92,34 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
const selectedProject = value ? getProjectById(value) : null;
const openDropdown = () => {
setIsOpen(true);
const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const handleClose = () => {
if (isOpen) setIsOpen(false);
if (referenceElement) referenceElement.blur();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: string) => {
onChange(val);
handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
return (
<Combobox
@@ -219,7 +128,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
tabIndex={tabIndex}
className={cn("h-full", className)}
value={value}
onChange={onChange}
onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
>
@@ -229,7 +138,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown}
onClick={handleOnClick}
>
{button}
</button>
@@ -245,72 +154,32 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
},
buttonContainerClassName
)}
onClick={openDropdown}
onClick={handleOnClick}
>
{buttonVariant === "border-with-text" ? (
<BorderButton
project={selectedProject}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
<BorderButton
project={selectedProject}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "background-with-text" ? (
<BackgroundButton
project={selectedProject}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "background-without-text" ? (
<BackgroundButton
project={selectedProject}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-with-text" ? (
<TransparentButton
project={selectedProject}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
<TransparentButton
project={selectedProject}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText
placeholder={placeholder}
tooltip={tooltip}
/>
) : null}
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Project"
tooltipContent={selectedProject?.name ?? placeholder}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && (
<span className="grid place-items-center flex-shrink-0">
{selectedProject?.emoji
? renderEmoji(selectedProject?.emoji)
: selectedProject?.icon_prop
? renderEmoji(selectedProject?.icon_prop)
: null}
</span>
)}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate">{selectedProject?.name ?? placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
@@ -344,7 +213,6 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={closeDropdown}
>
{({ selected }) => (
<>
+57 -198
View File
@@ -7,13 +7,16 @@ import { Check, ChevronDown, Search } from "lucide-react";
import { useApplication, useProjectState } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { DropdownButton } from "./buttons";
// icons
import { StateGroupIcon, Tooltip } from "@plane/ui";
import { StateGroupIcon } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
import { IState } from "@plane/types";
import { TDropdownProps } from "./types";
// constants
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
type Props = TDropdownProps & {
button?: ReactNode;
@@ -24,130 +27,6 @@ type Props = TDropdownProps & {
value: string;
};
type ButtonProps = {
className?: string;
dropdownArrow: boolean;
dropdownArrowClassName: string;
hideIcon?: boolean;
hideText?: boolean;
isActive?: boolean;
state: IState | undefined;
tooltip: boolean;
};
const BorderButton = (props: ButtonProps) => {
const {
className,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
state,
tooltip,
} = props;
return (
<Tooltip tooltipHeading="State" tooltipContent={state?.name ?? "State"} disabled={!tooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
{
"bg-custom-background-80": isActive,
},
className
)}
>
{!hideIcon && (
<StateGroupIcon
stateGroup={state?.group ?? "backlog"}
color={state?.color}
className="h-3 w-3 flex-shrink-0"
/>
)}
{!hideText && <span className="flex-grow truncate">{state?.name ?? "State"}</span>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
const BackgroundButton = (props: ButtonProps) => {
const {
className,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
state,
tooltip,
} = props;
return (
<Tooltip tooltipHeading="State" tooltipContent={state?.name ?? "State"} disabled={!tooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
className
)}
>
{!hideIcon && (
<StateGroupIcon
stateGroup={state?.group ?? "backlog"}
color={state?.color}
className="h-3 w-3 flex-shrink-0"
/>
)}
{!hideText && <span className="flex-grow truncate">{state?.name ?? "State"}</span>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
const TransparentButton = (props: ButtonProps) => {
const {
className,
dropdownArrow,
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
state,
tooltip,
} = props;
return (
<Tooltip tooltipHeading="State" tooltipContent={state?.name ?? "State"} disabled={!tooltip}>
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
{
"bg-custom-background-80": isActive,
},
className
)}
>
{!hideIcon && (
<StateGroupIcon
stateGroup={state?.group ?? "backlog"}
color={state?.color}
className="h-3 w-3 flex-shrink-0"
/>
)}
{!hideText && <span className="flex-grow truncate">{state?.name ?? "State"}</span>}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</div>
</Tooltip>
);
};
export const StateDropdown: React.FC<Props> = observer((props) => {
const {
button,
@@ -162,8 +41,8 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
onChange,
placement,
projectId,
showTooltip = false,
tabIndex,
tooltip = false,
value,
} = props;
// states
@@ -209,14 +88,35 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
const selectedState = getStateById(value);
const openDropdown = () => {
setIsOpen(true);
const onOpen = () => {
if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const handleClose = () => {
if (isOpen) setIsOpen(false);
if (referenceElement) referenceElement.blur();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: string) => {
onChange(val);
handleClose();
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
return (
<Combobox
@@ -225,7 +125,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
tabIndex={tabIndex}
className={cn("h-full", className)}
value={value}
onChange={onChange}
onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
>
@@ -235,7 +135,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
onClick={openDropdown}
onClick={handleOnClick}
>
{button}
</button>
@@ -251,70 +151,30 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
},
buttonContainerClassName
)}
onClick={openDropdown}
onClick={handleOnClick}
>
{buttonVariant === "border-with-text" ? (
<BorderButton
state={selectedState}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
<BorderButton
state={selectedState}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
isActive={isOpen}
tooltip={tooltip}
hideText
/>
) : buttonVariant === "background-with-text" ? (
<BackgroundButton
state={selectedState}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
tooltip={tooltip}
/>
) : buttonVariant === "background-without-text" ? (
<BackgroundButton
state={selectedState}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
tooltip={tooltip}
hideText
/>
) : buttonVariant === "transparent-with-text" ? (
<TransparentButton
state={selectedState}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
<TransparentButton
state={selectedState}
className={buttonClassName}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
isActive={isOpen}
tooltip={tooltip}
hideText
/>
) : null}
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="State"
tooltipContent={selectedState?.name ?? "State"}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && (
<StateGroupIcon
stateGroup={selectedState?.group ?? "backlog"}
color={selectedState?.color}
className="h-3 w-3 flex-shrink-0"
/>
)}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate">{selectedState?.name ?? "State"}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
@@ -348,7 +208,6 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={closeDropdown}
>
{({ selected }) => (
<>
+1 -2
View File
@@ -17,7 +17,6 @@ export type TDropdownProps = {
hideIcon?: boolean;
placeholder?: string;
placement?: Placement;
showTooltip?: boolean;
tabIndex?: number;
// TODO: rename this prop to showTooltip
tooltip?: boolean;
};
@@ -1,9 +1,11 @@
import { FC } from "react";
// hooks
import { useIssueDetail } from "hooks/store";
import { useChart } from "../hooks";
// helpers
import { ChartAddBlock, ChartDraggable } from "components/gantt-chart";
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "../types";
@@ -31,6 +33,7 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
} = props;
const { activeBlock, dispatch } = useChart();
const { peekIssue } = useIssueDetail();
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
@@ -88,7 +91,14 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
return (
<div
key={`block-${block.id}`}
className={`h-11 ${activeBlock?.id === block.id ? "bg-custom-background-80" : ""}`}
className={cn(
"h-11",
{ "rounded bg-custom-background-80": activeBlock?.id === block.id },
{
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id,
}
)}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
>
+12 -2
View File
@@ -3,12 +3,14 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea
import { MoreVertical } from "lucide-react";
// hooks
import { useChart } from "components/gantt-chart/hooks";
import { useIssueDetail } from "hooks/store";
// ui
import { Loader } from "@plane/ui";
// components
import { GanttQuickAddIssueForm, IssueGanttSidebarBlock } from "components/issues";
// helpers
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
import { TIssue } from "@plane/types";
@@ -45,6 +47,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
const { cycleId } = router.query;
const { activeBlock, dispatch } = useChart();
const { peekIssue } = useIssueDetail();
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
@@ -104,7 +107,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
{(droppableProvided) => (
<div
id={`gantt-sidebar-${cycleId}`}
className="mt-3 max-h-full overflow-y-auto pl-2.5"
className="mt-[12px] max-h-full overflow-y-auto pl-2.5"
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
@@ -130,7 +133,14 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
>
{(provided, snapshot) => (
<div
className={`h-11 ${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
className={cn(
"h-11",
{ "rounded bg-custom-background-80": snapshot.isDragging },
{
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id,
}
)}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
+42 -23
View File
@@ -2,13 +2,13 @@ import { FC } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// ui
import { Breadcrumbs } from "@plane/ui";
import { Breadcrumbs, CustomMenu } from "@plane/ui";
// helper
import { renderEmoji } from "helpers/emoji.helper";
// hooks
import { useProject, useUser } from "hooks/store";
// constants
import { EUserProjectRoles } from "constants/project";
import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
@@ -20,7 +20,7 @@ export const ProjectSettingHeader: FC<IProjectSettingHeader> = observer((props)
const { title } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
membership: { currentProjectRole },
@@ -31,29 +31,48 @@ export const ProjectSettingHeader: FC<IProjectSettingHeader> = observer((props)
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">
<SidebarHamburgerToggle />
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle/>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails?.emoji ? (
renderEmoji(currentProjectDetails.emoji)
) : currentProjectDetails?.icon_prop ? (
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span>
)
}
link={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
/>
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
</Breadcrumbs>
<div className="z-50">
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails?.emoji ? (
renderEmoji(currentProjectDetails.emoji)
) : currentProjectDetails?.icon_prop ? (
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span>
)
}
link={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
/>
<div className="hidden sm:hidden md:block lg:block">
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
</div>
</Breadcrumbs>
</div>
</div>
<CustomMenu
className="flex-shrink-0 block sm:block md:hidden lg:hidden"
maxHeight="lg"
customButton={
<span className="text-xs px-1.5 py-1 border rounded-md text-custom-text-200 border-custom-border-300">{title}</span>
}
placement="bottom-start"
closeOnSelect
>
{PROJECT_SETTINGS_LINKS.map((item) => (
<CustomMenu.MenuItem key={item.key} onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)}>
{item.label}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
</div>
);
+21 -3
View File
@@ -1,12 +1,13 @@
import { FC } from "react";
import { useRouter } from "next/router";
// ui
import { Breadcrumbs } from "@plane/ui";
import { Breadcrumbs, CustomMenu } from "@plane/ui";
import { Settings } from "lucide-react";
// hooks
import { observer } from "mobx-react-lite";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { WORKSPACE_SETTINGS_LINKS } from "constants/workspace";
export interface IWorkspaceSettingHeader {
title: string;
@@ -21,7 +22,7 @@ export const WorkspaceSettingHeader: FC<IWorkspaceSettingHeader> = observer((pro
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">
<SidebarHamburgerToggle/>
<SidebarHamburgerToggle />
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
@@ -30,9 +31,26 @@ export const WorkspaceSettingHeader: FC<IWorkspaceSettingHeader> = observer((pro
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
link={`/${workspaceSlug}/settings`}
/>
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
<div className="hidden sm:hidden md:block lg:block">
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
</div>
</Breadcrumbs>
</div>
<CustomMenu
className="flex-shrink-0 block sm:block md:hidden lg:hidden"
maxHeight="lg"
customButton={
<span className="text-xs px-1.5 py-1 border rounded-md text-custom-text-200 border-custom-border-300">{title}</span>
}
placement="bottom-start"
closeOnSelect
>
{WORKSPACE_SETTINGS_LINKS.map((item) => (
<CustomMenu.MenuItem key={item.key} onClick={() => router.push(`/${workspaceSlug}${item.href}`)}>
{item.label}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
</div>
);
@@ -1,6 +1,6 @@
import { FC } from "react";
import { FC, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { XCircle } from "lucide-react";
import { Eye, EyeOff, XCircle } from "lucide-react";
// hooks
import { useUser } from "hooks/store";
// ui
@@ -24,6 +24,8 @@ export interface IInstanceSetupEmailForm {
export const InstanceSetupSignInForm: FC<IInstanceSetupEmailForm> = (props) => {
const { handleNextStep } = props;
// states
const [showPassword, setShowPassword] = useState(false);
// store hooks
const { fetchCurrentUser } = useUser();
// form info
@@ -107,14 +109,27 @@ export const InstanceSetupSignInForm: FC<IInstanceSetupEmailForm> = (props) => {
required: "Password is required",
}}
render={({ field: { value, onChange } }) => (
<Input
type="password"
value={value}
onChange={onChange}
hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
/>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
)}
/>
<p className="pb-2 text-xs text-custom-text-200">
@@ -7,6 +7,8 @@ import { Plus } from "lucide-react";
import { useApplication, useProject } from "hooks/store";
// components
import { CustomSelect, Input } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
// types
import { IJiraImporterForm } from "@plane/types";
@@ -46,17 +48,18 @@ export const JiraGetImportDetail: React.FC = observer(() => {
render={({ field: { value, onChange, ref } }) => (
<Input
id="metadata.api_token"
name="metadata.api_token"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.metadata?.api_token)}
placeholder="XXXXXXXX"
className="w-full"
autoComplete="off"
/>
)}
/>
{errors.metadata?.api_token && <p className="text-red-500 text-xs">{errors.metadata.api_token.message}</p>}
</div>
</div>
@@ -75,7 +78,6 @@ export const JiraGetImportDetail: React.FC = observer(() => {
render={({ field: { value, onChange, ref } }) => (
<Input
id="metadata.project_key"
name="metadata.project_key"
type="text"
value={value}
onChange={onChange}
@@ -86,6 +88,9 @@ export const JiraGetImportDetail: React.FC = observer(() => {
/>
)}
/>
{errors.metadata?.project_key && (
<p className="text-red-500 text-xs">{errors.metadata.project_key.message}</p>
)}
</div>
</div>
@@ -100,11 +105,11 @@ export const JiraGetImportDetail: React.FC = observer(() => {
name="metadata.email"
rules={{
required: "Please enter email address.",
validate: (value) => checkEmailValidity(value) || "Please enter a valid email address",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="metadata.email"
name="metadata.email"
type="email"
value={value}
onChange={onChange}
@@ -115,6 +120,7 @@ export const JiraGetImportDetail: React.FC = observer(() => {
/>
)}
/>
{errors.metadata?.email && <p className="text-red-500 text-xs">{errors.metadata.email.message}</p>}
</div>
</div>
@@ -129,12 +135,11 @@ export const JiraGetImportDetail: React.FC = observer(() => {
name="metadata.cloud_hostname"
rules={{
required: "Please enter your cloud host name.",
validate: (value) => !/^https?:\/\//.test(value) || "Hostname should not begin with http:// or https://",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="metadata.cloud_hostname"
name="metadata.cloud_hostname"
type="email"
value={value}
onChange={onChange}
ref={ref}
@@ -144,6 +149,9 @@ export const JiraGetImportDetail: React.FC = observer(() => {
/>
)}
/>
{errors.metadata?.cloud_hostname && (
<p className="text-red-500 text-xs">{errors.metadata.cloud_hostname.message}</p>
)}
</div>
</div>
+6 -23
View File
@@ -21,7 +21,7 @@ import {
CycleDropdown,
DateDropdown,
EstimateDropdown,
ModuleSelectDropdown,
ModuleDropdown,
PriorityDropdown,
ProjectDropdown,
ProjectMemberDropdown,
@@ -170,17 +170,6 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
// handleClose();
// };
useEffect(() => {
if (!isOpen || data) return;
setLocalStorageValue(
JSON.stringify({
...payload,
})
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(payload), isOpen, data]);
// const onClose = () => {
// handleClose();
// };
@@ -258,13 +247,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
useEffect(() => {
setFocus("name");
reset({
...defaultValues,
...(prePopulatedData ?? {}),
...(data ?? {}),
});
}, [setFocus, prePopulatedData, reset, data]);
}, [setFocus]);
// update projectId in form when projectId changes
useEffect(() => {
@@ -577,12 +560,12 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
name="module_ids"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ModuleSelectDropdown
workspaceSlug={workspaceSlug?.toString()}
<ModuleDropdown
projectId={projectId}
value={value || undefined}
onChange={(moduleId) => onChange(moduleId)}
value={value ?? []}
onChange={onChange}
buttonVariant="border-with-text"
multiple
/>
</div>
)}
@@ -79,7 +79,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
</div>
<div className="pb-12">
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!is_editable} />
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
</div>
</>
);
@@ -10,14 +10,13 @@ import { TActivityOperations } from "../root";
import { TIssueComment } from "@plane/types";
// icons
import { Globe2, Lock } from "lucide-react";
import { useWorkspace } from "hooks/store";
import { useMention, useWorkspace } from "hooks/store";
const fileService = new FileService();
type TIssueCommentCreate = {
workspaceSlug: string;
activityOperations: TActivityOperations;
disabled: boolean;
showAccessSpecifier?: boolean;
};
@@ -40,10 +39,12 @@ const commentAccess: commentAccessType[] = [
];
export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
const { workspaceSlug, activityOperations, disabled, showAccessSpecifier = false } = props;
const { workspaceSlug, activityOperations, showAccessSpecifier = false } = props;
const workspaceStore = useWorkspace();
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
const { mentionHighlights, mentionSuggestions } = useMention();
// refs
const editorRef = useRef<any>(null);
// react hook form
@@ -62,7 +63,14 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
};
return (
<div>
<div
// onKeyDown={(e) => {
// if (e.key === "Enter" && !e.shiftKey) {
// e.preventDefault();
// // handleSubmit(onSubmit)(e);
// }
// }}
>
<Controller
name="access"
control={control}
@@ -73,6 +81,7 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
render={({ field: { value, onChange } }) => (
<LiteTextEditorWithRef
onEnterKeyPress={(e) => {
console.log("yo");
handleSubmit(onSubmit)(e);
}}
cancelUploadImage={fileService.cancelUpload}
@@ -87,6 +96,8 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
onChange={(comment_json: Object, comment_html: string) => {
onChange(comment_html);
}}
mentionSuggestions={mentionSuggestions}
mentionHighlights={mentionHighlights}
commentAccessSpecifier={
showAccessSpecifier
? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess }
@@ -94,7 +105,7 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
}
submitButton={
<Button
disabled={isSubmitting || disabled}
disabled={isSubmitting}
variant="primary"
type="submit"
className="!px-2.5 !py-1.5 !text-xs"
@@ -13,7 +13,6 @@ type TIssueActivity = {
workspaceSlug: string;
projectId: string;
issueId: string;
disabled: boolean;
};
type TActivityTabs = "all" | "activity" | "comments";
@@ -43,7 +42,7 @@ export type TActivityOperations = {
};
export const IssueActivity: FC<TIssueActivity> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled } = props;
const { workspaceSlug, projectId, issueId } = props;
// hooks
const { createComment, updateComment, removeComment } = useIssueDetail();
const { setToastAlert } = useToast();
@@ -147,14 +146,11 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
activityOperations={activityOperations}
showAccessSpecifier={project.is_deployed}
/>
{!disabled && (
<IssueCommentCreate
workspaceSlug={workspaceSlug}
activityOperations={activityOperations}
disabled={disabled}
showAccessSpecifier={project.is_deployed}
/>
)}
<IssueCommentCreate
workspaceSlug={workspaceSlug}
activityOperations={activityOperations}
showAccessSpecifier={project.is_deployed}
/>
</div>
) : activityTab === "activity" ? (
<IssueActivityRoot issueId={issueId} />
@@ -166,14 +162,11 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
activityOperations={activityOperations}
showAccessSpecifier={project.is_deployed}
/>
{!disabled && (
<IssueCommentCreate
workspaceSlug={workspaceSlug}
activityOperations={activityOperations}
disabled={disabled}
showAccessSpecifier={project.is_deployed}
/>
)}
<IssueCommentCreate
workspaceSlug={workspaceSlug}
activityOperations={activityOperations}
showAccessSpecifier={project.is_deployed}
/>
</div>
)}
</div>
@@ -99,7 +99,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
disabled={!is_editable}
/>
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!is_editable} />
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
</>
);
});
@@ -1,10 +1,9 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
import xor from "lodash/xor";
// hooks
import { useIssueDetail } from "hooks/store";
// components
import { ModuleSelectDropdown } from "components/dropdowns";
import { ModuleDropdown } from "components/dropdowns";
// ui
import { Spinner } from "@plane/ui";
// helpers
@@ -33,56 +32,42 @@ export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props)
const issue = getIssueById(issueId);
const disableSelect = disabled || isUpdating;
const handleIssueModuleChange = async (moduleIds: undefined | string | (string | undefined)[]) => {
if (!issue) return;
const handleIssueModuleChange = async (moduleIds: string[]) => {
if (!issue || !issue.module_ids) return;
setIsUpdating(true);
if (moduleIds === undefined && issue?.module_ids && issue?.module_ids.length > 0)
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, issue?.module_ids);
if (typeof moduleIds === "string" && moduleIds)
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, [moduleIds]);
if (Array.isArray(moduleIds)) {
if (moduleIds.includes(undefined)) {
await issueOperations.removeModulesFromIssue?.(
workspaceSlug,
projectId,
issueId,
moduleIds.filter((x) => x != undefined) as string[]
);
} else {
const _moduleIds = xor(issue?.module_ids, moduleIds)[0];
if (_moduleIds) {
if (issue?.module_ids?.includes(_moduleIds))
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, [_moduleIds]);
else await issueOperations.addModulesToIssue?.(workspaceSlug, projectId, issueId, [_moduleIds]);
}
}
if (moduleIds.length === 0)
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, issue.module_ids);
else if (moduleIds.length > issue.module_ids.length) {
const newModuleIds = moduleIds.filter((m) => !issue.module_ids?.includes(m));
await issueOperations.addModulesToIssue?.(workspaceSlug, projectId, issueId, newModuleIds);
} else if (moduleIds.length < issue.module_ids.length) {
const removedModuleIds = issue.module_ids.filter((m) => !moduleIds.includes(m));
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, removedModuleIds);
}
setIsUpdating(false);
};
return (
<div className={cn(`flex items-center gap-1 h-full`, className)}>
<ModuleSelectDropdown
workspaceSlug={workspaceSlug}
<ModuleDropdown
projectId={projectId}
value={issue?.module_ids?.length ? issue?.module_ids : undefined}
value={issue?.module_ids ?? []}
onChange={handleIssueModuleChange}
multiple={true}
placeholder="No module"
disabled={disableSelect}
className={`w-full h-full group`}
className="w-full h-full group"
buttonContainerClassName="w-full"
buttonClassName={`min-h-8 ${issue?.module_ids?.length ? `` : `text-custom-text-400`}`}
buttonClassName={`min-h-8 text-sm justify-between ${issue?.module_ids?.length ? "" : "text-custom-text-400"}`}
buttonVariant="transparent-with-text"
hideIcon={false}
dropdownArrow={true}
hideIcon
dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
showTooltip={true}
showTooltip
multiple
/>
{isUpdating && <Spinner className="h-4 w-4" />}
</div>
);
@@ -78,7 +78,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-medium mt-0.5"
className="text-xs font-medium"
onClick={(e) => e.stopPropagation()}
>
{parentIssueProjectDetails?.identifier}-{parentIssue.sequence_id}
@@ -132,7 +132,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
href={`/${workspaceSlug}/projects/${projectDetails?.id}/issues/${currentIssue.id}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-medium mt-0.5"
className="text-xs font-medium"
onClick={(e) => e.stopPropagation()}
>
{`${projectDetails?.identifier}-${currentIssue?.sequence_id}`}
+4 -4
View File
@@ -198,15 +198,15 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
try {
await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds);
setToastAlert({
title: "Module removed from issue successfully",
type: "success",
message: "Module removed from issue successfully",
title: "Successful!",
message: "Issue removed from module successfully.",
});
} catch (error) {
setToastAlert({
title: "Module remove from issue failed",
type: "error",
message: "Module remove from issue failed",
title: "Error!",
message: "Issue could not be removed from module. Please try again.",
});
}
},
@@ -233,7 +233,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
showPlaceholderIcon
// TODO: add this logic
// showPlaceholderIcon
/>
</div>
@@ -258,7 +259,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
showPlaceholderIcon
// TODO: add this logic
// showPlaceholderIcon
/>
</div>
@@ -61,7 +61,8 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
projectId?.toString(),
issueStore,
issueMap,
groupedIssueIds
groupedIssueIds,
viewId
).catch((err) => {
setToastAlert({
title: "Error",
@@ -6,7 +6,8 @@ import { MoreHorizontal } from "lucide-react";
import { Tooltip, ControlLink } from "@plane/ui";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// ui
// helpers
import { cn } from "helpers/common.helper";
// types
import { TIssue, TIssueMap } from "@plane/types";
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
@@ -26,7 +27,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
} = useApplication();
const { getProjectById } = useProject();
const { getProjectStates } = useProjectState();
const { setPeekIssue } = useIssueDetail();
const { peekIssue, setPeekIssue } = useIssueDetail();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
@@ -84,11 +85,18 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
)}
<div
className={`group/calendar-block flex h-8 w-full items-center justify-between gap-1.5 rounded border-[0.5px] border-custom-border-100 px-1 py-1.5 shadow-custom-shadow-2xs ${
snapshot.isDragging
? "bg-custom-background-90 shadow-custom-shadow-rg"
: "bg-custom-background-100 hover:bg-custom-background-90"
}`}
className={cn(
"group/calendar-block flex h-8 w-full items-center justify-between gap-1.5 rounded border-[0.5px] border-custom-border-200 hover:border-custom-border-400 px-1 py-1.5 ",
{
"bg-custom-background-90 shadow-custom-shadow-rg border-custom-primary-100":
snapshot.isDragging,
},
{ "bg-custom-background-100 hover:bg-custom-background-90": !snapshot.isDragging },
{
"border border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === issue.id,
}
)}
>
<div className="flex h-full items-center gap-1.5">
<span
@@ -2,7 +2,7 @@ import { useState } from "react";
import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react";
// hooks
import { useApplication, useIssueDetail, useIssues, useUser } from "hooks/store";
import { useApplication, useIssues, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { EmptyState } from "components/common";
@@ -29,7 +29,6 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
// store hooks
const { issues } = useIssues(EIssuesStoreType.MODULE);
const { updateIssue, fetchIssue } = useIssueDetail();
const {
commandPalette: { toggleCreateIssueModal },
@@ -1,6 +1,8 @@
import { memo } from "react";
import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite";
// hooks
import { useApplication, useIssueDetail, useProject } from "hooks/store";
// components
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
import { IssueProperties } from "../properties/all-properties";
@@ -9,9 +11,11 @@ import { Tooltip, ControlLink } from "@plane/ui";
// types
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
import { EIssueActions } from "../types";
import { useApplication, useIssueDetail, useProject } from "hooks/store";
// helper
import { cn } from "helpers/common.helper";
interface IssueBlockProps {
peekIssueId?: string;
issueId: string;
issuesMap: IIssueMap;
displayProperties: IIssueDisplayProperties | undefined;
@@ -86,6 +90,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
const {
peekIssueId,
issueId,
issuesMap,
displayProperties,
@@ -121,9 +126,12 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)}
<div
className={`space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs transition-all ${
isDragDisabled ? "" : "hover:cursor-grab"
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
className={cn(
"space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm transition-all hover:border-custom-border-400",
{ "hover:cursor-grab": !isDragDisabled },
{ "border-custom-primary-100": snapshot.isDragging },
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id }
)}
>
<KanbanIssueDetailsBlock
issue={issue}
@@ -9,6 +9,7 @@ interface IssueBlocksListProps {
sub_group_id: string;
columnId: string;
issuesMap: IIssueMap;
peekIssueId?: string;
issueIds: string[];
displayProperties: IIssueDisplayProperties | undefined;
isDragDisabled: boolean;
@@ -22,6 +23,7 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
sub_group_id,
columnId,
issuesMap,
peekIssueId,
issueIds,
displayProperties,
isDragDisabled,
@@ -44,6 +46,7 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
return (
<KanbanIssueBlock
key={draggableId}
peekIssueId={peekIssueId}
issueId={issueId}
issuesMap={issuesMap}
displayProperties={displayProperties}
@@ -1,6 +1,6 @@
import { observer } from "mobx-react-lite";
// hooks
import { useKanbanView, useLabel, useMember, useProject, useProjectState } from "hooks/store";
import { useIssueDetail, useKanbanView, useLabel, useMember, useProject, useProjectState } from "hooks/store";
// components
import { HeaderGroupByCard } from "./headers/group-by-card";
import { KanbanGroup } from "./kanban-group";
@@ -73,6 +73,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
const project = useProject();
const label = useLabel();
const projectState = useProjectState();
const { peekIssue } = useIssueDetail();
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member);
@@ -120,6 +121,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
groupId={_list.id}
issuesMap={issuesMap}
issueIds={issueIds}
peekIssueId={peekIssue?.issueId ?? ""}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
@@ -17,6 +17,7 @@ import { EIssueActions } from "../types";
interface IKanbanGroup {
groupId: string;
issuesMap: IIssueMap;
peekIssueId?: string;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null;
@@ -47,6 +48,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
issuesMap,
displayProperties,
issueIds,
peekIssueId,
isDragDisabled,
handleIssues,
quickActions,
@@ -118,6 +120,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
sub_group_id={sub_group_id}
columnId={groupId}
issuesMap={issuesMap}
peekIssueId={peekIssueId}
issueIds={(issueIds as TGroupedIssues)?.[groupId] || []}
displayProperties={displayProperties}
isDragDisabled={isDragDisabled}
@@ -5,6 +5,8 @@ import { IssueProperties } from "../properties/all-properties";
import { useApplication, useIssueDetail, useProject } from "hooks/store";
// ui
import { Spinner, Tooltip, ControlLink } from "@plane/ui";
// helper
import { cn } from "helpers/common.helper";
// types
import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types";
import { EIssueActions } from "../types";
@@ -25,7 +27,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
router: { workspaceSlug, projectId },
} = useApplication();
const { getProjectById } = useProject();
const { setPeekIssue } = useIssueDetail();
const { peekIssue, setPeekIssue } = useIssueDetail();
const updateIssue = (issueToUpdate: TIssue) => {
handleIssues(issueToUpdate, EIssueActions.UPDATE);
@@ -47,7 +49,15 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
return (
<>
<div className="relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm">
<div
className={cn(
"relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm border border-transparent border-b-custom-border-200 last:border-b-transparent",
{
"border border-custom-primary-70 hover:border-custom-primary-70":
peekIssue && peekIssue.issueId === issue.id,
}
)}
>
{displayProperties && displayProperties?.key && (
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
{projectDetails?.identifier}-{issue.sequence_id}
@@ -18,7 +18,7 @@ export const IssueBlocksList: FC<Props> = (props) => {
const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties } = props;
return (
<div className="relative h-full w-full divide-y-[0.5px] divide-custom-border-200">
<div className="relative h-full w-full">
{issueIds && issueIds.length > 0 ? (
issueIds.map((issueId: string) => {
if (!issueId) return null;
@@ -81,7 +81,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
projectId={issue.project_id}
disabled={isReadOnly}
buttonVariant="border-with-text"
tooltip
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
@@ -95,7 +95,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
disabled={isReadOnly}
buttonVariant="border-without-text"
buttonClassName="border"
tooltip
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
@@ -123,7 +123,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
placeholder="Start date"
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
disabled={isReadOnly}
tooltip
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
@@ -139,7 +139,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
placeholder="Due date"
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
disabled={isReadOnly}
tooltip
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
@@ -169,7 +169,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
projectId={issue.project_id}
disabled={isReadOnly}
buttonVariant="border-with-text"
tooltip
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
@@ -97,6 +97,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
portalElement={portalElement}
/>
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[handleIssues]
);
@@ -12,6 +12,8 @@ import { ControlLink, Tooltip } from "@plane/ui";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { useIssueDetail, useProject } from "hooks/store";
// helper
import { cn } from "helpers/common.helper";
// types
import { IIssueDisplayProperties, TIssue } from "@plane/types";
import { EIssueActions } from "../types";
@@ -48,7 +50,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
const { workspaceSlug } = router.query;
//hooks
const { getProjectById } = useProject();
const { setPeekIssue } = useIssueDetail();
const { peekIssue, setPeekIssue } = useIssueDetail();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
const [isExpanded, setExpanded] = useState<boolean>(false);
@@ -95,9 +97,20 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
return (
<>
<tr>
<tr
className={cn({
"border border-custom-primary-70 hover:border-custom-primary-70": peekIssue?.issueId === issueDetail.id,
})}
>
{/* first column/ issue name and key column */}
<td className="sticky group left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-l-0 after:border-custom-border-100 before:absolute before:h-full before:right-0 before:border before:border-l-0 before:border-custom-border-100">
<td
className={cn(
"sticky group left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] border-custom-border-200",
{
"border-b-[0.5px]": peekIssue?.issueId !== issueDetail.id,
}
)}
>
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
<div
className="flex min-w-min items-center gap-1.5 px-4 py-2.5 pr-0"
@@ -36,7 +36,7 @@ export const SpreadsheetTable = observer((props: Props) => {
} = props;
return (
<table className="divide-x-[0.5px] divide-custom-border-200 overflow-y-auto">
<table className="overflow-y-auto">
<SpreadsheetHeader
displayProperties={displayProperties}
displayFilters={displayFilters}
+9 -8
View File
@@ -20,7 +20,7 @@ import {
CycleDropdown,
DateDropdown,
EstimateDropdown,
ModuleSelectDropdown,
ModuleDropdown,
PriorityDropdown,
ProjectDropdown,
ProjectMemberDropdown,
@@ -132,6 +132,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
parent_id: formData.parent_id,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]);
const issueName = watch("name");
@@ -267,6 +268,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
handleFormChange();
}}
buttonVariant="border-with-text"
// TODO: update tabIndex logic
tabIndex={19}
/>
</div>
@@ -547,18 +549,17 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
name="module_ids"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ModuleSelectDropdown
workspaceSlug={workspaceSlug.toString()}
<ModuleDropdown
projectId={projectId}
value={value || undefined}
onChange={(moduleId) => {
onChange(moduleId);
value={value ?? []}
onChange={(moduleIds) => {
onChange(moduleIds);
handleFormChange();
}}
buttonVariant="border-with-text"
tabIndex={13}
multiple={true}
showCount={true}
multiple
showCount
/>
</div>
)}
@@ -148,7 +148,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
showPlaceholderIcon
// TODO: add this logic
// showPlaceholderIcon
/>
</div>
@@ -174,7 +175,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
showPlaceholderIcon
// TODO: add this logic
// showPlaceholderIcon
/>
</div>
@@ -234,7 +234,6 @@ export const IssueView: FC<IIssueView> = observer((props) => {
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={disabled}
/>
</div>
) : (
@@ -255,7 +254,6 @@ export const IssueView: FC<IIssueView> = observer((props) => {
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={disabled}
/>
</div>
</div>
+27 -7
View File
@@ -47,14 +47,34 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const filteredOptions =
query === "" ? projectLabels : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
const openDropdown = () => {
setIsDropdownOpen(true);
const onOpen = () => {
if (!projectLabels && workspaceSlug && projectId) fetchProjectLabels(workspaceSlug.toString(), projectId);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsDropdownOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isDropdownOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const handleClose = () => {
if (isDropdownOpen) setIsDropdownOpen(false);
if (referenceElement) referenceElement.blur();
};
const toggleDropdown = () => {
if (!isDropdownOpen) onOpen();
setIsDropdownOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: string[]) => {
onChange(val);
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
return (
<Combobox
@@ -62,7 +82,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
ref={dropdownRef}
tabIndex={tabIndex}
value={value}
onChange={(val) => onChange(val)}
onChange={dropdownOnChange}
className="relative flex-shrink-0 h-full"
multiple
disabled={disabled}
@@ -73,7 +93,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
type="button"
ref={setReferenceElement}
className="h-full flex cursor-pointer items-center gap-2 text-xs text-custom-text-200"
onClick={openDropdown}
onClick={handleOnClick}
>
{label ? (
label
+13 -6
View File
@@ -1,4 +1,4 @@
import { FC, useEffect } from "react";
import { FC, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
// hooks
import { useApplication, useProject, useWorkspace } from "hooks/store";
@@ -29,6 +29,8 @@ const projectService = new ProjectService();
export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
const { project, workspaceSlug, isAdmin } = props;
// states
const [isLoading, setIsLoading] = useState(false);
// store hooks
const {
eventTracker: { postHogEventTracker },
@@ -45,7 +47,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
setValue,
setError,
reset,
formState: { errors, isSubmitting },
formState: { errors },
} = useForm<IProject>({
defaultValues: {
...project,
@@ -114,6 +116,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
const onSubmit = async (formData: IProject) => {
if (!workspaceSlug) return;
setIsLoading(true);
const payload: Partial<IProject> = {
name: formData.name,
@@ -139,6 +142,10 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
else await handleUpdateChange(payload);
});
else await handleUpdateChange(payload);
setTimeout(() => {
setIsLoading(false);
}, 300);
};
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === project?.network);
@@ -147,10 +154,10 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="relative mt-6 h-44 w-full">
<div className="absolute inset-0 z-[1] bg-gradient-to-t from-black/50 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
<img src={watch("cover_image")!} alt={watch("cover_image")!} className="h-44 w-full rounded-md object-cover" />
<div className="absolute bottom-4 z-10 flex w-full items-end justify-between gap-3 px-4">
<div className="absolute bottom-4 z-5 flex w-full items-end justify-between gap-3 px-4">
<div className="flex flex-grow gap-3 truncate">
<div className="flex h-[52px] w-[52px] flex-shrink-0 items-center justify-center rounded-lg bg-custom-background-90">
<div className="grid h-7 w-7 place-items-center">
@@ -308,8 +315,8 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
<div className="flex items-center justify-between py-2">
<>
<Button variant="primary" type="submit" loading={isSubmitting} disabled={!isAdmin}>
{isSubmitting ? "Updating" : "Update project"}
<Button variant="primary" type="submit" loading={isLoading} disabled={!isAdmin}>
{isLoading ? "Updating..." : "Update project"}
</Button>
<span className="text-sm italic text-custom-sidebar-text-400">
Created on {renderFormattedDate(project?.created_at)}
@@ -32,6 +32,7 @@ const fileService = new FileService();
export const WorkspaceDetails: FC = observer(() => {
// states
const [isLoading, setIsLoading] = useState(false);
const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState(false);
const [isImageRemoving, setIsImageRemoving] = useState(false);
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
@@ -51,7 +52,7 @@ export const WorkspaceDetails: FC = observer(() => {
control,
reset,
watch,
formState: { errors, isSubmitting },
formState: { errors },
} = useForm<IWorkspace>({
defaultValues: { ...defaultValues, ...currentWorkspace },
});
@@ -59,6 +60,8 @@ export const WorkspaceDetails: FC = observer(() => {
const onSubmit = async (formData: IWorkspace) => {
if (!currentWorkspace) return;
setIsLoading(true);
const payload: Partial<IWorkspace> = {
logo: formData.logo,
name: formData.name,
@@ -83,6 +86,9 @@ export const WorkspaceDetails: FC = observer(() => {
});
console.error(err);
});
setTimeout(() => {
setIsLoading(false);
}, 300);
};
const handleRemoveLogo = () => {
@@ -289,8 +295,8 @@ export const WorkspaceDetails: FC = observer(() => {
{isAdmin && (
<div className="flex items-center justify-between py-2">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Update Workspace"}
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isLoading}>
{isLoading ? "Updating..." : "Update Workspace"}
</Button>
</div>
)}
+37 -22
View File
@@ -1,4 +1,4 @@
import { Fragment } from "react";
import { Fragment, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import Link from "next/link";
@@ -6,6 +6,7 @@ import { useTheme } from "next-themes";
import { Menu, Transition } from "@headlessui/react";
import { mutate } from "swr";
import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react";
import { usePopper } from "react-popper";
// hooks
import { useApplication, useUser, useWorkspace } from "hooks/store";
// hooks
@@ -14,7 +15,6 @@ import useToast from "hooks/use-toast";
import { Avatar, Loader } from "@plane/ui";
// types
import { IWorkspace } from "@plane/types";
// Static Data
const userLinks = (workspaceSlug: string, userId: string) => [
{
@@ -36,7 +36,6 @@ const userLinks = (workspaceSlug: string, userId: string) => [
icon: Settings,
},
];
const profileLinks = (workspaceSlug: string, userId: string) => [
{
name: "View profile",
@@ -49,7 +48,6 @@ const profileLinks = (workspaceSlug: string, userId: string) => [
link: "/profile",
},
];
export const WorkspaceSidebarDropdown = observer(() => {
// router
const router = useRouter();
@@ -64,12 +62,25 @@ export const WorkspaceSidebarDropdown = observer(() => {
// hooks
const { setToastAlert } = useToast();
const { setTheme } = useTheme();
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "right",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const handleWorkspaceNavigation = (workspace: IWorkspace) =>
updateCurrentUser({
last_workspace_id: workspace?.id,
});
const handleSignOut = async () => {
await signOut()
.then(() => {
@@ -85,16 +96,12 @@ export const WorkspaceSidebarDropdown = observer(() => {
})
);
};
const handleItemClick = () => {
console.log('CLICKED')
if (window.innerWidth < 768) {
toggleSidebar();
}
};
const workspacesList = Object.values(workspaces ?? {});
// TODO: fix workspaces list scroll
return (
<div className="flex items-center gap-x-3 gap-y-2 px-4 pt-4">
@@ -121,14 +128,12 @@ export const WorkspaceSidebarDropdown = observer(() => {
activeWorkspace?.name?.charAt(0) ?? "..."
)}
</div>
{!sidebarCollapsed && (
<h4 className="truncate text-base font-medium text-custom-text-100">
{activeWorkspace?.name ? activeWorkspace.name : "Loading..."}
</h4>
)}
</div>
{!sidebarCollapsed && (
<ChevronDown
className={`mx-1 hidden h-4 w-4 flex-shrink-0 group-hover/menu-button:block ${open ? "rotate-180" : ""
@@ -137,7 +142,6 @@ export const WorkspaceSidebarDropdown = observer(() => {
)}
</div>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
@@ -185,7 +189,6 @@ export const WorkspaceSidebarDropdown = observer(() => {
workspace?.name?.charAt(0) ?? "..."
)}
</span>
<h5
className={`truncate text-sm font-medium ${workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
}`}
@@ -226,9 +229,14 @@ export const WorkspaceSidebarDropdown = observer(() => {
</Menu.Item>
</Link>
{userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
<Link key={link.key} href={link.href} className="w-full" onClick={() => {
if (index > 0) handleItemClick();
}}>
<Link
key={link.key}
href={link.href}
className="w-full"
onClick={() => {
if (index > 0) handleItemClick();
}}
>
<Menu.Item
as="div"
className="flex items-center gap-2 rounded px-2 py-1 text-sm text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 font-medium"
@@ -256,10 +264,9 @@ export const WorkspaceSidebarDropdown = observer(() => {
</>
)}
</Menu>
{!sidebarCollapsed && (
<Menu as="div" className="relative flex-shrink-0">
<Menu.Button className="grid place-items-center outline-none">
<Menu.Button className="grid place-items-center outline-none" ref={setReferenceElement}>
<Avatar
name={currentUser?.display_name}
src={currentUser?.avatar}
@@ -268,7 +275,6 @@ export const WorkspaceSidebarDropdown = observer(() => {
className="!text-base"
/>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
@@ -281,11 +287,20 @@ export const WorkspaceSidebarDropdown = observer(() => {
<Menu.Items
className="absolute left-0 z-20 mt-1 flex w-52 origin-top-left flex-col divide-y
divide-custom-sidebar-border-200 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
{profileLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
<Link key={index} href={link.link} onClick={() => { if (index == 0) handleItemClick(); }}>
<Link
key={index}
href={link.link}
onClick={() => {
if (index == 0) handleItemClick();
}}
>
<Menu.Item key={index} as="div">
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
<link.icon className="h-4 w-4 stroke-[1.5]" />
@@ -323,4 +338,4 @@ export const WorkspaceSidebarDropdown = observer(() => {
)}
</div>
);
});
});
+21
View File
@@ -324,6 +324,27 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
values: [],
},
},
list: {
filters: [
"priority",
"state_group",
"labels",
"assignees",
"created_by",
"subscriber",
"project",
"start_date",
"target_date",
],
display_properties: true,
display_filters: {
type: [null, "active", "backlog"],
},
extra_options: {
access: false,
values: [],
},
},
},
issues: {
list: {
+8 -8
View File
@@ -1,23 +1,23 @@
import { useCallback } from "react";
type TUseDropdownKeyDown = {
(onOpen: () => void, onClose: () => void, isOpen: boolean): (event: React.KeyboardEvent<HTMLElement>) => void;
(onEnterKeyDown: () => void, onEscKeyDown: () => void): (event: React.KeyboardEvent<HTMLElement>) => void;
};
export const useDropdownKeyDown: TUseDropdownKeyDown = (onOpen, onClose, isOpen) => {
export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown) => {
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "Enter") {
event.stopPropagation();
if (!isOpen) {
onOpen();
}
} else if (event.key === "Escape" && isOpen) {
event.preventDefault();
onEnterKeyDown();
} else if (event.key === "Escape") {
event.stopPropagation();
onClose();
event.preventDefault();
onEscKeyDown();
}
},
[isOpen, onOpen, onClose]
[onEnterKeyDown, onEscKeyDown]
);
return handleKeyDown;
@@ -41,11 +41,13 @@ export const ProjectSettingLayout: FC<IProjectSettingLayout> = observer((props)
}
/>
) : (
<div className="flex h-full w-full gap-2 overflow-x-hidden overflow-y-scroll">
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8">
<div className="inset-y-0 z-20 flex flex-grow-0 h-full w-full gap-2 overflow-x-hidden overflow-y-scroll">
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8 sm:hidden hidden md:block lg:block">
<ProjectSettingsSidebar />
</div>
{children}
<div className="w-full pl-10 sm:pl-10 md:pl-0 lg:pl-0">
{children}
</div>
</div>
);
});
@@ -10,11 +10,13 @@ export const WorkspaceSettingLayout: FC<IWorkspaceSettingLayout> = (props) => {
const { children } = props;
return (
<div className="flex h-full w-full gap-2 overflow-x-hidden overflow-y-scroll">
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8">
<div className="inset-y-0 z-20 flex h-full w-full gap-2 overflow-x-hidden overflow-y-scroll">
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8 sm:hidden hidden md:block lg:block">
<WorkspaceSettingsSidebar />
</div>
{children}
<div className="w-full pl-10 sm:pl-10 md:pl-0 lg:pl-0">
{children}
</div>
</div>
);
};
+2 -3
View File
@@ -47,7 +47,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
<InstanceLayout>
<StoreWrapper>
<CrispWrapper user={currentUser}>
{/* <PosthogWrapper
<PosthogWrapper
user={currentUser}
workspaceRole={currentWorkspaceRole}
projectRole={currentProjectRole}
@@ -55,8 +55,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
posthogHost={envConfig?.posthog_host || null}
>
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
</PosthogWrapper> */}
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
</PosthogWrapper>
</CrispWrapper>
</StoreWrapper>
</InstanceLayout>
+3 -3
View File
@@ -40,9 +40,9 @@ const PosthogWrapper: FC<IPosthogWrapper> = (props) => {
posthog.init(posthogAPIKey, {
api_host: posthogHost || "https://app.posthog.com",
// Enable debug mode in development
loaded: (posthog) => {
if (process.env.NODE_ENV === "development") posthog.debug();
},
// loaded: (posthog) => {
// if (process.env.NODE_ENV === "development") posthog.debug();
// },
autocapture: false,
capture_pageview: false, // Disable automatic pageview capture, as we capture manually
});
+27 -10
View File
@@ -1,4 +1,4 @@
import { ReactElement } from "react";
import { ReactElement, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form";
@@ -19,6 +19,8 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
import { checkEmailValidity } from "helpers/string.helper";
// type
import { NextPageWithLayout } from "lib/types";
// icons
import { Eye, EyeOff } from "lucide-react";
type TResetPasswordFormValues = {
email: string;
@@ -37,6 +39,8 @@ const ResetPasswordPage: NextPageWithLayout = () => {
// router
const router = useRouter();
const { uidb64, token, email } = router.query;
// states
const [showPassword, setShowPassword] = useState(false);
// toast
const { setToastAlert } = useToast();
// sign in redirection hook
@@ -117,15 +121,28 @@ const ResetPasswordPage: NextPageWithLayout = () => {
required: "Password is required",
}}
render={({ field: { value, onChange } }) => (
<Input
type="password"
value={value}
onChange={onChange}
hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
/>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
)}
/>
<Button
+8 -3
View File
@@ -39,6 +39,7 @@ const fileService = new FileService();
const ProfileSettingsPage: NextPageWithLayout = observer(() => {
// states
const [isLoading, setIsLoading] = useState(false);
const [isRemoving, setIsRemoving] = useState(false);
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
const [deactivateAccountModal, setDeactivateAccountModal] = useState(false);
@@ -48,7 +49,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
reset,
watch,
control,
formState: { errors, isSubmitting },
formState: { errors },
} = useForm<IUser>({ defaultValues });
// toast alert
const { setToastAlert } = useToast();
@@ -62,6 +63,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
}, [myProfile, reset]);
const onSubmit = async (formData: IUser) => {
setIsLoading(true);
const payload: Partial<IUser> = {
first_name: formData.first_name,
last_name: formData.last_name,
@@ -87,6 +89,9 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
message: "There was some error in updating your profile. Please try again.",
})
);
setTimeout(() => {
setIsLoading(false);
}, 300);
};
const handleDelete = (url: string | null | undefined, updateUser: boolean = false) => {
@@ -388,8 +393,8 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
</div>
<div className="flex items-center justify-between py-2">
<Button variant="primary" type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save changes"}
<Button variant="primary" type="submit" loading={isLoading}>
{isLoading ? "Saving..." : "Save changes"}
</Button>
</div>
</div>
+2 -3
View File
@@ -200,6 +200,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
if (!cycleId) throw new Error("Cycle Id is required");
const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
return response;
} catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId);
@@ -267,9 +268,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
});
runInAction(() => {
update(this.issues, cycleId, (cycleIssueIds = []) => {
uniq(concat(cycleIssueIds, issueIds));
});
update(this.issues, cycleId, (cycleIssueIds = []) => uniq(concat(cycleIssueIds, issueIds)));
});
issueIds.forEach((issueId) => {
this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId });
+1
View File
@@ -205,6 +205,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
if (!moduleId) throw new Error("Module Id is required");
const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
return response;
} catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId);
+6
View File
@@ -38,6 +38,8 @@ export interface IIssueRootStore {
members: string[] | undefined;
projects: string[] | undefined;
rootStore: RootStore;
issues: IIssueStore;
state: IStateStore;
@@ -87,6 +89,8 @@ export class IssueRootStore implements IIssueRootStore {
members: string[] | undefined = undefined;
projects: string[] | undefined = undefined;
rootStore: RootStore;
issues: IIssueStore;
state: IStateStore;
@@ -136,6 +140,8 @@ export class IssueRootStore implements IIssueRootStore {
projects: observable,
});
this.rootStore = rootStore;
autorun(() => {
if (rootStore.user.currentUser?.id) this.currentUserId = rootStore.user.currentUser?.id;
if (rootStore.app.router.workspaceSlug) this.workspaceSlug = rootStore.app.router.workspaceSlug;