mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of https://github.com/makeplane/plane into fix/gantt-scroll
This commit is contained in:
commit
0cea96060e
@ -186,7 +186,7 @@ def send_email_notification(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
summary = "updates were made to the issue by"
|
summary = "Updates were made to the issue by"
|
||||||
|
|
||||||
# Send the mail
|
# Send the mail
|
||||||
subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
|
subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
|
||||||
|
@ -108,14 +108,33 @@
|
|||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
{% if actors_involved > 0 %}
|
{% if actors_involved == 1 %}
|
||||||
|
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
||||||
|
{{summary}}
|
||||||
|
<span style="font-size: 1rem; font-weight: 700; line-height: 28px">
|
||||||
|
{{ data.0.actor_detail.first_name}}
|
||||||
|
{{data.0.actor_detail.last_name}}
|
||||||
|
</span>.
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
||||||
|
{{summary}}
|
||||||
|
<span style="font-size: 1rem; font-weight: 700; line-height: 28px">
|
||||||
|
{{ data.0.actor_detail.first_name}}
|
||||||
|
{{data.0.actor_detail.last_name }}
|
||||||
|
</span>and others.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<!-- {% if actors_involved == 1 %}
|
||||||
{% if data|length > 0 and comments|length == 0 %}
|
{% if data|length > 0 and comments|length == 0 %}
|
||||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
||||||
<span style="font-size: 1rem; font-weight: 700; line-height: 28px">
|
<span style="font-size: 1rem; font-weight: 700; line-height: 28px">
|
||||||
{{ data.0.actor_detail.first_name}}
|
{{ data.0.actor_detail.first_name}}
|
||||||
{{data.0.actor_detail.last_name }}
|
{{data.0.actor_detail.last_name }}
|
||||||
</span>
|
</span>
|
||||||
made {{data|length}} {% if data|length > 1 %}updates{% else %}update{% endif %} to the issue.
|
made {{total_updates}} {% if total_updates > 1 %}updates{% else %}update{% endif %} to the issue.
|
||||||
</p>
|
</p>
|
||||||
{% elif data|length == 0 and comments|length > 0 %}
|
{% elif data|length == 0 and comments|length > 0 %}
|
||||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
||||||
@ -123,7 +142,7 @@
|
|||||||
{{ comments.0.actor_detail.first_name}}
|
{{ comments.0.actor_detail.first_name}}
|
||||||
{{comments.0.actor_detail.last_name }}
|
{{comments.0.actor_detail.last_name }}
|
||||||
</span>
|
</span>
|
||||||
added {{comments|length}} new {% if comments|length > 1 %}comments{% else %}comment{% endif %}.
|
added {{total_comments}} new {% if total_comments > 1 %}comments{% else %}comment{% endif %}.
|
||||||
</p>
|
</p>
|
||||||
{% elif data|length > 0 and comments|length > 0 %}
|
{% elif data|length > 0 and comments|length > 0 %}
|
||||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
||||||
@ -131,14 +150,14 @@
|
|||||||
{{ data.0.actor_detail.first_name}}
|
{{ data.0.actor_detail.first_name}}
|
||||||
{{data.0.actor_detail.last_name }}
|
{{data.0.actor_detail.last_name }}
|
||||||
</span>
|
</span>
|
||||||
made {{data|length}} {% if data|length > 1 %}updates{% else %}update{% endif %} and added {{comments|length}} new {% if comments|length > 1 %}comments{% else %}comment{% endif %} on the issue.
|
made {{total_updates}} {% if total_updates > 1 %}updates{% else %}update{% endif %} and added {{total_comments}} new {% if total_comments > 1 %}comments{% else %}comment{% endif %} on the issue.
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
||||||
There are {{ data|length }} new updates and {{comments|length}} new comments on the issue
|
There are {{ total_updates }} new updates and {{total_comments}} new comments on the issue.
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %} -->
|
||||||
{% for update in data %} {% if update.changes.name %}
|
{% for update in data %} {% if update.changes.name %}
|
||||||
<!-- Issue title updated -->
|
<!-- Issue title updated -->
|
||||||
<p style="font-size: 1rem; line-height: 28px; color: #1f2d5c">
|
<p style="font-size: 1rem; line-height: 28px; color: #1f2d5c">
|
||||||
|
@ -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 it’s 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 { Color } from "@tiptap/extension-color";
|
||||||
import TaskItem from "@tiptap/extension-task-item";
|
import TaskItem from "@tiptap/extension-task-item";
|
||||||
import TaskList from "@tiptap/extension-task-list";
|
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 { Markdown } from "tiptap-markdown";
|
||||||
|
|
||||||
import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
|
|
||||||
import { Table } from "src/ui/extensions/table/table";
|
import { Table } from "src/ui/extensions/table/table";
|
||||||
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
|
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 { 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 { ImageExtension } from "src/ui/extensions/image";
|
||||||
|
|
||||||
import { isValidHttpUrl } from "src/lib/utils";
|
import { isValidHttpUrl } from "src/lib/utils";
|
||||||
import { Mentions } from "src/ui/mentions";
|
import { Mentions } from "src/ui/mentions";
|
||||||
|
|
||||||
import { CustomKeymap } from "src/ui/extensions/keymap";
|
|
||||||
import { CustomCodeBlockExtension } from "src/ui/extensions/code";
|
import { CustomCodeBlockExtension } from "src/ui/extensions/code";
|
||||||
import { CustomQuoteExtension } from "src/ui/extensions/quote";
|
|
||||||
import { ListKeymap } from "src/ui/extensions/custom-list-keymap";
|
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 { DeleteImage } from "src/types/delete-image";
|
||||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||||
@ -55,7 +54,9 @@ export const CoreEditorExtensions = (
|
|||||||
},
|
},
|
||||||
code: false,
|
code: false,
|
||||||
codeBlock: false,
|
codeBlock: false,
|
||||||
horizontalRule: false,
|
horizontalRule: {
|
||||||
|
HTMLAttributes: { class: "mt-4 mb-4" },
|
||||||
|
},
|
||||||
blockquote: false,
|
blockquote: false,
|
||||||
dropcursor: {
|
dropcursor: {
|
||||||
color: "rgba(var(--color-text-100))",
|
color: "rgba(var(--color-text-100))",
|
||||||
@ -104,7 +105,6 @@ export const CoreEditorExtensions = (
|
|||||||
transformCopiedText: true,
|
transformCopiedText: true,
|
||||||
transformPastedText: true,
|
transformPastedText: true,
|
||||||
}),
|
}),
|
||||||
HorizontalRule,
|
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableCell,
|
TableCell,
|
||||||
|
@ -10,6 +10,11 @@ export interface CustomMentionOptions extends MentionOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CustomMention = Mention.extend<CustomMentionOptions>({
|
export const CustomMention = Mention.extend<CustomMentionOptions>({
|
||||||
|
addStorage(this) {
|
||||||
|
return {
|
||||||
|
mentionsOpen: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
id: {
|
id: {
|
||||||
|
@ -14,6 +14,7 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
|
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
|
||||||
|
props.editor.storage.mentionsOpen = true;
|
||||||
reactRenderer = new ReactRenderer(MentionList, {
|
reactRenderer = new ReactRenderer(MentionList, {
|
||||||
props,
|
props,
|
||||||
editor: props.editor,
|
editor: props.editor,
|
||||||
@ -45,10 +46,18 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
|
||||||
return reactRenderer?.ref?.onKeyDown(props);
|
|
||||||
|
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();
|
popup?.[0].destroy();
|
||||||
reactRenderer?.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 { Table } from "src/ui/extensions/table/table";
|
||||||
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
|
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
|
||||||
import { TableRow } from "src/ui/extensions/table/table-row/table-row";
|
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 { ReadOnlyImageExtension } from "src/ui/extensions/image/read-only-image";
|
||||||
import { isValidHttpUrl } from "src/lib/utils";
|
import { isValidHttpUrl } from "src/lib/utils";
|
||||||
@ -51,7 +50,9 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
codeBlock: false,
|
codeBlock: false,
|
||||||
horizontalRule: false,
|
horizontalRule: {
|
||||||
|
HTMLAttributes: { class: "mt-4 mb-4" },
|
||||||
|
},
|
||||||
dropcursor: {
|
dropcursor: {
|
||||||
color: "rgba(var(--color-text-100))",
|
color: "rgba(var(--color-text-100))",
|
||||||
width: 2,
|
width: 2,
|
||||||
@ -72,7 +73,6 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
|||||||
class: "rounded-lg border border-custom-border-300",
|
class: "rounded-lg border border-custom-border-300",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
HorizontalRule,
|
|
||||||
TiptapUnderline,
|
TiptapUnderline,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
Color,
|
Color,
|
||||||
|
@ -4,13 +4,16 @@ export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
|
|||||||
Extension.create({
|
Extension.create({
|
||||||
name: "enterKey",
|
name: "enterKey",
|
||||||
|
|
||||||
addKeyboardShortcuts() {
|
addKeyboardShortcuts(this) {
|
||||||
return {
|
return {
|
||||||
Enter: () => {
|
Enter: () => {
|
||||||
if (onEnterKeyPress) {
|
if (!this.editor.storage.mentionsOpen) {
|
||||||
onEnterKeyPress();
|
if (onEnterKeyPress) {
|
||||||
|
onEnterKeyPress();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
return false;
|
||||||
},
|
},
|
||||||
"Shift-Enter": ({ editor }) =>
|
"Shift-Enter": ({ editor }) =>
|
||||||
editor.commands.first(({ commands }) => [
|
editor.commands.first(({ commands }) => [
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { EnterKeyExtension } from "src/ui/extensions/enter-key-extension";
|
import { EnterKeyExtension } from "src/ui/extensions/enter-key-extension";
|
||||||
|
|
||||||
export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [
|
export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [EnterKeyExtension(onEnterKeyPress)];
|
||||||
// EnterKeyExtension(onEnterKeyPress),
|
|
||||||
];
|
|
||||||
|
@ -31,7 +31,6 @@ export const GoogleSignInButton: FC<Props> = (props) => {
|
|||||||
size: "large",
|
size: "large",
|
||||||
logo_alignment: "center",
|
logo_alignment: "center",
|
||||||
text: type === "sign_in" ? "signin_with" : "signup_with",
|
text: type === "sign_in" ? "signin_with" : "signup_with",
|
||||||
width: 384,
|
|
||||||
} as GsiButtonConfiguration // customization attributes
|
} as GsiButtonConfiguration // customization attributes
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -130,17 +130,29 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
|||||||
onChange(unsplashImages[0].urls.regular);
|
onChange(unsplashImages[0].urls.regular);
|
||||||
}, [value, onChange, unsplashImages]);
|
}, [value, onChange, unsplashImages]);
|
||||||
|
|
||||||
const openDropdown = () => setIsOpen(true);
|
const handleClose = () => {
|
||||||
const closeDropdown = () => setIsOpen(false);
|
if (isOpen) setIsOpen(false);
|
||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
};
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Popover className="relative z-[2]" ref={ref} tabIndex={tabIndex} onKeyDown={handleKeyDown}>
|
<Popover className="relative z-[2]" ref={ref} tabIndex={tabIndex} onKeyDown={handleKeyDown}>
|
||||||
<Popover.Button
|
<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"
|
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}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
@ -587,14 +587,16 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative h-40 w-80">
|
{cycleDetails && cycleDetails.distribution && (
|
||||||
<ProgressChart
|
<div className="relative h-40 w-80">
|
||||||
distribution={cycleDetails.distribution?.completion_chart ?? {}}
|
<ProgressChart
|
||||||
startDate={cycleDetails.start_date ?? ""}
|
distribution={cycleDetails.distribution?.completion_chart ?? {}}
|
||||||
endDate={cycleDetails.end_date ?? ""}
|
startDate={cycleDetails.start_date ?? ""}
|
||||||
totalIssues={cycleDetails.total_issues}
|
endDate={cycleDetails.end_date ?? ""}
|
||||||
/>
|
totalIssues={cycleDetails.total_issues}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
|
@ -77,7 +77,7 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
Issues
|
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}
|
{totalIssues}
|
||||||
</span>
|
</span>
|
||||||
</h6>
|
</h6>
|
||||||
|
@ -72,14 +72,14 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
|||||||
startedCount > 0
|
startedCount > 0
|
||||||
? "started"
|
? "started"
|
||||||
: unStartedCount > 0
|
: unStartedCount > 0
|
||||||
? "unstarted"
|
? "unstarted"
|
||||||
: backlogCount > 0
|
: backlogCount > 0
|
||||||
? "backlog"
|
? "backlog"
|
||||||
: completedCount > 0
|
: completedCount > 0
|
||||||
? "completed"
|
? "completed"
|
||||||
: canceledCount > 0
|
: canceledCount > 0
|
||||||
? "cancelled"
|
? "cancelled"
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
setActiveStateGroup(stateGroup);
|
setActiveStateGroup(stateGroup);
|
||||||
setDefaultStateGroup(stateGroup);
|
setDefaultStateGroup(stateGroup);
|
||||||
@ -151,13 +151,13 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{totalCount > 0 ? (
|
{totalCount > 0 ? (
|
||||||
<div className="flex items-center pl-20 md:pl-11 lg:pl-14 pr-11 mt-11">
|
<div className="flex items-center pl-10 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="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 className="w-full flex justify-center">
|
<div>
|
||||||
<PieGraph
|
<PieGraph
|
||||||
data={chartData}
|
data={chartData}
|
||||||
height="220px"
|
height="220px"
|
||||||
width="220px"
|
width="200px"
|
||||||
innerRadius={0.6}
|
innerRadius={0.6}
|
||||||
cornerRadius={5}
|
cornerRadius={5}
|
||||||
colors={(datum) => datum.data.color}
|
colors={(datum) => datum.data.color}
|
||||||
@ -189,7 +189,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
|||||||
layers={["arcs", CenteredMetric]}
|
layers={["arcs", CenteredMetric]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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) => (
|
{chartData.map((item) => (
|
||||||
<div key={item.id} className="flex items-center justify-between gap-6">
|
<div key={item.id} className="flex items-center justify-between gap-6">
|
||||||
<div className="flex items-center gap-2.5 w-24">
|
<div className="flex items-center gap-2.5 w-24">
|
||||||
|
101
web/components/dropdowns/buttons.tsx
Normal file
101
web/components/dropdowns/buttons.tsx
Normal 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
web/components/dropdowns/constants.ts
Normal file
20
web/components/dropdowns/constants.ts
Normal 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",
|
||||||
|
];
|
@ -7,13 +7,16 @@ import { Check, ChevronDown, Search } from "lucide-react";
|
|||||||
import { useApplication, useCycle } from "hooks/store";
|
import { useApplication, useCycle } from "hooks/store";
|
||||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
// components
|
||||||
|
import { DropdownButton } from "./buttons";
|
||||||
// icons
|
// icons
|
||||||
import { ContrastIcon, Tooltip } from "@plane/ui";
|
import { ContrastIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { ICycle } from "@plane/types";
|
|
||||||
import { TDropdownProps } from "./types";
|
import { TDropdownProps } from "./types";
|
||||||
|
// constants
|
||||||
|
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||||
|
|
||||||
type Props = TDropdownProps & {
|
type Props = TDropdownProps & {
|
||||||
button?: ReactNode;
|
button?: ReactNode;
|
||||||
@ -24,18 +27,6 @@ type Props = TDropdownProps & {
|
|||||||
value: string | null;
|
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 =
|
type DropdownOptions =
|
||||||
| {
|
| {
|
||||||
value: string | null;
|
value: string | null;
|
||||||
@ -44,100 +35,6 @@ type DropdownOptions =
|
|||||||
}[]
|
}[]
|
||||||
| undefined;
|
| 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) => {
|
export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
button,
|
button,
|
||||||
@ -153,8 +50,8 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
placeholder = "Cycle",
|
placeholder = "Cycle",
|
||||||
placement,
|
placement,
|
||||||
projectId,
|
projectId,
|
||||||
|
showTooltip = false,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
tooltip = false,
|
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
@ -221,13 +118,34 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const selectedCycle = value ? getCycleById(value) : null;
|
const selectedCycle = value ? getCycleById(value) : null;
|
||||||
|
|
||||||
const openDropdown = () => {
|
const onOpen = () => {
|
||||||
setIsOpen(true);
|
|
||||||
if (referenceElement) referenceElement.focus();
|
if (referenceElement) referenceElement.focus();
|
||||||
};
|
};
|
||||||
const closeDropdown = () => setIsOpen(false);
|
|
||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
const handleClose = () => {
|
||||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
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 (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
@ -236,7 +154,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
className={cn("h-full", className)}
|
className={cn("h-full", className)}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={dropdownOnChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
@ -246,7 +164,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
</button>
|
</button>
|
||||||
@ -262,77 +180,24 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
},
|
},
|
||||||
buttonContainerClassName
|
buttonContainerClassName
|
||||||
)}
|
)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{/* TODO: move button components to a single file for each dropdown */}
|
<DropdownButton
|
||||||
{buttonVariant === "border-with-text" ? (
|
className={buttonClassName}
|
||||||
<BorderButton
|
isActive={isOpen}
|
||||||
cycle={selectedCycle}
|
tooltipHeading="Cycle"
|
||||||
className={buttonClassName}
|
tooltipContent={selectedCycle?.name ?? placeholder}
|
||||||
dropdownArrow={dropdownArrow && !disabled}
|
showTooltip={showTooltip}
|
||||||
dropdownArrowClassName={dropdownArrowClassName}
|
variant={buttonVariant}
|
||||||
hideIcon={hideIcon}
|
>
|
||||||
placeholder={placeholder}
|
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
|
||||||
isActive={isOpen}
|
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||||
tooltip={tooltip}
|
<span className="flex-grow truncate">{selectedCycle?.name ?? placeholder}</span>
|
||||||
/>
|
)}
|
||||||
) : buttonVariant === "border-without-text" ? (
|
{dropdownArrow && (
|
||||||
<BorderButton
|
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||||
cycle={selectedCycle}
|
)}
|
||||||
className={buttonClassName}
|
</DropdownButton>
|
||||||
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}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
@ -366,7 +231,6 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
active ? "bg-custom-background-80" : ""
|
active ? "bg-custom-background-80" : ""
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||||
}
|
}
|
||||||
onClick={closeDropdown}
|
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
|
@ -2,17 +2,19 @@ import React, { useRef, useState } from "react";
|
|||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
import { Calendar, CalendarDays, X } from "lucide-react";
|
import { CalendarDays, X } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// ui
|
// components
|
||||||
import { Tooltip } from "@plane/ui";
|
import { DropdownButton } from "./buttons";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { TDropdownProps } from "./types";
|
import { TDropdownProps } from "./types";
|
||||||
|
// constants
|
||||||
|
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||||
|
|
||||||
type Props = TDropdownProps & {
|
type Props = TDropdownProps & {
|
||||||
clearIconClassName?: string;
|
clearIconClassName?: string;
|
||||||
@ -23,157 +25,6 @@ type Props = TDropdownProps & {
|
|||||||
onChange: (val: Date | null) => void;
|
onChange: (val: Date | null) => void;
|
||||||
value: Date | string | null;
|
value: Date | string | null;
|
||||||
closeOnSelect?: boolean;
|
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) => {
|
export const DateDropdown: React.FC<Props> = (props) => {
|
||||||
@ -193,9 +44,8 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
|||||||
onChange,
|
onChange,
|
||||||
placeholder = "Date",
|
placeholder = "Date",
|
||||||
placement,
|
placement,
|
||||||
|
showTooltip = false,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
tooltip = false,
|
|
||||||
showPlaceholderIcon = false,
|
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
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 = () => {
|
const onOpen = () => {
|
||||||
setIsOpen(true);
|
|
||||||
if (referenceElement) referenceElement.focus();
|
if (referenceElement) referenceElement.focus();
|
||||||
};
|
};
|
||||||
const closeDropdown = () => setIsOpen(false);
|
|
||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
const handleClose = () => {
|
||||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
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 (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
@ -247,90 +118,30 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
|||||||
},
|
},
|
||||||
buttonContainerClassName
|
buttonContainerClassName
|
||||||
)}
|
)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{buttonVariant === "border-with-text" ? (
|
<DropdownButton
|
||||||
<BorderButton
|
className={buttonClassName}
|
||||||
date={value}
|
isActive={isOpen}
|
||||||
className={buttonClassName}
|
tooltipHeading={placeholder}
|
||||||
clearIconClassName={clearIconClassName}
|
tooltipContent={value ? renderFormattedDate(value) : "None"}
|
||||||
hideIcon={hideIcon}
|
showTooltip={showTooltip}
|
||||||
icon={icon}
|
variant={buttonVariant}
|
||||||
placeholder={placeholder}
|
>
|
||||||
isClearable={isClearable && isDateSelected}
|
{!hideIcon && icon}
|
||||||
onClear={() => onChange(null)}
|
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||||
isActive={isOpen}
|
<span className="flex-grow truncate">{value ? renderFormattedDate(value) : placeholder}</span>
|
||||||
tooltip={tooltip}
|
)}
|
||||||
/>
|
{isClearable && isDateSelected && (
|
||||||
) : buttonVariant === "border-without-text" ? (
|
<X
|
||||||
<BorderButton
|
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||||
date={value}
|
onClick={(e) => {
|
||||||
className={buttonClassName}
|
e.stopPropagation();
|
||||||
clearIconClassName={clearIconClassName}
|
onChange(null);
|
||||||
hideIcon={hideIcon}
|
}}
|
||||||
icon={icon}
|
/>
|
||||||
placeholder={placeholder}
|
)}
|
||||||
isClearable={isClearable && isDateSelected}
|
</DropdownButton>
|
||||||
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}
|
|
||||||
</button>
|
</button>
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
@ -338,10 +149,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
|||||||
<div className="my-1" ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
<div className="my-1" ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
selected={value ? new Date(value) : null}
|
selected={value ? new Date(value) : null}
|
||||||
onChange={(val) => {
|
onChange={dropdownOnChange}
|
||||||
onChange(val);
|
|
||||||
if (closeOnSelect) closeDropdown();
|
|
||||||
}}
|
|
||||||
dateFormat="dd-MM-yyyy"
|
dateFormat="dd-MM-yyyy"
|
||||||
minDate={minDate}
|
minDate={minDate}
|
||||||
maxDate={maxDate}
|
maxDate={maxDate}
|
||||||
|
@ -8,12 +8,14 @@ import sortBy from "lodash/sortBy";
|
|||||||
import { useApplication, useEstimate } from "hooks/store";
|
import { useApplication, useEstimate } from "hooks/store";
|
||||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// ui
|
// components
|
||||||
import { Tooltip } from "@plane/ui";
|
import { DropdownButton } from "./buttons";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { TDropdownProps } from "./types";
|
import { TDropdownProps } from "./types";
|
||||||
|
// constants
|
||||||
|
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||||
|
|
||||||
type Props = TDropdownProps & {
|
type Props = TDropdownProps & {
|
||||||
button?: ReactNode;
|
button?: ReactNode;
|
||||||
@ -24,18 +26,6 @@ type Props = TDropdownProps & {
|
|||||||
value: number | null;
|
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 =
|
type DropdownOptions =
|
||||||
| {
|
| {
|
||||||
value: number | null;
|
value: number | null;
|
||||||
@ -44,118 +34,6 @@ type DropdownOptions =
|
|||||||
}[]
|
}[]
|
||||||
| undefined;
|
| 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) => {
|
export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
button,
|
button,
|
||||||
@ -171,8 +49,8 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
placeholder = "Estimate",
|
placeholder = "Estimate",
|
||||||
placement,
|
placement,
|
||||||
projectId,
|
projectId,
|
||||||
|
showTooltip = false,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
tooltip = false,
|
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
@ -228,15 +106,35 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null;
|
const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null;
|
||||||
|
|
||||||
const openDropdown = () => {
|
const onOpen = () => {
|
||||||
setIsOpen(true);
|
|
||||||
|
|
||||||
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
|
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
|
||||||
if (referenceElement) referenceElement.focus();
|
if (referenceElement) referenceElement.focus();
|
||||||
};
|
};
|
||||||
const closeDropdown = () => setIsOpen(false);
|
|
||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
const handleClose = () => {
|
||||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
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 (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
@ -245,7 +143,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
className={cn("h-full w-full", className)}
|
className={cn("h-full w-full", className)}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={dropdownOnChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
@ -255,7 +153,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
</button>
|
</button>
|
||||||
@ -271,76 +169,24 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
},
|
},
|
||||||
buttonContainerClassName
|
buttonContainerClassName
|
||||||
)}
|
)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{buttonVariant === "border-with-text" ? (
|
<DropdownButton
|
||||||
<BorderButton
|
className={buttonClassName}
|
||||||
estimatePoint={selectedEstimate}
|
isActive={isOpen}
|
||||||
className={buttonClassName}
|
tooltipHeading="Estimate"
|
||||||
dropdownArrow={dropdownArrow && !disabled}
|
tooltipContent={selectedEstimate !== null ? selectedEstimate : placeholder}
|
||||||
dropdownArrowClassName={dropdownArrowClassName}
|
showTooltip={showTooltip}
|
||||||
hideIcon={hideIcon}
|
variant={buttonVariant}
|
||||||
placeholder={placeholder}
|
>
|
||||||
isActive={isOpen}
|
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||||
tooltip={tooltip}
|
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||||
/>
|
<span className="flex-grow truncate">{selectedEstimate !== null ? selectedEstimate : placeholder}</span>
|
||||||
) : buttonVariant === "border-without-text" ? (
|
)}
|
||||||
<BorderButton
|
{dropdownArrow && (
|
||||||
estimatePoint={selectedEstimate}
|
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||||
className={buttonClassName}
|
)}
|
||||||
dropdownArrow={dropdownArrow && !disabled}
|
</DropdownButton>
|
||||||
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}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
@ -374,7 +220,6 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
active ? "bg-custom-background-80" : ""
|
active ? "bg-custom-background-80" : ""
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||||
}
|
}
|
||||||
onClick={closeDropdown}
|
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
|
@ -3,7 +3,6 @@ export * from "./cycle";
|
|||||||
export * from "./date";
|
export * from "./date";
|
||||||
export * from "./estimate";
|
export * from "./estimate";
|
||||||
export * from "./module";
|
export * from "./module";
|
||||||
export * from "./module-select";
|
|
||||||
export * from "./priority";
|
export * from "./priority";
|
||||||
export * from "./project";
|
export * from "./project";
|
||||||
export * from "./state";
|
export * from "./state";
|
||||||
|
37
web/components/dropdowns/member/avatar.tsx
Normal file
37
web/components/dropdowns/member/avatar.tsx
Normal file
@ -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" />;
|
||||||
|
});
|
@ -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,3 +1,2 @@
|
|||||||
export * from "./buttons";
|
|
||||||
export * from "./project-member";
|
export * from "./project-member";
|
||||||
export * from "./workspace-member";
|
export * from "./workspace-member";
|
||||||
|
@ -2,19 +2,22 @@ import { Fragment, useRef, useState } from "react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
import { Check, Search } from "lucide-react";
|
import { Check, ChevronDown, Search } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useMember, useUser } from "hooks/store";
|
import { useApplication, useMember, useUser } from "hooks/store";
|
||||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// components
|
// components
|
||||||
import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns";
|
import { ButtonAvatars } from "./avatar";
|
||||||
|
import { DropdownButton } from "../buttons";
|
||||||
// icons
|
// icons
|
||||||
import { Avatar } from "@plane/ui";
|
import { Avatar } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { MemberDropdownProps } from "./types";
|
import { MemberDropdownProps } from "./types";
|
||||||
|
// constants
|
||||||
|
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -36,8 +39,8 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
placeholder = "Members",
|
placeholder = "Members",
|
||||||
placement,
|
placement,
|
||||||
projectId,
|
projectId,
|
||||||
|
showTooltip = false,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
tooltip = false,
|
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
@ -96,15 +99,35 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
if (multiple) comboboxProps.multiple = true;
|
if (multiple) comboboxProps.multiple = true;
|
||||||
|
|
||||||
const openDropdown = () => {
|
const onOpen = () => {
|
||||||
setIsOpen(true);
|
|
||||||
|
|
||||||
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
|
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
|
||||||
if (referenceElement) referenceElement.focus();
|
if (referenceElement) referenceElement.focus();
|
||||||
};
|
};
|
||||||
const closeDropdown = () => setIsOpen(false);
|
|
||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
const handleClose = () => {
|
||||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
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 (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
@ -112,6 +135,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
className={cn("h-full", className)}
|
className={cn("h-full", className)}
|
||||||
|
onChange={dropdownOnChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
{...comboboxProps}
|
{...comboboxProps}
|
||||||
>
|
>
|
||||||
@ -121,7 +145,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
</button>
|
</button>
|
||||||
@ -137,76 +161,30 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
},
|
},
|
||||||
buttonContainerClassName
|
buttonContainerClassName
|
||||||
)}
|
)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{buttonVariant === "border-with-text" ? (
|
<DropdownButton
|
||||||
<BorderButton
|
className={buttonClassName}
|
||||||
userIds={value}
|
isActive={isOpen}
|
||||||
className={buttonClassName}
|
tooltipHeading={placeholder}
|
||||||
dropdownArrow={dropdownArrow && !disabled}
|
tooltipContent={`${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
|
||||||
dropdownArrowClassName={dropdownArrowClassName}
|
showTooltip={showTooltip}
|
||||||
hideIcon={hideIcon}
|
variant={buttonVariant}
|
||||||
placeholder={placeholder}
|
>
|
||||||
isActive={isOpen}
|
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
|
||||||
tooltip={tooltip}
|
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||||
/>
|
<span className="flex-grow truncate text-sm leading-5">
|
||||||
) : buttonVariant === "border-without-text" ? (
|
{Array.isArray(value) && value.length > 0
|
||||||
<BorderButton
|
? value.length === 1
|
||||||
userIds={value}
|
? getUserDetails(value[0])?.display_name
|
||||||
className={buttonClassName}
|
: ""
|
||||||
dropdownArrow={dropdownArrow && !disabled}
|
: placeholder}
|
||||||
dropdownArrowClassName={dropdownArrowClassName}
|
</span>
|
||||||
hideIcon={hideIcon}
|
)}
|
||||||
placeholder={placeholder}
|
{dropdownArrow && (
|
||||||
isActive={isOpen}
|
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||||
tooltip={tooltip}
|
)}
|
||||||
hideText
|
</DropdownButton>
|
||||||
/>
|
|
||||||
) : 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}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
@ -240,9 +218,6 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
active ? "bg-custom-background-80" : ""
|
active ? "bg-custom-background-80" : ""
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||||
}
|
}
|
||||||
onClick={() => {
|
|
||||||
if (!multiple) closeDropdown();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
|
@ -2,19 +2,22 @@ import { Fragment, useRef, useState } from "react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
import { Check, Search } from "lucide-react";
|
import { Check, ChevronDown, Search } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMember, useUser } from "hooks/store";
|
import { useMember, useUser } from "hooks/store";
|
||||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// components
|
// components
|
||||||
import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns";
|
import { ButtonAvatars } from "./avatar";
|
||||||
|
import { DropdownButton } from "../buttons";
|
||||||
// icons
|
// icons
|
||||||
import { Avatar } from "@plane/ui";
|
import { Avatar } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { MemberDropdownProps } from "./types";
|
import { MemberDropdownProps } from "./types";
|
||||||
|
// constants
|
||||||
|
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
|
||||||
|
|
||||||
export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((props) => {
|
export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
@ -31,13 +34,13 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
|||||||
onChange,
|
onChange,
|
||||||
placeholder = "Members",
|
placeholder = "Members",
|
||||||
placement,
|
placement,
|
||||||
|
showTooltip = false,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
tooltip = false,
|
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
// popper-js refs
|
// popper-js refs
|
||||||
@ -87,13 +90,34 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
|||||||
};
|
};
|
||||||
if (multiple) comboboxProps.multiple = true;
|
if (multiple) comboboxProps.multiple = true;
|
||||||
|
|
||||||
const openDropdown = () => {
|
const onOpen = () => {
|
||||||
setIsOpen(true);
|
|
||||||
if (referenceElement) referenceElement.focus();
|
if (referenceElement) referenceElement.focus();
|
||||||
};
|
};
|
||||||
const closeDropdown = () => setIsOpen(false);
|
|
||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
const handleClose = () => {
|
||||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
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 (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
@ -103,6 +127,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
|||||||
className={cn("h-full", className)}
|
className={cn("h-full", className)}
|
||||||
{...comboboxProps}
|
{...comboboxProps}
|
||||||
handleKeyDown={handleKeyDown}
|
handleKeyDown={handleKeyDown}
|
||||||
|
onChange={dropdownOnChange}
|
||||||
>
|
>
|
||||||
<Combobox.Button as={Fragment}>
|
<Combobox.Button as={Fragment}>
|
||||||
{button ? (
|
{button ? (
|
||||||
@ -110,6 +135,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||||
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
</button>
|
</button>
|
||||||
@ -125,124 +151,82 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
|||||||
},
|
},
|
||||||
buttonContainerClassName
|
buttonContainerClassName
|
||||||
)}
|
)}
|
||||||
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{buttonVariant === "border-with-text" ? (
|
<DropdownButton
|
||||||
<BorderButton
|
className={buttonClassName}
|
||||||
userIds={value}
|
isActive={isOpen}
|
||||||
className={buttonClassName}
|
tooltipHeading={placeholder}
|
||||||
dropdownArrow={dropdownArrow && !disabled}
|
tooltipContent={`${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
|
||||||
dropdownArrowClassName={dropdownArrowClassName}
|
showTooltip={showTooltip}
|
||||||
hideIcon={hideIcon}
|
variant={buttonVariant}
|
||||||
placeholder={placeholder}
|
>
|
||||||
tooltip={tooltip}
|
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
|
||||||
/>
|
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||||
) : buttonVariant === "border-without-text" ? (
|
<span className="flex-grow truncate text-sm leading-5">
|
||||||
<BorderButton
|
{Array.isArray(value) && value.length > 0
|
||||||
userIds={value}
|
? value.length === 1
|
||||||
className={buttonClassName}
|
? getUserDetails(value[0])?.display_name
|
||||||
dropdownArrow={dropdownArrow && !disabled}
|
: ""
|
||||||
dropdownArrowClassName={dropdownArrowClassName}
|
: placeholder}
|
||||||
hideIcon={hideIcon}
|
</span>
|
||||||
placeholder={placeholder}
|
)}
|
||||||
tooltip={tooltip}
|
{dropdownArrow && (
|
||||||
hideText
|
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||||
/>
|
)}
|
||||||
) : buttonVariant === "background-with-text" ? (
|
</DropdownButton>
|
||||||
<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}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
<Combobox.Options className="fixed z-10">
|
{isOpen && (
|
||||||
<div
|
<Combobox.Options className="fixed z-10" static>
|
||||||
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"
|
<div
|
||||||
ref={setPopperElement}
|
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"
|
||||||
style={styles.popper}
|
ref={setPopperElement}
|
||||||
{...attributes.popper}
|
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} />
|
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
|
||||||
<Combobox.Input
|
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
|
||||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
<Combobox.Input
|
||||||
value={query}
|
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
value={query}
|
||||||
placeholder="Search"
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
placeholder="Search"
|
||||||
/>
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
</div>
|
/>
|
||||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
</div>
|
||||||
{filteredOptions ? (
|
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||||
filteredOptions.length > 0 ? (
|
{filteredOptions ? (
|
||||||
filteredOptions.map((option) => (
|
filteredOptions.length > 0 ? (
|
||||||
<Combobox.Option
|
filteredOptions.map((option) => (
|
||||||
key={option.value}
|
<Combobox.Option
|
||||||
value={option.value}
|
key={option.value}
|
||||||
className={({ active, selected }) =>
|
value={option.value}
|
||||||
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
|
className={({ active, selected }) =>
|
||||||
active ? "bg-custom-background-80" : ""
|
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
active ? "bg-custom-background-80" : ""
|
||||||
}
|
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||||
onClick={() => {
|
}
|
||||||
if (!multiple) closeDropdown();
|
>
|
||||||
}}
|
{({ selected }) => (
|
||||||
>
|
<>
|
||||||
{({ selected }) => (
|
<span className="flex-grow truncate">{option.content}</span>
|
||||||
<>
|
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||||
<span className="flex-grow truncate">{option.content}</span>
|
</>
|
||||||
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
)}
|
||||||
</>
|
</Combobox.Option>
|
||||||
)}
|
))
|
||||||
</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>
|
||||||
)
|
)}
|
||||||
) : (
|
</div>
|
||||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Combobox.Options>
|
||||||
</Combobox.Options>
|
)}
|
||||||
</Combobox>
|
</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>
|
|
||||||
);
|
|
||||||
});
|
|
@ -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;
|
|
||||||
};
|
|
@ -2,27 +2,40 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
import { Check, ChevronDown, Search } from "lucide-react";
|
import { Check, ChevronDown, Search, X } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useModule } from "hooks/store";
|
import { useApplication, useModule } from "hooks/store";
|
||||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
// components
|
||||||
|
import { DropdownButton } from "./buttons";
|
||||||
// icons
|
// icons
|
||||||
import { DiceIcon, Tooltip } from "@plane/ui";
|
import { DiceIcon, Tooltip } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { IModule } from "@plane/types";
|
|
||||||
import { TDropdownProps } from "./types";
|
import { TDropdownProps } from "./types";
|
||||||
|
// constants
|
||||||
|
import { BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants";
|
||||||
|
|
||||||
type Props = TDropdownProps & {
|
type Props = TDropdownProps & {
|
||||||
button?: ReactNode;
|
button?: ReactNode;
|
||||||
dropdownArrow?: boolean;
|
dropdownArrow?: boolean;
|
||||||
dropdownArrowClassName?: string;
|
dropdownArrowClassName?: string;
|
||||||
onChange: (val: string | null) => void;
|
|
||||||
projectId: string;
|
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 =
|
type DropdownOptions =
|
||||||
| {
|
| {
|
||||||
@ -32,110 +45,97 @@ type DropdownOptions =
|
|||||||
}[]
|
}[]
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
type ButtonProps = {
|
type ButtonContentProps = {
|
||||||
className?: string;
|
disabled: boolean;
|
||||||
dropdownArrow: boolean;
|
dropdownArrow: boolean;
|
||||||
dropdownArrowClassName: string;
|
dropdownArrowClassName: string;
|
||||||
hideIcon?: boolean;
|
hideIcon: boolean;
|
||||||
hideText?: boolean;
|
hideText: boolean;
|
||||||
isActive?: boolean;
|
onChange: (moduleIds: string[]) => void;
|
||||||
module: IModule | null;
|
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
tooltip: boolean;
|
showCount: boolean;
|
||||||
|
value: string | string[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BorderButton = (props: ButtonProps) => {
|
const ButtonContent: React.FC<ButtonContentProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
className,
|
disabled,
|
||||||
dropdownArrow,
|
dropdownArrow,
|
||||||
dropdownArrowClassName,
|
dropdownArrowClassName,
|
||||||
hideIcon = false,
|
hideIcon,
|
||||||
hideText = false,
|
hideText,
|
||||||
isActive = false,
|
onChange,
|
||||||
module,
|
|
||||||
placeholder,
|
placeholder,
|
||||||
tooltip,
|
showCount,
|
||||||
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
|
// store hooks
|
||||||
|
const { getModuleById } = useModule();
|
||||||
|
|
||||||
return (
|
if (Array.isArray(value))
|
||||||
<Tooltip tooltipHeading="Module" tooltipContent={module?.name ?? placeholder} disabled={!tooltip}>
|
return (
|
||||||
<div
|
<>
|
||||||
className={cn(
|
{showCount ? (
|
||||||
"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 },
|
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||||
className
|
<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 && (
|
{dropdownArrow && (
|
||||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
</Tooltip>
|
);
|
||||||
);
|
else
|
||||||
};
|
return (
|
||||||
|
<>
|
||||||
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
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
{!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 && (
|
{dropdownArrow && (
|
||||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
<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) => {
|
export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||||
@ -149,12 +149,14 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
dropdownArrow = false,
|
dropdownArrow = false,
|
||||||
dropdownArrowClassName = "",
|
dropdownArrowClassName = "",
|
||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
|
multiple,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder = "Module",
|
placeholder = "Module",
|
||||||
placement,
|
placement,
|
||||||
projectId,
|
projectId,
|
||||||
|
showCount = false,
|
||||||
|
showTooltip = false,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
tooltip = false,
|
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
@ -186,7 +188,6 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const options: DropdownOptions = moduleIds?.map((moduleId) => {
|
const options: DropdownOptions = moduleIds?.map((moduleId) => {
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: moduleId,
|
value: moduleId,
|
||||||
query: `${moduleDetails?.name}`,
|
query: `${moduleDetails?.name}`,
|
||||||
@ -198,16 +199,17 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
options?.unshift({
|
if (!multiple)
|
||||||
value: null,
|
options?.unshift({
|
||||||
query: "No module",
|
value: null,
|
||||||
content: (
|
query: "No module",
|
||||||
<div className="flex items-center gap-2">
|
content: (
|
||||||
<DiceIcon className="h-3 w-3 flex-shrink-0" />
|
<div className="flex items-center gap-2">
|
||||||
<span className="flex-grow truncate">No module</span>
|
<DiceIcon className="h-3 w-3 flex-shrink-0" />
|
||||||
</div>
|
<span className="flex-grow truncate">No module</span>
|
||||||
),
|
</div>
|
||||||
});
|
),
|
||||||
|
});
|
||||||
|
|
||||||
const filteredOptions =
|
const filteredOptions =
|
||||||
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
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);
|
if (!moduleIds) fetchModules(workspaceSlug, projectId);
|
||||||
}, [moduleIds, fetchModules, projectId, workspaceSlug]);
|
}, [moduleIds, fetchModules, projectId, workspaceSlug]);
|
||||||
|
|
||||||
const selectedModule = value ? getModuleById(value) : null;
|
const onOpen = () => {
|
||||||
|
|
||||||
const openDropdown = () => {
|
|
||||||
setIsOpen(true);
|
|
||||||
if (referenceElement) referenceElement.focus();
|
if (referenceElement) referenceElement.focus();
|
||||||
};
|
};
|
||||||
const closeDropdown = () => setIsOpen(false);
|
|
||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
const handleClose = () => {
|
||||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
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 (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
@ -235,10 +263,8 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
className={cn("h-full", className)}
|
className={cn("h-full", className)}
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
disabled={disabled}
|
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
{...comboboxProps}
|
||||||
>
|
>
|
||||||
<Combobox.Button as={Fragment}>
|
<Combobox.Button as={Fragment}>
|
||||||
{button ? (
|
{button ? (
|
||||||
@ -246,7 +272,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
</button>
|
</button>
|
||||||
@ -262,76 +288,31 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
},
|
},
|
||||||
buttonContainerClassName
|
buttonContainerClassName
|
||||||
)}
|
)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{buttonVariant === "border-with-text" ? (
|
<DropdownButton
|
||||||
<BorderButton
|
className={buttonClassName}
|
||||||
module={selectedModule}
|
isActive={isOpen}
|
||||||
className={buttonClassName}
|
tooltipHeading="Module"
|
||||||
dropdownArrow={dropdownArrow && !disabled}
|
tooltipContent={
|
||||||
|
Array.isArray(value) ? `${value?.length ?? 0} module${value?.length !== 1 ? "s" : ""}` : ""
|
||||||
|
}
|
||||||
|
showTooltip={showTooltip}
|
||||||
|
variant={buttonVariant}
|
||||||
|
>
|
||||||
|
<ButtonContent
|
||||||
|
disabled={disabled}
|
||||||
|
dropdownArrow={dropdownArrow}
|
||||||
dropdownArrowClassName={dropdownArrowClassName}
|
dropdownArrowClassName={dropdownArrowClassName}
|
||||||
hideIcon={hideIcon}
|
hideIcon={hideIcon}
|
||||||
|
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
isActive={isOpen}
|
showCount={showCount}
|
||||||
tooltip={tooltip}
|
value={value}
|
||||||
|
// @ts-ignore
|
||||||
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
) : buttonVariant === "border-without-text" ? (
|
</DropdownButton>
|
||||||
<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}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
@ -361,11 +342,15 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
|
cn(
|
||||||
active ? "bg-custom-background-80" : ""
|
"w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none",
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
{
|
||||||
|
"bg-custom-background-80": active,
|
||||||
|
"text-custom-text-100": selected,
|
||||||
|
"text-custom-text-200": !selected,
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
onClick={closeDropdown}
|
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
|
@ -15,6 +15,7 @@ import { TIssuePriorities } from "@plane/types";
|
|||||||
import { TDropdownProps } from "./types";
|
import { TDropdownProps } from "./types";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_PRIORITIES } from "constants/issue";
|
import { ISSUE_PRIORITIES } from "constants/issue";
|
||||||
|
import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants";
|
||||||
|
|
||||||
type Props = TDropdownProps & {
|
type Props = TDropdownProps & {
|
||||||
button?: ReactNode;
|
button?: ReactNode;
|
||||||
@ -34,7 +35,7 @@ type ButtonProps = {
|
|||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
highlightUrgent: boolean;
|
highlightUrgent: boolean;
|
||||||
priority: TIssuePriorities;
|
priority: TIssuePriorities;
|
||||||
tooltip: boolean;
|
showTooltip: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BorderButton = (props: ButtonProps) => {
|
const BorderButton = (props: ButtonProps) => {
|
||||||
@ -46,7 +47,7 @@ const BorderButton = (props: ButtonProps) => {
|
|||||||
hideText = false,
|
hideText = false,
|
||||||
highlightUrgent,
|
highlightUrgent,
|
||||||
priority,
|
priority,
|
||||||
tooltip,
|
showTooltip,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
|
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
|
||||||
@ -60,7 +61,7 @@ const BorderButton = (props: ButtonProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}>
|
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5",
|
"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,
|
hideText = false,
|
||||||
highlightUrgent,
|
highlightUrgent,
|
||||||
priority,
|
priority,
|
||||||
tooltip,
|
showTooltip,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
|
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
|
||||||
@ -129,7 +130,7 @@ const BackgroundButton = (props: ButtonProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}>
|
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5",
|
"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,
|
isActive = false,
|
||||||
highlightUrgent,
|
highlightUrgent,
|
||||||
priority,
|
priority,
|
||||||
tooltip,
|
showTooltip,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
|
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
|
||||||
@ -199,7 +200,7 @@ const TransparentButton = (props: ButtonProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}>
|
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
"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,
|
highlightUrgent = true,
|
||||||
onChange,
|
onChange,
|
||||||
placement,
|
placement,
|
||||||
|
showTooltip = false,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
tooltip = false,
|
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
@ -302,13 +303,40 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
const filteredOptions =
|
const filteredOptions =
|
||||||
query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
|
||||||
const openDropdown = () => {
|
const onOpen = () => {
|
||||||
setIsOpen(true);
|
|
||||||
if (referenceElement) referenceElement.focus();
|
if (referenceElement) referenceElement.focus();
|
||||||
};
|
};
|
||||||
const closeDropdown = () => setIsOpen(false);
|
|
||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
const handleClose = () => {
|
||||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
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 (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
@ -323,7 +351,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={dropdownOnChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
@ -333,7 +361,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
</button>
|
</button>
|
||||||
@ -349,86 +377,20 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
},
|
},
|
||||||
buttonContainerClassName
|
buttonContainerClassName
|
||||||
)}
|
)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{buttonVariant === "border-with-text" ? (
|
<ButtonToRender
|
||||||
<BorderButton
|
priority={value}
|
||||||
priority={value}
|
className={cn(buttonClassName, {
|
||||||
className={cn(buttonClassName, {
|
"text-white": resolvedTheme === "dark",
|
||||||
"text-white": resolvedTheme === "dark",
|
})}
|
||||||
})}
|
highlightUrgent={highlightUrgent}
|
||||||
highlightUrgent={highlightUrgent}
|
dropdownArrow={dropdownArrow && !disabled}
|
||||||
dropdownArrow={dropdownArrow && !disabled}
|
dropdownArrowClassName={dropdownArrowClassName}
|
||||||
dropdownArrowClassName={dropdownArrowClassName}
|
hideIcon={hideIcon}
|
||||||
hideIcon={hideIcon}
|
showTooltip={showTooltip}
|
||||||
tooltip={tooltip}
|
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
|
||||||
/>
|
/>
|
||||||
) : 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}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
@ -461,7 +423,6 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
active ? "bg-custom-background-80" : ""
|
active ? "bg-custom-background-80" : ""
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||||
}
|
}
|
||||||
onClick={closeDropdown}
|
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
|
@ -7,14 +7,15 @@ import { Check, ChevronDown, Search } from "lucide-react";
|
|||||||
import { useProject } from "hooks/store";
|
import { useProject } from "hooks/store";
|
||||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// ui
|
// components
|
||||||
import { Tooltip } from "@plane/ui";
|
import { DropdownButton } from "./buttons";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// types
|
// types
|
||||||
import { IProject } from "@plane/types";
|
|
||||||
import { TDropdownProps } from "./types";
|
import { TDropdownProps } from "./types";
|
||||||
|
// constants
|
||||||
|
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||||
|
|
||||||
type Props = TDropdownProps & {
|
type Props = TDropdownProps & {
|
||||||
button?: ReactNode;
|
button?: ReactNode;
|
||||||
@ -24,119 +25,6 @@ type Props = TDropdownProps & {
|
|||||||
value: string | null;
|
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) => {
|
export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
button,
|
button,
|
||||||
@ -151,8 +39,8 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
onChange,
|
onChange,
|
||||||
placeholder = "Project",
|
placeholder = "Project",
|
||||||
placement,
|
placement,
|
||||||
|
showTooltip = false,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
tooltip = false,
|
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
@ -204,13 +92,34 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const selectedProject = value ? getProjectById(value) : null;
|
const selectedProject = value ? getProjectById(value) : null;
|
||||||
|
|
||||||
const openDropdown = () => {
|
const onOpen = () => {
|
||||||
setIsOpen(true);
|
|
||||||
if (referenceElement) referenceElement.focus();
|
if (referenceElement) referenceElement.focus();
|
||||||
};
|
};
|
||||||
const closeDropdown = () => setIsOpen(false);
|
|
||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
const handleClose = () => {
|
||||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
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 (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
@ -219,7 +128,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
className={cn("h-full", className)}
|
className={cn("h-full", className)}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={dropdownOnChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
@ -229,7 +138,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
</button>
|
</button>
|
||||||
@ -245,72 +154,32 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
},
|
},
|
||||||
buttonContainerClassName
|
buttonContainerClassName
|
||||||
)}
|
)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{buttonVariant === "border-with-text" ? (
|
<DropdownButton
|
||||||
<BorderButton
|
className={buttonClassName}
|
||||||
project={selectedProject}
|
isActive={isOpen}
|
||||||
className={buttonClassName}
|
tooltipHeading="Project"
|
||||||
dropdownArrow={dropdownArrow && !disabled}
|
tooltipContent={selectedProject?.name ?? placeholder}
|
||||||
dropdownArrowClassName={dropdownArrowClassName}
|
showTooltip={showTooltip}
|
||||||
hideIcon={hideIcon}
|
variant={buttonVariant}
|
||||||
placeholder={placeholder}
|
>
|
||||||
tooltip={tooltip}
|
{!hideIcon && (
|
||||||
/>
|
<span className="grid place-items-center flex-shrink-0">
|
||||||
) : buttonVariant === "border-without-text" ? (
|
{selectedProject?.emoji
|
||||||
<BorderButton
|
? renderEmoji(selectedProject?.emoji)
|
||||||
project={selectedProject}
|
: selectedProject?.icon_prop
|
||||||
className={buttonClassName}
|
? renderEmoji(selectedProject?.icon_prop)
|
||||||
dropdownArrow={dropdownArrow && !disabled}
|
: null}
|
||||||
dropdownArrowClassName={dropdownArrowClassName}
|
</span>
|
||||||
hideIcon={hideIcon}
|
)}
|
||||||
hideText
|
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||||
placeholder={placeholder}
|
<span className="flex-grow truncate">{selectedProject?.name ?? placeholder}</span>
|
||||||
tooltip={tooltip}
|
)}
|
||||||
/>
|
{dropdownArrow && (
|
||||||
) : buttonVariant === "background-with-text" ? (
|
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||||
<BackgroundButton
|
)}
|
||||||
project={selectedProject}
|
</DropdownButton>
|
||||||
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}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
@ -344,7 +213,6 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
active ? "bg-custom-background-80" : ""
|
active ? "bg-custom-background-80" : ""
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||||
}
|
}
|
||||||
onClick={closeDropdown}
|
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
|
@ -7,13 +7,16 @@ import { Check, ChevronDown, Search } from "lucide-react";
|
|||||||
import { useApplication, useProjectState } from "hooks/store";
|
import { useApplication, useProjectState } from "hooks/store";
|
||||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
// components
|
||||||
|
import { DropdownButton } from "./buttons";
|
||||||
// icons
|
// icons
|
||||||
import { StateGroupIcon, Tooltip } from "@plane/ui";
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { IState } from "@plane/types";
|
|
||||||
import { TDropdownProps } from "./types";
|
import { TDropdownProps } from "./types";
|
||||||
|
// constants
|
||||||
|
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||||
|
|
||||||
type Props = TDropdownProps & {
|
type Props = TDropdownProps & {
|
||||||
button?: ReactNode;
|
button?: ReactNode;
|
||||||
@ -24,130 +27,6 @@ type Props = TDropdownProps & {
|
|||||||
value: string;
|
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) => {
|
export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
button,
|
button,
|
||||||
@ -162,8 +41,8 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
onChange,
|
onChange,
|
||||||
placement,
|
placement,
|
||||||
projectId,
|
projectId,
|
||||||
|
showTooltip = false,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
tooltip = false,
|
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
@ -209,14 +88,35 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const selectedState = getStateById(value);
|
const selectedState = getStateById(value);
|
||||||
|
|
||||||
const openDropdown = () => {
|
const onOpen = () => {
|
||||||
setIsOpen(true);
|
|
||||||
if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId);
|
if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId);
|
||||||
if (referenceElement) referenceElement.focus();
|
if (referenceElement) referenceElement.focus();
|
||||||
};
|
};
|
||||||
const closeDropdown = () => setIsOpen(false);
|
|
||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
const handleClose = () => {
|
||||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
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 (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
@ -225,7 +125,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
className={cn("h-full", className)}
|
className={cn("h-full", className)}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={dropdownOnChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
@ -235,7 +135,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
</button>
|
</button>
|
||||||
@ -251,70 +151,30 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
},
|
},
|
||||||
buttonContainerClassName
|
buttonContainerClassName
|
||||||
)}
|
)}
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{buttonVariant === "border-with-text" ? (
|
<DropdownButton
|
||||||
<BorderButton
|
className={buttonClassName}
|
||||||
state={selectedState}
|
isActive={isOpen}
|
||||||
className={buttonClassName}
|
tooltipHeading="State"
|
||||||
dropdownArrow={dropdownArrow && !disabled}
|
tooltipContent={selectedState?.name ?? "State"}
|
||||||
dropdownArrowClassName={dropdownArrowClassName}
|
showTooltip={showTooltip}
|
||||||
hideIcon={hideIcon}
|
variant={buttonVariant}
|
||||||
isActive={isOpen}
|
>
|
||||||
tooltip={tooltip}
|
{!hideIcon && (
|
||||||
/>
|
<StateGroupIcon
|
||||||
) : buttonVariant === "border-without-text" ? (
|
stateGroup={selectedState?.group ?? "backlog"}
|
||||||
<BorderButton
|
color={selectedState?.color}
|
||||||
state={selectedState}
|
className="h-3 w-3 flex-shrink-0"
|
||||||
className={buttonClassName}
|
/>
|
||||||
dropdownArrow={dropdownArrow && !disabled}
|
)}
|
||||||
dropdownArrowClassName={dropdownArrowClassName}
|
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||||
hideIcon={hideIcon}
|
<span className="flex-grow truncate">{selectedState?.name ?? "State"}</span>
|
||||||
isActive={isOpen}
|
)}
|
||||||
tooltip={tooltip}
|
{dropdownArrow && (
|
||||||
hideText
|
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||||
/>
|
)}
|
||||||
) : buttonVariant === "background-with-text" ? (
|
</DropdownButton>
|
||||||
<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}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
@ -348,7 +208,6 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
active ? "bg-custom-background-80" : ""
|
active ? "bg-custom-background-80" : ""
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||||
}
|
}
|
||||||
onClick={closeDropdown}
|
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
|
3
web/components/dropdowns/types.d.ts
vendored
3
web/components/dropdowns/types.d.ts
vendored
@ -17,7 +17,6 @@ export type TDropdownProps = {
|
|||||||
hideIcon?: boolean;
|
hideIcon?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
|
showTooltip?: boolean;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
// TODO: rename this prop to showTooltip
|
|
||||||
tooltip?: boolean;
|
|
||||||
};
|
};
|
||||||
|
@ -2,13 +2,13 @@ import { FC } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs, CustomMenu } from "@plane/ui";
|
||||||
// helper
|
// helper
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProject, useUser } from "hooks/store";
|
import { useProject, useUser } from "hooks/store";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project";
|
||||||
// components
|
// components
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ export const ProjectSettingHeader: FC<IProjectSettingHeader> = observer((props)
|
|||||||
const { title } = props;
|
const { title } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
@ -31,29 +31,48 @@ export const ProjectSettingHeader: FC<IProjectSettingHeader> = observer((props)
|
|||||||
|
|
||||||
return (
|
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="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">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle/>
|
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs>
|
<div className="z-50">
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs>
|
||||||
type="text"
|
<Breadcrumbs.BreadcrumbItem
|
||||||
label={currentProjectDetails?.name ?? "Project"}
|
type="text"
|
||||||
icon={
|
label={currentProjectDetails?.name ?? "Project"}
|
||||||
currentProjectDetails?.emoji ? (
|
icon={
|
||||||
renderEmoji(currentProjectDetails.emoji)
|
currentProjectDetails?.emoji ? (
|
||||||
) : currentProjectDetails?.icon_prop ? (
|
renderEmoji(currentProjectDetails.emoji)
|
||||||
renderEmoji(currentProjectDetails.icon_prop)
|
) : 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 className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||||
</span>
|
{currentProjectDetails?.name.charAt(0)}
|
||||||
)
|
</span>
|
||||||
}
|
)
|
||||||
link={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
}
|
||||||
/>
|
link={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
|
||||||
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
|
/>
|
||||||
</Breadcrumbs>
|
<div className="hidden sm:hidden md:block lg:block">
|
||||||
|
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
|
||||||
|
</div>
|
||||||
|
</Breadcrumbs>
|
||||||
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs, CustomMenu } from "@plane/ui";
|
||||||
import { Settings } from "lucide-react";
|
import { Settings } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
|
import { WORKSPACE_SETTINGS_LINKS } from "constants/workspace";
|
||||||
|
|
||||||
export interface IWorkspaceSettingHeader {
|
export interface IWorkspaceSettingHeader {
|
||||||
title: string;
|
title: string;
|
||||||
@ -21,7 +22,7 @@ export const WorkspaceSettingHeader: FC<IWorkspaceSettingHeader> = observer((pro
|
|||||||
return (
|
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="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">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle/>
|
<SidebarHamburgerToggle />
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
@ -30,9 +31,26 @@ export const WorkspaceSettingHeader: FC<IWorkspaceSettingHeader> = observer((pro
|
|||||||
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
||||||
link={`/${workspaceSlug}/settings`}
|
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>
|
</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
|
||||||
|
>
|
||||||
|
{WORKSPACE_SETTINGS_LINKS.map((item) => (
|
||||||
|
<CustomMenu.MenuItem key={item.key} onClick={() => router.push(`/${workspaceSlug}${item.href}`)}>
|
||||||
|
{item.label}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -7,6 +7,8 @@ import { Plus } from "lucide-react";
|
|||||||
import { useApplication, useProject } from "hooks/store";
|
import { useApplication, useProject } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { CustomSelect, Input } from "@plane/ui";
|
import { CustomSelect, Input } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { checkEmailValidity } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IJiraImporterForm } from "@plane/types";
|
import { IJiraImporterForm } from "@plane/types";
|
||||||
|
|
||||||
@ -46,17 +48,18 @@ export const JiraGetImportDetail: React.FC = observer(() => {
|
|||||||
render={({ field: { value, onChange, ref } }) => (
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
<Input
|
<Input
|
||||||
id="metadata.api_token"
|
id="metadata.api_token"
|
||||||
name="metadata.api_token"
|
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
hasError={Boolean(errors.metadata?.api_token)}
|
||||||
placeholder="XXXXXXXX"
|
placeholder="XXXXXXXX"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{errors.metadata?.api_token && <p className="text-red-500 text-xs">{errors.metadata.api_token.message}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -75,7 +78,6 @@ export const JiraGetImportDetail: React.FC = observer(() => {
|
|||||||
render={({ field: { value, onChange, ref } }) => (
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
<Input
|
<Input
|
||||||
id="metadata.project_key"
|
id="metadata.project_key"
|
||||||
name="metadata.project_key"
|
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -100,11 +105,11 @@ export const JiraGetImportDetail: React.FC = observer(() => {
|
|||||||
name="metadata.email"
|
name="metadata.email"
|
||||||
rules={{
|
rules={{
|
||||||
required: "Please enter email address.",
|
required: "Please enter email address.",
|
||||||
|
validate: (value) => checkEmailValidity(value) || "Please enter a valid email address",
|
||||||
}}
|
}}
|
||||||
render={({ field: { value, onChange, ref } }) => (
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
<Input
|
<Input
|
||||||
id="metadata.email"
|
id="metadata.email"
|
||||||
name="metadata.email"
|
|
||||||
type="email"
|
type="email"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -129,12 +135,11 @@ export const JiraGetImportDetail: React.FC = observer(() => {
|
|||||||
name="metadata.cloud_hostname"
|
name="metadata.cloud_hostname"
|
||||||
rules={{
|
rules={{
|
||||||
required: "Please enter your cloud host name.",
|
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 } }) => (
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
<Input
|
<Input
|
||||||
id="metadata.cloud_hostname"
|
id="metadata.cloud_hostname"
|
||||||
name="metadata.cloud_hostname"
|
|
||||||
type="email"
|
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
CycleDropdown,
|
CycleDropdown,
|
||||||
DateDropdown,
|
DateDropdown,
|
||||||
EstimateDropdown,
|
EstimateDropdown,
|
||||||
ModuleSelectDropdown,
|
ModuleDropdown,
|
||||||
PriorityDropdown,
|
PriorityDropdown,
|
||||||
ProjectDropdown,
|
ProjectDropdown,
|
||||||
ProjectMemberDropdown,
|
ProjectMemberDropdown,
|
||||||
@ -257,14 +257,16 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFocus("name");
|
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
...(prePopulatedData ?? {}),
|
...(prePopulatedData ?? {}),
|
||||||
...(data ?? {}),
|
...(data ?? {}),
|
||||||
});
|
});
|
||||||
}, [setFocus, prePopulatedData, reset, data]);
|
}, [prePopulatedData, reset, data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFocus("name");
|
||||||
|
}, [setFocus]);
|
||||||
|
|
||||||
// update projectId in form when projectId changes
|
// update projectId in form when projectId changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -577,12 +579,12 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
name="module_ids"
|
name="module_ids"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="h-7">
|
<div className="h-7">
|
||||||
<ModuleSelectDropdown
|
<ModuleDropdown
|
||||||
workspaceSlug={workspaceSlug?.toString()}
|
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
value={value || undefined}
|
value={value ?? []}
|
||||||
onChange={(moduleId) => onChange(moduleId)}
|
onChange={onChange}
|
||||||
buttonVariant="border-with-text"
|
buttonVariant="border-with-text"
|
||||||
|
multiple
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -10,7 +10,7 @@ import { TActivityOperations } from "../root";
|
|||||||
import { TIssueComment } from "@plane/types";
|
import { TIssueComment } from "@plane/types";
|
||||||
// icons
|
// icons
|
||||||
import { Globe2, Lock } from "lucide-react";
|
import { Globe2, Lock } from "lucide-react";
|
||||||
import { useWorkspace } from "hooks/store";
|
import { useMention, useWorkspace } from "hooks/store";
|
||||||
|
|
||||||
const fileService = new FileService();
|
const fileService = new FileService();
|
||||||
|
|
||||||
@ -43,6 +43,8 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
|||||||
const workspaceStore = useWorkspace();
|
const workspaceStore = useWorkspace();
|
||||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
|
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
|
||||||
|
|
||||||
|
const { mentionHighlights, mentionSuggestions } = useMention();
|
||||||
|
|
||||||
// refs
|
// refs
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
// react hook form
|
// react hook form
|
||||||
@ -61,7 +63,14 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
|
// onKeyDown={(e) => {
|
||||||
|
// if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
// e.preventDefault();
|
||||||
|
// // handleSubmit(onSubmit)(e);
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="access"
|
name="access"
|
||||||
control={control}
|
control={control}
|
||||||
@ -72,6 +81,7 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
|||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<LiteTextEditorWithRef
|
<LiteTextEditorWithRef
|
||||||
onEnterKeyPress={(e) => {
|
onEnterKeyPress={(e) => {
|
||||||
|
console.log("yo");
|
||||||
handleSubmit(onSubmit)(e);
|
handleSubmit(onSubmit)(e);
|
||||||
}}
|
}}
|
||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
@ -86,6 +96,8 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
|||||||
onChange={(comment_json: Object, comment_html: string) => {
|
onChange={(comment_json: Object, comment_html: string) => {
|
||||||
onChange(comment_html);
|
onChange(comment_html);
|
||||||
}}
|
}}
|
||||||
|
mentionSuggestions={mentionSuggestions}
|
||||||
|
mentionHighlights={mentionHighlights}
|
||||||
commentAccessSpecifier={
|
commentAccessSpecifier={
|
||||||
showAccessSpecifier
|
showAccessSpecifier
|
||||||
? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess }
|
? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess }
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import xor from "lodash/xor";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { ModuleSelectDropdown } from "components/dropdowns";
|
import { ModuleDropdown } from "components/dropdowns";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -33,56 +32,42 @@ export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props)
|
|||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
const disableSelect = disabled || isUpdating;
|
const disableSelect = disabled || isUpdating;
|
||||||
|
|
||||||
const handleIssueModuleChange = async (moduleIds: undefined | string | (string | undefined)[]) => {
|
const handleIssueModuleChange = async (moduleIds: string[]) => {
|
||||||
if (!issue) return;
|
if (!issue || !issue.module_ids) return;
|
||||||
|
|
||||||
setIsUpdating(true);
|
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)
|
if (moduleIds.length === 0)
|
||||||
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, [moduleIds]);
|
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, issue.module_ids);
|
||||||
|
else if (moduleIds.length > issue.module_ids.length) {
|
||||||
if (Array.isArray(moduleIds)) {
|
const newModuleIds = moduleIds.filter((m) => !issue.module_ids?.includes(m));
|
||||||
if (moduleIds.includes(undefined)) {
|
await issueOperations.addModulesToIssue?.(workspaceSlug, projectId, issueId, newModuleIds);
|
||||||
await issueOperations.removeModulesFromIssue?.(
|
} else if (moduleIds.length < issue.module_ids.length) {
|
||||||
workspaceSlug,
|
const removedModuleIds = issue.module_ids.filter((m) => !moduleIds.includes(m));
|
||||||
projectId,
|
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, removedModuleIds);
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(`flex items-center gap-1 h-full`, className)}>
|
<div className={cn(`flex items-center gap-1 h-full`, className)}>
|
||||||
<ModuleSelectDropdown
|
<ModuleDropdown
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
value={issue?.module_ids?.length ? issue?.module_ids : undefined}
|
value={issue?.module_ids ?? []}
|
||||||
onChange={handleIssueModuleChange}
|
onChange={handleIssueModuleChange}
|
||||||
multiple={true}
|
|
||||||
placeholder="No module"
|
placeholder="No module"
|
||||||
disabled={disableSelect}
|
disabled={disableSelect}
|
||||||
className={`w-full h-full group`}
|
className="w-full h-full group"
|
||||||
buttonContainerClassName="w-full"
|
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"
|
buttonVariant="transparent-with-text"
|
||||||
hideIcon={false}
|
hideIcon
|
||||||
dropdownArrow={true}
|
dropdownArrow
|
||||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||||
showTooltip={true}
|
showTooltip
|
||||||
|
multiple
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isUpdating && <Spinner className="h-4 w-4" />}
|
{isUpdating && <Spinner className="h-4 w-4" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -78,7 +78,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
|||||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`}
|
href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-xs font-medium mt-0.5"
|
className="text-xs font-medium"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{parentIssueProjectDetails?.identifier}-{parentIssue.sequence_id}
|
{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}`}
|
href={`/${workspaceSlug}/projects/${projectDetails?.id}/issues/${currentIssue.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-xs font-medium mt-0.5"
|
className="text-xs font-medium"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{`${projectDetails?.identifier}-${currentIssue?.sequence_id}`}
|
{`${projectDetails?.identifier}-${currentIssue?.sequence_id}`}
|
||||||
|
@ -198,15 +198,15 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
try {
|
try {
|
||||||
await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds);
|
await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Module removed from issue successfully",
|
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Module removed from issue successfully",
|
title: "Successful!",
|
||||||
|
message: "Issue removed from module successfully.",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Module remove from issue failed",
|
|
||||||
type: "error",
|
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"}`}
|
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
|
||||||
hideIcon
|
hideIcon
|
||||||
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
||||||
showPlaceholderIcon
|
// TODO: add this logic
|
||||||
|
// showPlaceholderIcon
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -258,7 +259,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
|
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
|
||||||
hideIcon
|
hideIcon
|
||||||
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
||||||
showPlaceholderIcon
|
// TODO: add this logic
|
||||||
|
// showPlaceholderIcon
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -61,7 +61,8 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
|||||||
projectId?.toString(),
|
projectId?.toString(),
|
||||||
issueStore,
|
issueStore,
|
||||||
issueMap,
|
issueMap,
|
||||||
groupedIssueIds
|
groupedIssueIds,
|
||||||
|
viewId
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
|
@ -81,7 +81,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
projectId={issue.project_id}
|
projectId={issue.project_id}
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
buttonVariant="border-with-text"
|
buttonVariant="border-with-text"
|
||||||
tooltip
|
showTooltip
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</WithDisplayPropertiesHOC>
|
</WithDisplayPropertiesHOC>
|
||||||
@ -95,7 +95,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
buttonVariant="border-without-text"
|
buttonVariant="border-without-text"
|
||||||
buttonClassName="border"
|
buttonClassName="border"
|
||||||
tooltip
|
showTooltip
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</WithDisplayPropertiesHOC>
|
</WithDisplayPropertiesHOC>
|
||||||
@ -123,7 +123,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
placeholder="Start date"
|
placeholder="Start date"
|
||||||
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
|
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
tooltip
|
showTooltip
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</WithDisplayPropertiesHOC>
|
</WithDisplayPropertiesHOC>
|
||||||
@ -139,7 +139,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
placeholder="Due date"
|
placeholder="Due date"
|
||||||
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
|
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
tooltip
|
showTooltip
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</WithDisplayPropertiesHOC>
|
</WithDisplayPropertiesHOC>
|
||||||
@ -169,7 +169,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
projectId={issue.project_id}
|
projectId={issue.project_id}
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
buttonVariant="border-with-text"
|
buttonVariant="border-with-text"
|
||||||
tooltip
|
showTooltip
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</WithDisplayPropertiesHOC>
|
</WithDisplayPropertiesHOC>
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
CycleDropdown,
|
CycleDropdown,
|
||||||
DateDropdown,
|
DateDropdown,
|
||||||
EstimateDropdown,
|
EstimateDropdown,
|
||||||
ModuleSelectDropdown,
|
ModuleDropdown,
|
||||||
PriorityDropdown,
|
PriorityDropdown,
|
||||||
ProjectDropdown,
|
ProjectDropdown,
|
||||||
ProjectMemberDropdown,
|
ProjectMemberDropdown,
|
||||||
@ -267,6 +267,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||||||
handleFormChange();
|
handleFormChange();
|
||||||
}}
|
}}
|
||||||
buttonVariant="border-with-text"
|
buttonVariant="border-with-text"
|
||||||
|
// TODO: update tabIndex logic
|
||||||
tabIndex={19}
|
tabIndex={19}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -547,18 +548,17 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||||||
name="module_ids"
|
name="module_ids"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="h-7">
|
<div className="h-7">
|
||||||
<ModuleSelectDropdown
|
<ModuleDropdown
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
value={value || undefined}
|
value={value ?? []}
|
||||||
onChange={(moduleId) => {
|
onChange={(moduleIds) => {
|
||||||
onChange(moduleId);
|
onChange(moduleIds);
|
||||||
handleFormChange();
|
handleFormChange();
|
||||||
}}
|
}}
|
||||||
buttonVariant="border-with-text"
|
buttonVariant="border-with-text"
|
||||||
tabIndex={13}
|
tabIndex={13}
|
||||||
multiple={true}
|
multiple
|
||||||
showCount={true}
|
showCount
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -148,7 +148,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
|
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
|
||||||
hideIcon
|
hideIcon
|
||||||
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
||||||
showPlaceholderIcon
|
// TODO: add this logic
|
||||||
|
// showPlaceholderIcon
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -174,7 +175,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
|
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
|
||||||
hideIcon
|
hideIcon
|
||||||
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
||||||
showPlaceholderIcon
|
// TODO: add this logic
|
||||||
|
// showPlaceholderIcon
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -47,14 +47,34 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
|||||||
const filteredOptions =
|
const filteredOptions =
|
||||||
query === "" ? projectLabels : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
|
query === "" ? projectLabels : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
|
||||||
const openDropdown = () => {
|
const onOpen = () => {
|
||||||
setIsDropdownOpen(true);
|
|
||||||
if (!projectLabels && workspaceSlug && projectId) fetchProjectLabels(workspaceSlug.toString(), projectId);
|
if (!projectLabels && workspaceSlug && projectId) fetchProjectLabels(workspaceSlug.toString(), projectId);
|
||||||
if (referenceElement) referenceElement.focus();
|
if (referenceElement) referenceElement.focus();
|
||||||
};
|
};
|
||||||
const closeDropdown = () => setIsDropdownOpen(false);
|
|
||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isDropdownOpen);
|
const handleClose = () => {
|
||||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
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 (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
@ -62,7 +82,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
|||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(val) => onChange(val)}
|
onChange={dropdownOnChange}
|
||||||
className="relative flex-shrink-0 h-full"
|
className="relative flex-shrink-0 h-full"
|
||||||
multiple
|
multiple
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -73,7 +93,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
|||||||
type="button"
|
type="button"
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
className="h-full flex cursor-pointer items-center gap-2 text-xs text-custom-text-200"
|
className="h-full flex cursor-pointer items-center gap-2 text-xs text-custom-text-200"
|
||||||
onClick={openDropdown}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{label ? (
|
{label ? (
|
||||||
label
|
label
|
||||||
|
@ -147,10 +147,10 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="relative mt-6 h-44 w-full">
|
<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" />
|
<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 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="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">
|
<div className="grid h-7 w-7 place-items-center">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@ -6,6 +6,7 @@ import { useTheme } from "next-themes";
|
|||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react";
|
import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react";
|
||||||
|
import { usePopper } from "react-popper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useUser, useWorkspace } from "hooks/store";
|
import { useApplication, useUser, useWorkspace } from "hooks/store";
|
||||||
// hooks
|
// hooks
|
||||||
@ -14,7 +15,6 @@ import useToast from "hooks/use-toast";
|
|||||||
import { Avatar, Loader } from "@plane/ui";
|
import { Avatar, Loader } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IWorkspace } from "@plane/types";
|
import { IWorkspace } from "@plane/types";
|
||||||
|
|
||||||
// Static Data
|
// Static Data
|
||||||
const userLinks = (workspaceSlug: string, userId: string) => [
|
const userLinks = (workspaceSlug: string, userId: string) => [
|
||||||
{
|
{
|
||||||
@ -36,7 +36,6 @@ const userLinks = (workspaceSlug: string, userId: string) => [
|
|||||||
icon: Settings,
|
icon: Settings,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const profileLinks = (workspaceSlug: string, userId: string) => [
|
const profileLinks = (workspaceSlug: string, userId: string) => [
|
||||||
{
|
{
|
||||||
name: "View profile",
|
name: "View profile",
|
||||||
@ -49,7 +48,6 @@ const profileLinks = (workspaceSlug: string, userId: string) => [
|
|||||||
link: "/profile",
|
link: "/profile",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const WorkspaceSidebarDropdown = observer(() => {
|
export const WorkspaceSidebarDropdown = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -64,12 +62,25 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
|||||||
// hooks
|
// hooks
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
const { setTheme } = useTheme();
|
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) =>
|
const handleWorkspaceNavigation = (workspace: IWorkspace) =>
|
||||||
updateCurrentUser({
|
updateCurrentUser({
|
||||||
last_workspace_id: workspace?.id,
|
last_workspace_id: workspace?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
await signOut()
|
await signOut()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -85,16 +96,12 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleItemClick = () => {
|
const handleItemClick = () => {
|
||||||
console.log('CLICKED')
|
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
toggleSidebar();
|
toggleSidebar();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const workspacesList = Object.values(workspaces ?? {});
|
const workspacesList = Object.values(workspaces ?? {});
|
||||||
|
|
||||||
// TODO: fix workspaces list scroll
|
// TODO: fix workspaces list scroll
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-3 gap-y-2 px-4 pt-4">
|
<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) ?? "..."
|
activeWorkspace?.name?.charAt(0) ?? "..."
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!sidebarCollapsed && (
|
{!sidebarCollapsed && (
|
||||||
<h4 className="truncate text-base font-medium text-custom-text-100">
|
<h4 className="truncate text-base font-medium text-custom-text-100">
|
||||||
{activeWorkspace?.name ? activeWorkspace.name : "Loading..."}
|
{activeWorkspace?.name ? activeWorkspace.name : "Loading..."}
|
||||||
</h4>
|
</h4>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!sidebarCollapsed && (
|
{!sidebarCollapsed && (
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={`mx-1 hidden h-4 w-4 flex-shrink-0 group-hover/menu-button:block ${open ? "rotate-180" : ""
|
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>
|
</div>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="transition ease-out duration-100"
|
enter="transition ease-out duration-100"
|
||||||
@ -185,7 +189,6 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
|||||||
workspace?.name?.charAt(0) ?? "..."
|
workspace?.name?.charAt(0) ?? "..."
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<h5
|
<h5
|
||||||
className={`truncate text-sm font-medium ${workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
|
className={`truncate text-sm font-medium ${workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
|
||||||
}`}
|
}`}
|
||||||
@ -226,9 +229,14 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Link>
|
</Link>
|
||||||
{userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
|
{userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
|
||||||
<Link key={link.key} href={link.href} className="w-full" onClick={() => {
|
<Link
|
||||||
if (index > 0) handleItemClick();
|
key={link.key}
|
||||||
}}>
|
href={link.href}
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
if (index > 0) handleItemClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
as="div"
|
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"
|
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>
|
</Menu>
|
||||||
|
|
||||||
{!sidebarCollapsed && (
|
{!sidebarCollapsed && (
|
||||||
<Menu as="div" className="relative flex-shrink-0">
|
<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
|
<Avatar
|
||||||
name={currentUser?.display_name}
|
name={currentUser?.display_name}
|
||||||
src={currentUser?.avatar}
|
src={currentUser?.avatar}
|
||||||
@ -268,7 +275,6 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
|||||||
className="!text-base"
|
className="!text-base"
|
||||||
/>
|
/>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="transition ease-out duration-100"
|
enter="transition ease-out duration-100"
|
||||||
@ -281,11 +287,20 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
|||||||
<Menu.Items
|
<Menu.Items
|
||||||
className="absolute left-0 z-20 mt-1 flex w-52 origin-top-left flex-col divide-y
|
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"
|
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">
|
<div className="flex flex-col gap-2.5 pb-2">
|
||||||
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
|
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
|
||||||
{profileLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
|
{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">
|
<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">
|
<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]" />
|
<link.icon className="h-4 w-4 stroke-[1.5]" />
|
||||||
@ -323,4 +338,4 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
@ -1,23 +1,23 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
type TUseDropdownKeyDown = {
|
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(
|
const handleKeyDown = useCallback(
|
||||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (!isOpen) {
|
event.preventDefault();
|
||||||
onOpen();
|
onEnterKeyDown();
|
||||||
}
|
} else if (event.key === "Escape") {
|
||||||
} else if (event.key === "Escape" && isOpen) {
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onClose();
|
event.preventDefault();
|
||||||
|
onEscKeyDown();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isOpen, onOpen, onClose]
|
[onEnterKeyDown, onEscKeyDown]
|
||||||
);
|
);
|
||||||
|
|
||||||
return handleKeyDown;
|
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="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">
|
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8 sm:hidden hidden md:block lg:block">
|
||||||
<ProjectSettingsSidebar />
|
<ProjectSettingsSidebar />
|
||||||
</div>
|
</div>
|
||||||
{children}
|
<div className="w-full pl-10 sm:pl-10 md:pl-0 lg:pl-0">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -10,11 +10,13 @@ export const WorkspaceSettingLayout: FC<IWorkspaceSettingLayout> = (props) => {
|
|||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full gap-2 overflow-x-hidden overflow-y-scroll">
|
<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">
|
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8 sm:hidden hidden md:block lg:block">
|
||||||
<WorkspaceSettingsSidebar />
|
<WorkspaceSettingsSidebar />
|
||||||
</div>
|
</div>
|
||||||
{children}
|
<div className="w-full pl-10 sm:pl-10 md:pl-0 lg:pl-0">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -200,6 +200,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
|
|||||||
if (!cycleId) throw new Error("Cycle Id is required");
|
if (!cycleId) throw new Error("Cycle Id is required");
|
||||||
|
|
||||||
const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
|
const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||||
|
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId);
|
this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId);
|
||||||
@ -267,9 +268,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
|
|||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
update(this.issues, cycleId, (cycleIssueIds = []) => {
|
update(this.issues, cycleId, (cycleIssueIds = []) => uniq(concat(cycleIssueIds, issueIds)));
|
||||||
return uniq(concat(cycleIssueIds, issueIds));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
issueIds.forEach((issueId) => {
|
issueIds.forEach((issueId) => {
|
||||||
this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId });
|
this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId });
|
||||||
|
@ -205,6 +205,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
|||||||
if (!moduleId) throw new Error("Module Id is required");
|
if (!moduleId) throw new Error("Module Id is required");
|
||||||
|
|
||||||
const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
|
const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||||
|
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId);
|
this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId);
|
||||||
|
@ -38,6 +38,8 @@ export interface IIssueRootStore {
|
|||||||
members: string[] | undefined;
|
members: string[] | undefined;
|
||||||
projects: string[] | undefined;
|
projects: string[] | undefined;
|
||||||
|
|
||||||
|
rootStore: RootStore;
|
||||||
|
|
||||||
issues: IIssueStore;
|
issues: IIssueStore;
|
||||||
|
|
||||||
state: IStateStore;
|
state: IStateStore;
|
||||||
@ -87,6 +89,8 @@ export class IssueRootStore implements IIssueRootStore {
|
|||||||
members: string[] | undefined = undefined;
|
members: string[] | undefined = undefined;
|
||||||
projects: string[] | undefined = undefined;
|
projects: string[] | undefined = undefined;
|
||||||
|
|
||||||
|
rootStore: RootStore;
|
||||||
|
|
||||||
issues: IIssueStore;
|
issues: IIssueStore;
|
||||||
|
|
||||||
state: IStateStore;
|
state: IStateStore;
|
||||||
@ -136,6 +140,8 @@ export class IssueRootStore implements IIssueRootStore {
|
|||||||
projects: observable,
|
projects: observable,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.rootStore = rootStore;
|
||||||
|
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
if (rootStore.user.currentUser?.id) this.currentUserId = rootStore.user.currentUser?.id;
|
if (rootStore.user.currentUser?.id) this.currentUserId = rootStore.user.currentUser?.id;
|
||||||
if (rootStore.app.router.workspaceSlug) this.workspaceSlug = rootStore.app.router.workspaceSlug;
|
if (rootStore.app.router.workspaceSlug) this.workspaceSlug = rootStore.app.router.workspaceSlug;
|
||||||
|
Loading…
Reference in New Issue
Block a user