Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1b1ba35c1 | ||
|
|
d99529b109 | ||
|
|
6eb7014ea4 | ||
|
|
894de80f41 | ||
|
|
4b706437d7 | ||
|
|
e4bccea824 | ||
|
|
d39f2526a2 | ||
|
|
bc6e48fcd6 | ||
|
|
e6f33eb262 | ||
|
|
5cfebb8dae | ||
|
|
e4988ee940 | ||
|
|
850bf01d65 | ||
|
|
f183e389ea | ||
|
|
5d7c0a2a64 | ||
|
|
92e994990c | ||
|
|
d1087820f6 | ||
|
|
65024fe5ec | ||
|
|
62693abb09 | ||
|
|
9326fb0762 | ||
|
|
56805203f1 | ||
|
|
39136d3a9f | ||
|
|
34301e4399 | ||
|
|
51f795fbd7 | ||
|
|
7abfbac479 | ||
|
|
c4028efd71 | ||
|
|
0215b697a5 | ||
|
|
4566d6e80c | ||
|
|
e8d359e625 | ||
|
|
351eba8d61 | ||
|
|
1e27e37b51 | ||
|
|
7df2e9cf11 | ||
|
|
c6e3f1b932 |
@@ -7,7 +7,7 @@
|
||||
</p>
|
||||
|
||||
<h3 align="center"><b>Plane</b></h3>
|
||||
<p align="center"><b>Flexible, extensible open-source project management</b></p>
|
||||
<p align="center"><b>Open-source project management that unlocks customer value.</b></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.com/invite/A92xrEGCge">
|
||||
@@ -16,6 +16,13 @@
|
||||
<img alt="Commit activity per month" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="http://www.plane.so"><b>Website</b></a> •
|
||||
<a href="https://github.com/makeplane/plane/releases"><b>Releases</b></a> •
|
||||
<a href="https://twitter.com/planepowers"><b>Twitter</b></a> •
|
||||
<a href="https://docs.plane.so/"><b>Documentation</b></a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://app.plane.so/#gh-light-mode-only" target="_blank">
|
||||
<img
|
||||
@@ -33,56 +40,93 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind 🧘♀️.
|
||||
Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind. 🧘♀️
|
||||
|
||||
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
|
||||
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose).
|
||||
|
||||
## ⚡️ Contributors Quick Start
|
||||
|
||||
### Prerequisite
|
||||
## ⚡ Installation
|
||||
|
||||
Development system must have docker engine installed and running.
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account where we offer a hosted solution for users.
|
||||
|
||||
### Steps
|
||||
If you want more control over your data prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose).
|
||||
|
||||
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute
|
||||
| Installation Methods | Documentation Link |
|
||||
|-----------------|----------------------------------------------------------------------------------------------------------|
|
||||
| Docker | [](https://docs.plane.so/docker-compose) |
|
||||
| Kubernetes | [](https://docs.plane.so/kubernetes) |
|
||||
|
||||
1. Clone the code locally using `git clone https://github.com/makeplane/plane.git`
|
||||
1. Switch to the code folder `cd plane`
|
||||
1. Create your feature or fix branch you plan to work on using `git checkout -b <feature-branch-name>`
|
||||
1. Open terminal and run `./setup.sh`
|
||||
1. Open the code on VSCode or similar equivalent IDE
|
||||
1. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system
|
||||
1. Run the docker command to initiate various services `docker compose -f docker-compose-local.yml up -d`
|
||||
|
||||
You are ready to make changes to the code. Do not forget to refresh the browser (in case id does not auto-reload)
|
||||
|
||||
Thats it!
|
||||
|
||||
## 🍙 Self Hosting
|
||||
|
||||
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/docker-compose) documentation page
|
||||
`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking.
|
||||
- **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents.
|
||||
- **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you.
|
||||
- **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features.
|
||||
- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress.
|
||||
- **Issues**: Quickly create issues and add details using a powerful, rich text editor that supports file uploads. Add sub-properties and references to problems for better organization and tracking.
|
||||
|
||||
- **Cycles**
|
||||
Keep up your team's momentum with Cycles. Gain insights into your project's progress with burn-down charts and other valuable features.
|
||||
|
||||
- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to track and plan your project's progress easily.
|
||||
|
||||
- **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
|
||||
- **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues.
|
||||
- **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location.
|
||||
- **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration.
|
||||
|
||||
- **Pages**: Plane pages, equipped with AI and a rich text editor, let you jot down your thoughts on the fly. Format your text, upload images, hyperlink, or sync your existing ideas into an actionable item or issue.
|
||||
|
||||
- **Analytics**: Get insights into all your Plane data in real-time. Visualize issue data to spot trends, remove blockers, and progress your work.
|
||||
|
||||
- **Drive** (*coming soon*): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution.
|
||||
|
||||
|
||||
|
||||
## 🛠️ Contributors Quick Start
|
||||
|
||||
> Development system must have docker engine installed and running.
|
||||
|
||||
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute
|
||||
|
||||
1. Clone the code locally using:
|
||||
```
|
||||
git clone https://github.com/makeplane/plane.git
|
||||
```
|
||||
2. Switch to the code folder:
|
||||
```
|
||||
cd plane
|
||||
```
|
||||
3. Create your feature or fix branch you plan to work on using:
|
||||
```
|
||||
git checkout -b <feature-branch-name>
|
||||
```
|
||||
4. Open terminal and run:
|
||||
```
|
||||
./setup.sh
|
||||
```
|
||||
5. Open the code on VSCode or similar equivalent IDE.
|
||||
6. Review the `.env` files available in various folders.
|
||||
Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system.
|
||||
7. Run the docker command to initiate services:
|
||||
```
|
||||
docker compose -f docker-compose-local.yml up -d
|
||||
```
|
||||
|
||||
You are ready to make changes to the code. Do not forget to refresh the browser (in case it does not auto-reload).
|
||||
|
||||
Thats it!
|
||||
|
||||
## ❤️ Community
|
||||
|
||||
The Plane community can be found on [GitHub Discussions](https://github.com/orgs/makeplane/discussions), and our [Discord server](https://discord.com/invite/A92xrEGCge). Our [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community chanels.
|
||||
|
||||
Ask questions, report bugs, join discussions, voice ideas, make feature requests, or share your projects.
|
||||
|
||||
### Repo Activity
|
||||

|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_views_dark_mode.webp"
|
||||
src="https://ik.imagekit.io/w2okwbtu2/Issues_rNZjrGgFl.png?updatedAt=1709298765880"
|
||||
alt="Plane Views"
|
||||
width="100%"
|
||||
/>
|
||||
@@ -91,8 +135,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_issue_detail_dark_mode.webp"
|
||||
alt="Plane Issue Details"
|
||||
src="https://ik.imagekit.io/w2okwbtu2/Cycles_jCDhqmTl9.png?updatedAt=1709298780697"
|
||||
width="100%"
|
||||
/>
|
||||
</a>
|
||||
@@ -100,7 +143,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_cycles_modules_dark_mode.webp"
|
||||
src="https://ik.imagekit.io/w2okwbtu2/Modules_PSCVsbSfI.png?updatedAt=1709298796783"
|
||||
alt="Plane Cycles and Modules"
|
||||
width="100%"
|
||||
/>
|
||||
@@ -109,7 +152,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_analytics_dark_mode.webp"
|
||||
src="https://ik.imagekit.io/w2okwbtu2/Views_uxXsRatS4.png?updatedAt=1709298834522"
|
||||
alt="Plane Analytics"
|
||||
width="100%"
|
||||
/>
|
||||
@@ -118,7 +161,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_pages_dark_mode.webp"
|
||||
src="https://ik.imagekit.io/w2okwbtu2/Analytics_0o22gLRtp.png?updatedAt=1709298834389"
|
||||
alt="Plane Pages"
|
||||
width="100%"
|
||||
/>
|
||||
@@ -128,7 +171,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_commad_k_dark_mode.webp"
|
||||
src="https://ik.imagekit.io/w2okwbtu2/Drive_LlfeY4xn3.png?updatedAt=1709298837917"
|
||||
alt="Plane Command Menu"
|
||||
width="100%"
|
||||
/>
|
||||
@@ -136,20 +179,22 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.
|
||||
</p>
|
||||
</p>
|
||||
|
||||
## 📚Documentation
|
||||
|
||||
For full documentation, visit [docs.plane.so](https://docs.plane.so/)
|
||||
|
||||
To see how to Contribute, visit [here](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md).
|
||||
|
||||
## ❤️ Community
|
||||
|
||||
The Plane community can be found on GitHub Discussions, where you can ask questions, voice ideas, and share your projects.
|
||||
|
||||
To chat with other community members you can join the [Plane Discord](https://discord.com/invite/A92xrEGCge).
|
||||
|
||||
Our [Code of Conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community channels.
|
||||
|
||||
## ⛓️ Security
|
||||
|
||||
If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports. Email engineering@plane.so to disclose any security vulnerabilities.
|
||||
If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports.
|
||||
|
||||
Email squawk@plane.so to disclose any security vulnerabilities.
|
||||
|
||||
## ❤️ Contribute
|
||||
|
||||
There are many ways to contribute to Plane, including:
|
||||
- Submitting [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) and [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+) for various components.
|
||||
- Reviewing [the documentation](https://docs.plane.so/) and submitting [pull requests](https://github.com/makeplane/plane), from fixing typos to adding new features.
|
||||
- Speaking or writing about Plane or any other ecosystem integration and [letting us know](https://discord.com/invite/A92xrEGCge)!
|
||||
- Upvoting [popular feature requests](https://github.com/makeplane/plane/issues) to show your support.
|
||||
|
||||
### We couldn't have done this without you.
|
||||
|
||||
<a href="https://github.com/makeplane/plane/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=makeplane/plane" />
|
||||
</a>
|
||||
@@ -716,9 +716,9 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
# Validation check if the issue already exists
|
||||
if (
|
||||
str(request.data.get("external_id"))
|
||||
request.data.get("external_id")
|
||||
and (issue_comment.external_id != str(request.data.get("external_id")))
|
||||
and Issue.objects.filter(
|
||||
and IssueComment.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
|
||||
@@ -58,6 +58,7 @@ def dashboard_overview_stats(self, request, slug):
|
||||
|
||||
pending_issues_count = Issue.issue_objects.filter(
|
||||
~Q(state__group__in=["completed", "cancelled"]),
|
||||
target_date__lt=timezone.now().date(),
|
||||
project__project_projectmember__is_active=True,
|
||||
project__project_projectmember__member=request.user,
|
||||
workspace__slug=slug,
|
||||
|
||||
@@ -40,9 +40,11 @@ export const LinkEditView = ({
|
||||
const [positionRef, setPositionRef] = useState({ from: from, to: to });
|
||||
const [localUrl, setLocalUrl] = useState(viewProps.url);
|
||||
|
||||
const linkRemoved = useRef<Boolean>();
|
||||
const linkRemoved = useRef<boolean>();
|
||||
|
||||
const getText = (from: number, to: number) => {
|
||||
if (to >= editor.state.doc.content.size) return "";
|
||||
|
||||
const text = editor.state.doc.textBetween(from, to, "\n");
|
||||
return text;
|
||||
};
|
||||
@@ -72,10 +74,12 @@ export const LinkEditView = ({
|
||||
|
||||
const url = isValidUrl(localUrl) ? localUrl : viewProps.url;
|
||||
|
||||
if (to >= editor.state.doc.content.size) return;
|
||||
|
||||
editor.view.dispatch(editor.state.tr.removeMark(from, to, editor.schema.marks.link));
|
||||
editor.view.dispatch(editor.state.tr.addMark(from, to, editor.schema.marks.link.create({ href: url })));
|
||||
},
|
||||
[localUrl]
|
||||
[localUrl, editor, from, to, viewProps.url]
|
||||
);
|
||||
|
||||
const handleUpdateText = (text: string) => {
|
||||
|
||||
Vendored
+2
@@ -203,6 +203,8 @@ export interface ViewFlags {
|
||||
|
||||
export type GroupByColumnTypes =
|
||||
| "project"
|
||||
| "cycle"
|
||||
| "module"
|
||||
| "state"
|
||||
| "state_detail.group"
|
||||
| "priority"
|
||||
|
||||
Vendored
+6
@@ -14,6 +14,8 @@ export type TIssueGroupByOptions =
|
||||
| "project"
|
||||
| "assignees"
|
||||
| "mentions"
|
||||
| "cycle"
|
||||
| "module"
|
||||
| null;
|
||||
|
||||
export type TIssueOrderByOptions =
|
||||
@@ -60,6 +62,8 @@ export type TIssueParams =
|
||||
| "created_by"
|
||||
| "subscriber"
|
||||
| "labels"
|
||||
| "cycle"
|
||||
| "module"
|
||||
| "start_date"
|
||||
| "target_date"
|
||||
| "project"
|
||||
@@ -79,6 +83,8 @@ export interface IIssueFilterOptions {
|
||||
labels?: string[] | null;
|
||||
priority?: string[] | null;
|
||||
project?: string[] | null;
|
||||
cycle?: string[] | null;
|
||||
module?: string[] | null;
|
||||
start_date?: string[] | null;
|
||||
state?: string[] | null;
|
||||
state_group?: string[] | null;
|
||||
|
||||
@@ -27,6 +27,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
noBorder = false,
|
||||
noChevron = false,
|
||||
optionsClassName = "",
|
||||
menuItemsClassName = "",
|
||||
verticalEllipsis = false,
|
||||
portalElement,
|
||||
menuButtonOnClick,
|
||||
@@ -70,7 +71,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
let menuItems = (
|
||||
<Menu.Items className="fixed z-10" static>
|
||||
<Menu.Items className={cn("fixed z-10", menuItemsClassName)} static>
|
||||
<div
|
||||
className={cn(
|
||||
"my-1 overflow-y-scroll rounded-md 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 min-w-[12rem] whitespace-nowrap",
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface ICustomMenuDropdownProps extends IDropdownProps {
|
||||
noBorder?: boolean;
|
||||
verticalEllipsis?: boolean;
|
||||
menuButtonOnClick?: (...args: any) => void;
|
||||
menuItemsClassName?: string;
|
||||
onMenuClose?: () => void;
|
||||
closeOnSelect?: boolean;
|
||||
portalElement?: Element | null;
|
||||
|
||||
@@ -5,17 +5,17 @@ import { observer } from "mobx-react";
|
||||
|
||||
type Props = {
|
||||
onClick?: () => void;
|
||||
}
|
||||
};
|
||||
|
||||
export const SidebarHamburgerToggle: FC<Props> = observer((props) => {
|
||||
const { onClick } = props
|
||||
const { onClick } = props;
|
||||
const { theme: themeStore } = useApplication();
|
||||
return (
|
||||
<div
|
||||
className="w-7 h-7 flex-shrink-0 rounded flex justify-center items-center bg-custom-background-80 transition-all hover:bg-custom-background-90 cursor-pointer group md:hidden"
|
||||
onClick={() => {
|
||||
if (onClick) onClick()
|
||||
else themeStore.toggleMobileSidebar()
|
||||
if (onClick) onClick();
|
||||
else themeStore.toggleSidebar();
|
||||
}}
|
||||
>
|
||||
<Menu size={14} className="text-custom-text-200 group-hover:text-custom-text-100 transition-all" />
|
||||
|
||||
@@ -152,6 +152,7 @@ export const CycleMobileHeader = () => {
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
ignoreGroupedFilters={["cycle"]}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ export const OverviewStatsWidget: React.FC<WidgetProps> = observer((props) => {
|
||||
key: "overdue",
|
||||
title: "Issues overdue",
|
||||
count: widgetStats?.pending_issues_count,
|
||||
link: `/${workspaceSlug}/workspace-views/assigned/?target_date=${today};before`,
|
||||
link: `/${workspaceSlug}/workspace-views/assigned/?state_group=backlog,unstarted,started&target_date=${today};before`,
|
||||
},
|
||||
{
|
||||
key: "created",
|
||||
|
||||
@@ -67,6 +67,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
if (isOpen) onClose && onClose();
|
||||
};
|
||||
|
||||
const dropdownOnChange = (val: string | null) => {
|
||||
|
||||
@@ -67,6 +67,7 @@ export const MemberDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
if (isOpen) onClose && onClose();
|
||||
};
|
||||
|
||||
const dropdownOnChange = (val: string & string[]) => {
|
||||
|
||||
@@ -58,7 +58,7 @@ const BorderButton = (props: ButtonProps) => {
|
||||
high: "bg-orange-500/20 text-orange-950 border-orange-500",
|
||||
medium: "bg-yellow-500/20 text-yellow-950 border-yellow-500",
|
||||
low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100",
|
||||
none: "bg-custom-background-80 border-custom-border-300",
|
||||
none: "hover:bg-custom-background-80 border-custom-border-300",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -197,7 +197,7 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
high: "text-orange-950",
|
||||
medium: "text-yellow-950",
|
||||
low: "text-blue-950",
|
||||
none: "",
|
||||
none: "hover:text-custom-text-300",
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -53,7 +53,7 @@ const EmojiIconPicker: React.FC<Props> = (props) => {
|
||||
<Popover.Button
|
||||
ref={buttonRef}
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
className="outline-none"
|
||||
className="outline-none flex items-center justify-center"
|
||||
disabled={disabled}
|
||||
>
|
||||
{label}
|
||||
|
||||
@@ -244,6 +244,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
ignoreGroupedFilters={["cycle"]}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
|
||||
|
||||
@@ -248,6 +248,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
ignoreGroupedFilters={["module"]}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
||||
@@ -131,6 +131,8 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
const handleInboxIssueNavigation = useCallback(
|
||||
(direction: "next" | "prev") => {
|
||||
if (!inboxIssues || !inboxIssueId) return;
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement && (activeElement.classList.contains("tiptap") || activeElement.id === "title-input")) return;
|
||||
const nextIssueIndex =
|
||||
direction === "next"
|
||||
? (currentIssueIndex + 1) % inboxIssues.length
|
||||
|
||||
@@ -50,7 +50,7 @@ export const IssueCycleSelect: React.FC<TIssueCycleSelect> = observer((props) =>
|
||||
buttonVariant="transparent-with-text"
|
||||
className="w-full group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm ${issue?.cycle_id ? "" : "text-custom-text-400"}`}
|
||||
buttonClassName={`text-sm justify-between ${issue?.cycle_id ? "" : "text-custom-text-400"}`}
|
||||
placeholder="No cycle"
|
||||
hideIcon
|
||||
dropdownArrow
|
||||
|
||||
@@ -74,7 +74,7 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
||||
return (
|
||||
<div
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey && !isEmpty) {
|
||||
if (e.key === "Enter" && !e.shiftKey && !isEmpty && !isSubmitting) {
|
||||
handleSubmit(onSubmit)(e);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
XCircle,
|
||||
CircleDot,
|
||||
CopyPlus,
|
||||
CalendarDays,
|
||||
CalendarClock,
|
||||
CalendarCheck2,
|
||||
} from "lucide-react";
|
||||
// hooks
|
||||
import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "hooks/store";
|
||||
@@ -236,7 +237,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarDays className="h-4 w-4 flex-shrink-0" />
|
||||
<CalendarClock className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Start date</span>
|
||||
</div>
|
||||
<DateDropdown
|
||||
@@ -262,7 +263,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarDays className="h-4 w-4 flex-shrink-0" />
|
||||
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Due date</span>
|
||||
</div>
|
||||
<DateDropdown
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { X } from "lucide-react";
|
||||
// hooks
|
||||
import { useCycle } from "hooks/store";
|
||||
// ui
|
||||
import { CycleGroupIcon } from "@plane/ui";
|
||||
// types
|
||||
import { TCycleGroups } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
values: string[];
|
||||
editable: boolean | undefined;
|
||||
};
|
||||
|
||||
export const AppliedCycleFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, values, editable } = props;
|
||||
// store hooks
|
||||
const { getCycleById } = useCycle();
|
||||
|
||||
return (
|
||||
<>
|
||||
{values.map((cycleId) => {
|
||||
const cycleDetails = getCycleById(cycleId) ?? null;
|
||||
|
||||
if (!cycleDetails) return null;
|
||||
|
||||
const cycleStatus = (cycleDetails?.status ? cycleDetails?.status.toLocaleLowerCase() : "draft") as TCycleGroups;
|
||||
|
||||
return (
|
||||
<div key={cycleId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="normal-case">{cycleDetails.name}</span>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(cycleId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,12 +1,15 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { X } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import { useApplication, useUser } from "hooks/store";
|
||||
// components
|
||||
import {
|
||||
AppliedCycleFilters,
|
||||
AppliedDateFilters,
|
||||
AppliedLabelsFilters,
|
||||
AppliedMembersFilters,
|
||||
AppliedModuleFilters,
|
||||
AppliedPriorityFilters,
|
||||
AppliedProjectFilters,
|
||||
AppliedStateFilters,
|
||||
@@ -34,6 +37,9 @@ const dateFilters = ["start_date", "target_date"];
|
||||
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, states, alwaysAllowEditing } = props;
|
||||
// store hooks
|
||||
const {
|
||||
router: { moduleId, cycleId },
|
||||
} = useApplication();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
@@ -104,6 +110,20 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{filterKey === "cycle" && !cycleId && (
|
||||
<AppliedCycleFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("cycle", val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{filterKey === "module" && !moduleId && (
|
||||
<AppliedModuleFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("module", val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -5,5 +5,7 @@ export * from "./label";
|
||||
export * from "./members";
|
||||
export * from "./priority";
|
||||
export * from "./project";
|
||||
export * from "./module";
|
||||
export * from "./cycle";
|
||||
export * from "./state";
|
||||
export * from "./state-group";
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { X } from "lucide-react";
|
||||
// hooks
|
||||
import { useModule } from "hooks/store";
|
||||
// ui
|
||||
import { DiceIcon } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
values: string[];
|
||||
editable: boolean | undefined;
|
||||
};
|
||||
|
||||
export const AppliedModuleFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, values, editable } = props;
|
||||
// store hooks
|
||||
const { getModuleById } = useModule();
|
||||
|
||||
return (
|
||||
<>
|
||||
{values.map((moduleId) => {
|
||||
const moduleDetails = getModuleById(moduleId) ?? null;
|
||||
|
||||
if (!moduleDetails) return null;
|
||||
|
||||
return (
|
||||
<div key={moduleId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||
<DiceIcon className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="normal-case">{moduleDetails.name}</span>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(moduleId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
+5
-1
@@ -11,7 +11,7 @@ import {
|
||||
FilterSubGroupBy,
|
||||
} from "components/issues";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueGroupByOptions } from "@plane/types";
|
||||
import { ILayoutDisplayFiltersOptions } from "constants/issue";
|
||||
|
||||
type Props = {
|
||||
@@ -20,6 +20,7 @@ type Props = {
|
||||
handleDisplayFiltersUpdate: (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => void;
|
||||
handleDisplayPropertiesUpdate: (updatedDisplayProperties: Partial<IIssueDisplayProperties>) => void;
|
||||
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined;
|
||||
ignoreGroupedFilters?: Partial<TIssueGroupByOptions>[];
|
||||
};
|
||||
|
||||
export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
|
||||
@@ -29,6 +30,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
|
||||
handleDisplayFiltersUpdate,
|
||||
handleDisplayPropertiesUpdate,
|
||||
layoutDisplayFiltersOptions,
|
||||
ignoreGroupedFilters = [],
|
||||
} = props;
|
||||
|
||||
const isDisplayFilterEnabled = (displayFilter: keyof IIssueDisplayFilterOptions) =>
|
||||
@@ -54,6 +56,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
|
||||
group_by: val,
|
||||
})
|
||||
}
|
||||
ignoreGroupedFilters={ignoreGroupedFilters}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -71,6 +74,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
|
||||
})
|
||||
}
|
||||
subGroupByOptions={layoutDisplayFiltersOptions?.display_filters.sub_group_by ?? []}
|
||||
ignoreGroupedFilters={ignoreGroupedFilters}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "components/issues";
|
||||
// types
|
||||
@@ -12,10 +11,11 @@ type Props = {
|
||||
displayFilters: IIssueDisplayFilterOptions;
|
||||
groupByOptions: TIssueGroupByOptions[];
|
||||
handleUpdate: (val: TIssueGroupByOptions) => void;
|
||||
ignoreGroupedFilters: Partial<TIssueGroupByOptions>[];
|
||||
};
|
||||
|
||||
export const FilterGroupBy: React.FC<Props> = observer((props) => {
|
||||
const { displayFilters, groupByOptions, handleUpdate } = props;
|
||||
const { displayFilters, groupByOptions, handleUpdate, ignoreGroupedFilters } = props;
|
||||
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
|
||||
@@ -34,6 +34,7 @@ export const FilterGroupBy: React.FC<Props> = observer((props) => {
|
||||
{ISSUE_GROUP_BY_OPTIONS.filter((option) => groupByOptions.includes(option.key)).map((groupBy) => {
|
||||
if (displayFilters.layout === "kanban" && selectedSubGroupBy !== null && groupBy.key === selectedSubGroupBy)
|
||||
return null;
|
||||
if (ignoreGroupedFilters.includes(groupBy?.key)) return null;
|
||||
|
||||
return (
|
||||
<FilterOption
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "components/issues";
|
||||
// types
|
||||
@@ -12,10 +11,11 @@ type Props = {
|
||||
displayFilters: IIssueDisplayFilterOptions;
|
||||
handleUpdate: (val: TIssueGroupByOptions) => void;
|
||||
subGroupByOptions: TIssueGroupByOptions[];
|
||||
ignoreGroupedFilters: Partial<TIssueGroupByOptions>[];
|
||||
};
|
||||
|
||||
export const FilterSubGroupBy: React.FC<Props> = observer((props) => {
|
||||
const { displayFilters, handleUpdate, subGroupByOptions } = props;
|
||||
const { displayFilters, handleUpdate, subGroupByOptions, ignoreGroupedFilters } = props;
|
||||
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
|
||||
@@ -33,6 +33,7 @@ export const FilterSubGroupBy: React.FC<Props> = observer((props) => {
|
||||
<div>
|
||||
{ISSUE_GROUP_BY_OPTIONS.filter((option) => subGroupByOptions.includes(option.key)).map((subGroupBy) => {
|
||||
if (selectedGroupBy !== null && subGroupBy.key === selectedGroupBy) return null;
|
||||
if (ignoreGroupedFilters.includes(subGroupBy?.key)) return null;
|
||||
|
||||
return (
|
||||
<FilterOption
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import sortBy from "lodash/sortBy";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "components/issues";
|
||||
import { useApplication, useCycle } from "hooks/store";
|
||||
// ui
|
||||
import { Loader, CycleGroupIcon } from "@plane/ui";
|
||||
// types
|
||||
import { TCycleGroups } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (val: string) => void;
|
||||
searchQuery: string;
|
||||
};
|
||||
|
||||
export const FilterCycle: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||
|
||||
// hooks
|
||||
const {
|
||||
router: { projectId },
|
||||
} = useApplication();
|
||||
const { getCycleById, getProjectCycleIds } = useCycle();
|
||||
|
||||
// states
|
||||
const [itemsToRender, setItemsToRender] = useState(5);
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
|
||||
const cycleIds = projectId ? getProjectCycleIds(projectId) : undefined;
|
||||
const cycles = cycleIds?.map((projectId) => getCycleById(projectId)!) ?? null;
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
const filteredOptions = sortBy(
|
||||
cycles?.filter((cycle) => cycle.name.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||
(cycle) => cycle.name.toLowerCase()
|
||||
);
|
||||
|
||||
const handleViewToggle = () => {
|
||||
if (!filteredOptions) return;
|
||||
|
||||
if (itemsToRender === filteredOptions.length) setItemsToRender(5);
|
||||
else setItemsToRender(filteredOptions.length);
|
||||
};
|
||||
|
||||
const cycleStatus = (status: TCycleGroups) => (status ? status.toLocaleLowerCase() : "draft") as TCycleGroups;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterHeader
|
||||
title={`Cycle ${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||
isPreviewEnabled={previewEnabled}
|
||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||
/>
|
||||
{previewEnabled && (
|
||||
<div>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
<>
|
||||
{filteredOptions.slice(0, itemsToRender).map((cycle) => (
|
||||
<FilterOption
|
||||
key={cycle.id}
|
||||
isChecked={appliedFilters?.includes(cycle.id) ? true : false}
|
||||
onClick={() => handleUpdate(cycle.id)}
|
||||
icon={
|
||||
<CycleGroupIcon cycleGroup={cycleStatus(cycle?.status)} className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
}
|
||||
title={cycle.name}
|
||||
activePulse={cycleStatus(cycle?.status) === "current" ? true : false}
|
||||
/>
|
||||
))}
|
||||
{filteredOptions.length > 5 && (
|
||||
<button
|
||||
type="button"
|
||||
className="ml-8 text-xs font-medium text-custom-primary-100"
|
||||
onClick={handleViewToggle}
|
||||
>
|
||||
{itemsToRender === filteredOptions.length ? "View less" : "View all"}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-2">
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Search, X } from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication } from "hooks/store";
|
||||
// components
|
||||
import {
|
||||
FilterAssignees,
|
||||
@@ -13,6 +15,8 @@ import {
|
||||
FilterState,
|
||||
FilterStateGroup,
|
||||
FilterTargetDate,
|
||||
FilterCycle,
|
||||
FilterModule,
|
||||
} from "components/issues";
|
||||
// types
|
||||
import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types";
|
||||
@@ -30,6 +34,10 @@ type Props = {
|
||||
|
||||
export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, memberIds, states } = props;
|
||||
// hooks
|
||||
const {
|
||||
router: { moduleId, cycleId },
|
||||
} = useApplication();
|
||||
// states
|
||||
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
|
||||
|
||||
@@ -102,6 +110,28 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* cycle */}
|
||||
{isFilterEnabled("cycle") && !cycleId && (
|
||||
<div className="py-2">
|
||||
<FilterCycle
|
||||
appliedFilters={filters.cycle ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("cycle", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* module */}
|
||||
{isFilterEnabled("module") && !moduleId && (
|
||||
<div className="py-2">
|
||||
<FilterModule
|
||||
appliedFilters={filters.module ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("module", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* assignees */}
|
||||
{isFilterEnabled("mentions") && (
|
||||
<div className="py-2">
|
||||
|
||||
@@ -8,4 +8,6 @@ export * from "./project";
|
||||
export * from "./start-date";
|
||||
export * from "./state-group";
|
||||
export * from "./state";
|
||||
export * from "./cycle";
|
||||
export * from "./module";
|
||||
export * from "./target-date";
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import sortBy from "lodash/sortBy";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "components/issues";
|
||||
import { useApplication, useModule } from "hooks/store";
|
||||
// ui
|
||||
import { Loader, DiceIcon } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (val: string) => void;
|
||||
searchQuery: string;
|
||||
};
|
||||
|
||||
export const FilterModule: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||
|
||||
// hooks
|
||||
const {
|
||||
router: { projectId },
|
||||
} = useApplication();
|
||||
const { getModuleById, getProjectModuleIds } = useModule();
|
||||
|
||||
// states
|
||||
const [itemsToRender, setItemsToRender] = useState(5);
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
|
||||
const moduleIds = projectId ? getProjectModuleIds(projectId) : undefined;
|
||||
const modules = moduleIds?.map((projectId) => getModuleById(projectId)!) ?? null;
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
const filteredOptions = sortBy(
|
||||
modules?.filter((module) => module.name.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||
(module) => module.name.toLowerCase()
|
||||
);
|
||||
|
||||
const handleViewToggle = () => {
|
||||
if (!filteredOptions) return;
|
||||
|
||||
if (itemsToRender === filteredOptions.length) setItemsToRender(5);
|
||||
else setItemsToRender(filteredOptions.length);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterHeader
|
||||
title={`Module ${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||
isPreviewEnabled={previewEnabled}
|
||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||
/>
|
||||
{previewEnabled && (
|
||||
<div>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
<>
|
||||
{filteredOptions.slice(0, itemsToRender).map((cycle) => (
|
||||
<FilterOption
|
||||
key={cycle.id}
|
||||
isChecked={appliedFilters?.includes(cycle.id) ? true : false}
|
||||
onClick={() => handleUpdate(cycle.id)}
|
||||
icon={<DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
title={cycle.name}
|
||||
/>
|
||||
))}
|
||||
{filteredOptions.length > 5 && (
|
||||
<button
|
||||
type="button"
|
||||
className="ml-8 text-xs font-medium text-custom-primary-100"
|
||||
onClick={handleViewToggle}
|
||||
>
|
||||
{itemsToRender === filteredOptions.length ? "View less" : "View all"}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-2">
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -8,10 +8,11 @@ type Props = {
|
||||
title: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
multiple?: boolean;
|
||||
activePulse?: boolean;
|
||||
};
|
||||
|
||||
export const FilterOption: React.FC<Props> = (props) => {
|
||||
const { icon, isChecked, multiple = true, onClick, title } = props;
|
||||
const { icon, isChecked, multiple = true, onClick, title, activePulse = false } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -30,6 +31,9 @@ export const FilterOption: React.FC<Props> = (props) => {
|
||||
{icon && <div className="grid w-5 flex-shrink-0 place-items-center">{icon}</div>}
|
||||
<div className="flex-grow truncate text-xs text-custom-text-200">{title}</div>
|
||||
</div>
|
||||
{activePulse && (
|
||||
<div className="flex-shrink-0 text-xs w-2 h-2 rounded-full bg-custom-primary-100 animate-pulse ml-auto" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -291,27 +291,29 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
</Droppable>
|
||||
</div>
|
||||
|
||||
<KanBanView
|
||||
issuesMap={issueMap}
|
||||
issueIds={issueIds}
|
||||
displayProperties={displayProperties}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={renderQuickActions}
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
kanbanFilters={kanbanFilters}
|
||||
enableQuickIssueCreate={enableQuickAdd}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true}
|
||||
quickAddCallback={issues?.quickAddIssue}
|
||||
viewId={viewId}
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
|
||||
canEditProperties={canEditProperties}
|
||||
storeType={storeType}
|
||||
addIssuesToView={addIssuesToView}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
isDragStarted={isDragStarted}
|
||||
/>
|
||||
<div className="w-max h-max">
|
||||
<KanBanView
|
||||
issuesMap={issueMap}
|
||||
issueIds={issueIds}
|
||||
displayProperties={displayProperties}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={renderQuickActions}
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
kanbanFilters={kanbanFilters}
|
||||
enableQuickIssueCreate={enableQuickAdd}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true}
|
||||
quickAddCallback={issues?.quickAddIssue}
|
||||
viewId={viewId}
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
|
||||
canEditProperties={canEditProperties}
|
||||
storeType={storeType}
|
||||
addIssuesToView={addIssuesToView}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
isDragStarted={isDragStarted}
|
||||
/>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +141,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
||||
"rounded border-[0.5px] w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
||||
{ "hover:cursor-grab": !isDragDisabled },
|
||||
{ "border-custom-primary-100": snapshot.isDragging },
|
||||
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id }
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useIssueDetail, useKanbanView, useLabel, useMember, useProject, useProjectState } from "hooks/store";
|
||||
import {
|
||||
useCycle,
|
||||
useIssueDetail,
|
||||
useKanbanView,
|
||||
useLabel,
|
||||
useMember,
|
||||
useModule,
|
||||
useProject,
|
||||
useProjectState,
|
||||
} from "hooks/store";
|
||||
// components
|
||||
import { HeaderGroupByCard } from "./headers/group-by-card";
|
||||
import { KanbanGroup } from "./kanban-group";
|
||||
@@ -79,31 +88,40 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
const member = useMember();
|
||||
const project = useProject();
|
||||
const label = useLabel();
|
||||
const cycle = useCycle();
|
||||
const _module = useModule();
|
||||
const projectState = useProjectState();
|
||||
const { peekIssue } = useIssueDetail();
|
||||
|
||||
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member);
|
||||
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, cycle, _module, label, projectState, member);
|
||||
|
||||
if (!list) return null;
|
||||
|
||||
const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)[_list.id]?.length > 0);
|
||||
const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0);
|
||||
|
||||
const groupList = showEmptyGroup ? list : groupWithIssues;
|
||||
|
||||
const visibilityGroupBy = (_list: IGroupByColumn) =>
|
||||
sub_group_by ? false : kanbanFilters?.group_by.includes(_list.id) ? true : false;
|
||||
const visibilityGroupBy = (_list: IGroupByColumn) => {
|
||||
if (sub_group_by) {
|
||||
if (kanbanFilters?.sub_group_by.includes(_list.id)) return true;
|
||||
return false;
|
||||
} else {
|
||||
if (kanbanFilters?.group_by.includes(_list.id)) return true;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isGroupByCreatedBy = group_by === "created_by";
|
||||
|
||||
return (
|
||||
<div className={`relative w-full flex gap-3 ${sub_group_by ? "h-full" : "h-full"}`}>
|
||||
<div className={`relative w-full flex gap-2 ${sub_group_by ? "h-full" : "h-full"}`}>
|
||||
{groupList &&
|
||||
groupList.length > 0 &&
|
||||
groupList.map((_list: IGroupByColumn) => {
|
||||
const groupByVisibilityToggle = visibilityGroupBy(_list);
|
||||
|
||||
return (
|
||||
<div className={`relative flex flex-shrink-0 flex-col group ${groupByVisibilityToggle ? `` : `w-[340px]`}`}>
|
||||
<div className={`relative flex flex-shrink-0 flex-col group ${groupByVisibilityToggle ? `` : `w-[350px]`}`}>
|
||||
{sub_group_by === null && (
|
||||
<div className="flex-shrink-0 sticky top-0 z-[2] w-full bg-custom-background-90 py-1">
|
||||
<HeaderGroupByCard
|
||||
|
||||
@@ -106,13 +106,21 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
{icon ? icon : <Circle width={14} strokeWidth={2} />}
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center gap-1 ${verticalAlignPosition ? `flex-col` : `w-full flex-row`}`}>
|
||||
<div
|
||||
className={`relative overflow-hidden flex items-center gap-1 ${
|
||||
verticalAlignPosition ? `flex-col` : `w-full flex-row`
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`line-clamp-1 font-medium text-custom-text-100 ${verticalAlignPosition ? `vertical-lr` : ``}`}
|
||||
className={`inline-block truncate line-clamp-1 font-medium text-custom-text-100 overflow-hidden ${
|
||||
verticalAlignPosition ? `vertical-lr max-h-[400px]` : ``
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div className={`text-sm font-medium text-custom-text-300 ${verticalAlignPosition ? `` : `pl-2`}`}>
|
||||
<div
|
||||
className={`flex-shrink-0 text-sm font-medium text-custom-text-300 ${verticalAlignPosition ? `` : `pl-2`}`}
|
||||
>
|
||||
{count || 0}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -80,6 +80,10 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
preloadedData = { ...preloadedData, state_id: groupValue };
|
||||
} else if (groupByKey === "priority") {
|
||||
preloadedData = { ...preloadedData, priority: groupValue };
|
||||
} else if (groupByKey === "cycle") {
|
||||
preloadedData = { ...preloadedData, cycle_id: groupValue };
|
||||
} else if (groupByKey === "module") {
|
||||
preloadedData = { ...preloadedData, module_ids: [groupValue] };
|
||||
} else if (groupByKey === "labels" && groupValue != "None") {
|
||||
preloadedData = { ...preloadedData, label_ids: [groupValue] };
|
||||
} else if (groupByKey === "assignees" && groupValue != "None") {
|
||||
@@ -96,6 +100,10 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
preloadedData = { ...preloadedData, state_id: subGroupValue };
|
||||
} else if (subGroupByKey === "priority") {
|
||||
preloadedData = { ...preloadedData, priority: subGroupValue };
|
||||
} else if (groupByKey === "cycle") {
|
||||
preloadedData = { ...preloadedData, cycle_id: subGroupValue };
|
||||
} else if (groupByKey === "module") {
|
||||
preloadedData = { ...preloadedData, module_ids: [subGroupValue] };
|
||||
} else if (subGroupByKey === "labels" && subGroupValue != "None") {
|
||||
preloadedData = { ...preloadedData, label_ids: [subGroupValue] };
|
||||
} else if (subGroupByKey === "assignees" && subGroupValue != "None") {
|
||||
@@ -115,9 +123,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
<Droppable droppableId={`${groupId}__${sub_group_id}`}>
|
||||
{(provided: any, snapshot: any) => (
|
||||
<div
|
||||
className={`relative h-full w-full transition-all ${
|
||||
snapshot.isDraggingOver ? `bg-custom-background-80` : ``
|
||||
}`}
|
||||
className={`relative h-full transition-all ${snapshot.isDraggingOver ? `bg-custom-background-80` : ``}`}
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "@plane/types";
|
||||
// constants
|
||||
import { EIssueActions } from "../types";
|
||||
import { useLabel, useMember, useProject, useProjectState } from "hooks/store";
|
||||
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store";
|
||||
import { getGroupByColumns } from "../utils";
|
||||
import { TCreateModalStoreTypes } from "constants/issue";
|
||||
|
||||
@@ -30,6 +30,15 @@ interface ISubGroupSwimlaneHeader {
|
||||
kanbanFilters: TIssueKanbanFilters;
|
||||
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
||||
}
|
||||
|
||||
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
|
||||
let headerCount = 0;
|
||||
Object.keys(issueIds).map((groupState) => {
|
||||
headerCount = headerCount + (issueIds?.[groupState]?.[groupById]?.length || 0);
|
||||
});
|
||||
return headerCount;
|
||||
};
|
||||
|
||||
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||
issueIds,
|
||||
sub_group_by,
|
||||
@@ -38,18 +47,18 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||
kanbanFilters,
|
||||
handleKanbanFilters,
|
||||
}) => (
|
||||
<div className="relative flex h-max min-h-full w-full items-center">
|
||||
<div className="relative flex gap-2 h-max min-h-full w-full items-center">
|
||||
{list &&
|
||||
list.length > 0 &&
|
||||
list.map((_list: IGroupByColumn) => (
|
||||
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[340px] flex-shrink-0 flex-col">
|
||||
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
|
||||
<HeaderGroupByCard
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
column_id={_list.id}
|
||||
icon={_list.icon}
|
||||
title={_list.name}
|
||||
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
|
||||
count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)}
|
||||
kanbanFilters={kanbanFilters}
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
issuePayload={_list.payload}
|
||||
@@ -217,10 +226,28 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
const member = useMember();
|
||||
const project = useProject();
|
||||
const label = useLabel();
|
||||
const cycle = useCycle();
|
||||
const _module = useModule();
|
||||
const projectState = useProjectState();
|
||||
|
||||
const groupByList = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member);
|
||||
const subGroupByList = getGroupByColumns(sub_group_by as GroupByColumnTypes, project, label, projectState, member);
|
||||
const groupByList = getGroupByColumns(
|
||||
group_by as GroupByColumnTypes,
|
||||
project,
|
||||
cycle,
|
||||
_module,
|
||||
label,
|
||||
projectState,
|
||||
member
|
||||
);
|
||||
const subGroupByList = getGroupByColumns(
|
||||
sub_group_by as GroupByColumnTypes,
|
||||
project,
|
||||
cycle,
|
||||
_module,
|
||||
label,
|
||||
projectState,
|
||||
member
|
||||
);
|
||||
|
||||
if (!groupByList || !subGroupByList) return null;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRef } from "react";
|
||||
import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues";
|
||||
import { HeaderGroupByCard } from "./headers/group-by-card";
|
||||
// hooks
|
||||
import { useLabel, useMember, useProject, useProjectState } from "hooks/store";
|
||||
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store";
|
||||
// types
|
||||
import {
|
||||
GroupByColumnTypes,
|
||||
@@ -65,10 +65,21 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
||||
const project = useProject();
|
||||
const label = useLabel();
|
||||
const projectState = useProjectState();
|
||||
const cycle = useCycle();
|
||||
const _module = useModule();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const groups = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member, true);
|
||||
const groups = getGroupByColumns(
|
||||
group_by as GroupByColumnTypes,
|
||||
project,
|
||||
cycle,
|
||||
_module,
|
||||
label,
|
||||
projectState,
|
||||
member,
|
||||
true
|
||||
);
|
||||
|
||||
if (!groups) return null;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { Layers, Link, Paperclip } from "lucide-react";
|
||||
import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react";
|
||||
import xor from "lodash/xor";
|
||||
// hooks
|
||||
import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store";
|
||||
@@ -286,6 +286,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
onChange={handleStartDate}
|
||||
maxDate={maxDate ?? undefined}
|
||||
placeholder="Start date"
|
||||
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
|
||||
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
|
||||
disabled={isReadOnly}
|
||||
showTooltip
|
||||
@@ -301,6 +302,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
onChange={handleTargetDate}
|
||||
minDate={minDate ?? undefined}
|
||||
placeholder="Due date"
|
||||
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
|
||||
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
|
||||
buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""}
|
||||
clearIconClassName="!text-custom-text-100"
|
||||
|
||||
@@ -96,6 +96,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
|
||||
storeType={EIssuesStoreType.PROJECT}
|
||||
/>
|
||||
<CustomMenu
|
||||
menuItemsClassName="z-[14]"
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
|
||||
@@ -56,6 +56,7 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
onSubmit={handleDelete}
|
||||
/>
|
||||
<CustomMenu
|
||||
menuItemsClassName="z-[14]"
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
|
||||
@@ -106,6 +106,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
||||
storeType={EIssuesStoreType.CYCLE}
|
||||
/>
|
||||
<CustomMenu
|
||||
menuItemsClassName="z-[14]"
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
|
||||
@@ -106,6 +106,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
|
||||
storeType={EIssuesStoreType.MODULE}
|
||||
/>
|
||||
<CustomMenu
|
||||
menuItemsClassName="z-[14]"
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
|
||||
@@ -108,6 +108,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
|
||||
isDraft={isDraftIssue}
|
||||
/>
|
||||
<CustomMenu
|
||||
menuItemsClassName="z-[14]"
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { CalendarCheck2 } from "lucide-react";
|
||||
// hooks
|
||||
import { useProjectState } from "hooks/store";
|
||||
// components
|
||||
@@ -43,6 +44,7 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props)
|
||||
}}
|
||||
disabled={disabled}
|
||||
placeholder="Due date"
|
||||
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonContainerClassName="w-full"
|
||||
buttonClassName={cn("rounded-none text-left", {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { CalendarClock } from "lucide-react";
|
||||
// components
|
||||
import { DateDropdown } from "components/dropdowns";
|
||||
// helpers
|
||||
@@ -35,6 +36,7 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Prop
|
||||
}}
|
||||
disabled={disabled}
|
||||
placeholder="Start date"
|
||||
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
|
||||
@@ -189,7 +189,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
<td
|
||||
id={`issue-${issueDetail.id}`}
|
||||
className={cn(
|
||||
"sticky group left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] z-[1] border-custom-border-200",
|
||||
"sticky group left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] z-10 border-custom-border-200",
|
||||
{
|
||||
"border-b-[0.5px]": peekIssue?.issueId !== issueDetail.id,
|
||||
},
|
||||
|
||||
@@ -19,10 +19,10 @@ export const SpreadsheetHeader = (props: Props) => {
|
||||
const { displayProperties, displayFilters, handleDisplayFilterUpdate, isEstimateEnabled } = props;
|
||||
|
||||
return (
|
||||
<thead className="sticky top-0 left-0 z-[2] border-b-[0.5px] border-custom-border-100">
|
||||
<thead className="sticky top-0 left-0 z-[12] border-b-[0.5px] border-custom-border-100">
|
||||
<tr>
|
||||
<th
|
||||
className="sticky left-0 z-[2] h-11 w-[28rem] flex items-center bg-custom-background-90 text-sm font-medium before:absolute before:h-full before:right-0 before:border-[0.5px] before:border-custom-border-100"
|
||||
className="sticky left-0 z-[15] h-11 w-[28rem] flex items-center bg-custom-background-90 text-sm font-medium before:absolute before:h-full before:right-0 before:border-[0.5px] before:border-custom-border-100"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
||||
import { EIssueListRow, ISSUE_PRIORITIES } from "constants/issue";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
||||
// stores
|
||||
import { IMemberRootStore } from "store/member";
|
||||
import { IProjectStore } from "store/project/project.store";
|
||||
import { IStateStore } from "store/state.store";
|
||||
import { GroupByColumnTypes, IGroupByColumn, IIssueListRow, TGroupedIssues, TUnGroupedIssues } from "@plane/types";
|
||||
import { STATE_GROUPS } from "constants/state";
|
||||
import { ILabelStore } from "store/label.store";
|
||||
import { ICycleStore } from "store/cycle.store";
|
||||
import { IModuleStore } from "store/module.store";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// constants
|
||||
import { STATE_GROUPS } from "constants/state";
|
||||
import { ISSUE_PRIORITIES } from "constants/issue";
|
||||
// types
|
||||
import { GroupByColumnTypes, IGroupByColumn, TCycleGroups } from "@plane/types";
|
||||
import { ContrastIcon } from "lucide-react";
|
||||
|
||||
export const getGroupByColumns = (
|
||||
groupBy: GroupByColumnTypes | null,
|
||||
project: IProjectStore,
|
||||
cycle: ICycleStore,
|
||||
module: IModuleStore,
|
||||
label: ILabelStore,
|
||||
projectState: IStateStore,
|
||||
member: IMemberRootStore,
|
||||
@@ -19,6 +28,10 @@ export const getGroupByColumns = (
|
||||
switch (groupBy) {
|
||||
case "project":
|
||||
return getProjectColumns(project);
|
||||
case "cycle":
|
||||
return getCycleColumns(project, cycle);
|
||||
case "module":
|
||||
return getModuleColumns(project, module);
|
||||
case "state":
|
||||
return getStateColumns(projectState);
|
||||
case "state_detail.group":
|
||||
@@ -55,6 +68,68 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined
|
||||
}) as any;
|
||||
};
|
||||
|
||||
const getCycleColumns = (projectStore: IProjectStore, cycleStore: ICycleStore): IGroupByColumn[] | undefined => {
|
||||
const { currentProjectDetails } = projectStore;
|
||||
const { getProjectCycleIds, getCycleById } = cycleStore;
|
||||
|
||||
if (!currentProjectDetails || !currentProjectDetails?.id) return;
|
||||
|
||||
const cycleIds = currentProjectDetails?.id ? getProjectCycleIds(currentProjectDetails?.id) : undefined;
|
||||
if (!cycleIds) return;
|
||||
|
||||
const cycles = [];
|
||||
|
||||
cycleIds.map((cycleId) => {
|
||||
const cycle = getCycleById(cycleId);
|
||||
if (cycle) {
|
||||
const cycleStatus = cycle.status ? (cycle.status.toLocaleLowerCase() as TCycleGroups) : "draft";
|
||||
cycles.push({
|
||||
id: cycle.id,
|
||||
name: cycle.name,
|
||||
icon: <CycleGroupIcon cycleGroup={cycleStatus as TCycleGroups} className="h-3.5 w-3.5" />,
|
||||
payload: { cycle_id: cycle.id },
|
||||
});
|
||||
}
|
||||
});
|
||||
cycles.push({
|
||||
id: "None",
|
||||
name: "None",
|
||||
icon: <ContrastIcon className="h-3.5 w-3.5" />,
|
||||
});
|
||||
|
||||
return cycles as any;
|
||||
};
|
||||
|
||||
const getModuleColumns = (projectStore: IProjectStore, moduleStore: IModuleStore): IGroupByColumn[] | undefined => {
|
||||
const { currentProjectDetails } = projectStore;
|
||||
const { getProjectModuleIds, getModuleById } = moduleStore;
|
||||
|
||||
if (!currentProjectDetails || !currentProjectDetails?.id) return;
|
||||
|
||||
const moduleIds = currentProjectDetails?.id ? getProjectModuleIds(currentProjectDetails?.id) : undefined;
|
||||
if (!moduleIds) return;
|
||||
|
||||
const modules = [];
|
||||
|
||||
moduleIds.map((moduleId) => {
|
||||
const _module = getModuleById(moduleId);
|
||||
if (_module)
|
||||
modules.push({
|
||||
id: _module.id,
|
||||
name: _module.name,
|
||||
icon: <DiceIcon className="w-3.5 h-3.5" />,
|
||||
payload: { module_ids: [_module.id] },
|
||||
});
|
||||
}) as any;
|
||||
modules.push({
|
||||
id: "None",
|
||||
name: "None",
|
||||
icon: <DiceIcon className="w-3.5 h-3.5" />,
|
||||
});
|
||||
|
||||
return modules as any;
|
||||
};
|
||||
|
||||
const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefined => {
|
||||
const { projectStates } = projectState;
|
||||
if (!projectStates) return;
|
||||
|
||||
@@ -100,8 +100,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
|
||||
const fetchIssueDetail = async (issueId: string | undefined) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (!projectId || issueId === undefined) {
|
||||
setDescription("<p></p>");
|
||||
setDescription(data?.description_html || "<p></p>");
|
||||
return;
|
||||
}
|
||||
const response = await fetchIssue(workspaceSlug, projectId, issueId, isDraft ? "DRAFT" : "DEFAULT");
|
||||
|
||||
@@ -14,153 +14,153 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
import { ProjectAnalyticsModal } from "components/analytics";
|
||||
|
||||
export const IssuesMobileHeader = () => {
|
||||
const layouts = [
|
||||
{ key: "list", title: "List", icon: List },
|
||||
{ key: "kanban", title: "Kanban", icon: Kanban },
|
||||
{ key: "calendar", title: "Calendar", icon: Calendar },
|
||||
];
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
const { workspaceSlug, projectId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
const layouts = [
|
||||
{ key: "list", title: "List", icon: List },
|
||||
{ key: "kanban", title: "Kanban", icon: Kanban },
|
||||
{ key: "calendar", title: "Calendar", icon: Calendar },
|
||||
];
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
const { workspaceSlug, projectId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues });
|
||||
},
|
||||
[workspaceSlug, projectId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property);
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
projectDetails={currentProjectDetails ?? undefined}
|
||||
/>
|
||||
<div className="flex justify-evenly py-2 border-b border-custom-border-200">
|
||||
<CustomMenu
|
||||
maxHeight={"md"}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
placement="bottom-start"
|
||||
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
|
||||
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
closeOnSelect
|
||||
>
|
||||
{layouts.map((layout, index) => (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleLayoutChange(ISSUE_LAYOUTS[index].key);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<layout.icon className="w-3 h-3" />
|
||||
<div className="text-custom-text-300">{layout.title}</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-custom-text-200 text-sm">
|
||||
Filters
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues });
|
||||
},
|
||||
[workspaceSlug, projectId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property);
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
projectDetails={currentProjectDetails ?? undefined}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
/>
|
||||
<div className="flex justify-evenly py-2 border-b border-custom-border-200">
|
||||
<CustomMenu
|
||||
maxHeight={"md"}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
placement="bottom-start"
|
||||
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
|
||||
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
closeOnSelect
|
||||
>
|
||||
{layouts.map((layout, index) => (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleLayoutChange(ISSUE_LAYOUTS[index].key);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<layout.icon className="w-3 h-3" />
|
||||
<div className="text-custom-text-300">{layout.title}</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-custom-text-200 text-sm">
|
||||
Filters
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
|
||||
<FiltersDropdown
|
||||
title="Display"
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-custom-text-200 text-sm">
|
||||
Display
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
|
||||
<FiltersDropdown
|
||||
title="Display"
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-custom-text-200 text-sm">
|
||||
Display
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setAnalyticsModal(true)}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
|
||||
>
|
||||
Analytics
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
<button
|
||||
onClick={() => setAnalyticsModal(true)}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
|
||||
>
|
||||
Analytics
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Signal, Tag, Triangle, LayoutPanelTop, CircleDot, CopyPlus, XCircle, CalendarDays } from "lucide-react";
|
||||
import {
|
||||
Signal,
|
||||
Tag,
|
||||
Triangle,
|
||||
LayoutPanelTop,
|
||||
CircleDot,
|
||||
CopyPlus,
|
||||
XCircle,
|
||||
CalendarClock,
|
||||
CalendarCheck2,
|
||||
} from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
|
||||
// ui icons
|
||||
@@ -118,7 +128,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
{/* start date */}
|
||||
<div className="flex w-full items-center gap-3 h-8">
|
||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarDays className="h-4 w-4 flex-shrink-0" />
|
||||
<CalendarClock className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Start date</span>
|
||||
</div>
|
||||
<DateDropdown
|
||||
@@ -145,7 +155,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
{/* due date */}
|
||||
<div className="flex w-full items-center gap-3 h-8">
|
||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarDays className="h-4 w-4 flex-shrink-0" />
|
||||
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Due date</span>
|
||||
</div>
|
||||
<DateDropdown
|
||||
|
||||
@@ -9,154 +9,155 @@ import router from "next/router";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export const ModuleMobileHeader = () => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
const { getModuleById } = useModule();
|
||||
const layouts = [
|
||||
{ key: "list", title: "List", icon: List },
|
||||
{ key: "kanban", title: "Kanban", icon: Kanban },
|
||||
{ key: "calendar", title: "Calendar", icon: Calendar },
|
||||
];
|
||||
const { workspaceSlug, projectId, moduleId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
moduleId: string;
|
||||
};
|
||||
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
const { getModuleById } = useModule();
|
||||
const layouts = [
|
||||
{ key: "list", title: "List", icon: List },
|
||||
{ key: "kanban", title: "Kanban", icon: Kanban },
|
||||
{ key: "calendar", title: "Calendar", icon: Calendar },
|
||||
];
|
||||
const { workspaceSlug, projectId, moduleId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
moduleId: string;
|
||||
};
|
||||
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
||||
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.MODULE);
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.MODULE);
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId);
|
||||
},
|
||||
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||
);
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId);
|
||||
},
|
||||
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId);
|
||||
},
|
||||
[workspaceSlug, projectId, moduleId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId);
|
||||
},
|
||||
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId);
|
||||
},
|
||||
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="block md:hidden">
|
||||
<ProjectAnalyticsModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
moduleDetails={moduleDetails ?? undefined}
|
||||
/>
|
||||
<div className="flex justify-evenly py-2 border-b border-custom-border-200">
|
||||
<CustomMenu
|
||||
maxHeight={"md"}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
placement="bottom-start"
|
||||
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
|
||||
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
closeOnSelect
|
||||
>
|
||||
{layouts.map((layout, index) => (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleLayoutChange(ISSUE_LAYOUTS[index].key);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<layout.icon className="w-3 h-3" />
|
||||
<div className="text-custom-text-300">{layout.title}</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-custom-text-200 text-sm">
|
||||
Filters
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId);
|
||||
},
|
||||
[workspaceSlug, projectId, moduleId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId);
|
||||
},
|
||||
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId);
|
||||
},
|
||||
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="block md:hidden">
|
||||
<ProjectAnalyticsModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
moduleDetails={moduleDetails ?? undefined}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
/>
|
||||
<div className="flex justify-evenly py-2 border-b border-custom-border-200">
|
||||
<CustomMenu
|
||||
maxHeight={"md"}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
placement="bottom-start"
|
||||
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
|
||||
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
closeOnSelect
|
||||
>
|
||||
{layouts.map((layout, index) => (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleLayoutChange(ISSUE_LAYOUTS[index].key);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<layout.icon className="w-3 h-3" />
|
||||
<div className="text-custom-text-300">{layout.title}</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-custom-text-200 text-sm">
|
||||
Filters
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
|
||||
<FiltersDropdown
|
||||
title="Display"
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-custom-text-200 text-sm">
|
||||
Display
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setAnalyticsModal(true)}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
|
||||
>
|
||||
Analytics
|
||||
</button>
|
||||
</div>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
);
|
||||
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
|
||||
<FiltersDropdown
|
||||
title="Display"
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-custom-text-200 text-sm">
|
||||
Display
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
ignoreGroupedFilters={["module"]}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setAnalyticsModal(true)}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
|
||||
>
|
||||
Analytics
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -368,12 +368,6 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{moduleDetails.description && (
|
||||
<span className="w-full whitespace-normal break-words py-2.5 text-sm leading-5 text-custom-text-200">
|
||||
{moduleDetails.description}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-5 pb-6 pt-2.5">
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
|
||||
|
||||
@@ -144,7 +144,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
|
||||
const handleProjectClick = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
themeStore.toggleMobileSidebar();
|
||||
themeStore.toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-reac
|
||||
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
|
||||
// assets
|
||||
import packageJson from "package.json";
|
||||
import useSize from "hooks/use-window-size";
|
||||
|
||||
const helpOptions = [
|
||||
{
|
||||
@@ -43,11 +42,10 @@ export interface WorkspaceHelpSectionProps {
|
||||
export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(() => {
|
||||
// store hooks
|
||||
const {
|
||||
theme: { sidebarCollapsed, toggleSidebar, toggleMobileSidebar },
|
||||
theme: { sidebarCollapsed, toggleSidebar },
|
||||
commandPalette: { toggleShortcutModal },
|
||||
} = useApplication();
|
||||
|
||||
const [windowWidth] = useSize();
|
||||
// states
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
// refs
|
||||
@@ -60,8 +58,9 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${isCollapsed ? "flex-col" : ""
|
||||
}`}
|
||||
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
|
||||
isCollapsed ? "flex-col" : ""
|
||||
}`}
|
||||
>
|
||||
{!isCollapsed && (
|
||||
<div className="w-1/2 cursor-default rounded-md bg-green-500/10 px-2.5 py-1.5 text-center text-sm font-medium text-green-500 outline-none">
|
||||
@@ -72,8 +71,9 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
||||
<Tooltip tooltipContent="Shortcuts">
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => toggleShortcutModal(true)}
|
||||
>
|
||||
<Zap className="h-3.5 w-3.5" />
|
||||
@@ -82,8 +82,9 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
||||
<Tooltip tooltipContent="Help">
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||
>
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
@@ -93,7 +94,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:hidden"
|
||||
onClick={() => windowWidth <= 768 ? toggleMobileSidebar() : toggleSidebar()}
|
||||
onClick={() => toggleSidebar()}
|
||||
>
|
||||
<MoveLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@@ -101,9 +102,10 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
||||
<Tooltip tooltipContent={`${isCollapsed ? "Expand" : "Hide"}`}>
|
||||
<button
|
||||
type="button"
|
||||
className={`hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => windowWidth <= 768 ? toggleMobileSidebar() : toggleSidebar()}
|
||||
className={`hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${
|
||||
isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => toggleSidebar()}
|
||||
>
|
||||
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isCollapsed ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
@@ -121,8 +123,9 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
className={`absolute bottom-2 min-w-[10rem] ${isCollapsed ? "left-full" : "-left-[75px]"
|
||||
} divide-y divide-custom-border-200 whitespace-nowrap rounded bg-custom-background-100 p-1 shadow-custom-shadow-xs`}
|
||||
className={`absolute bottom-2 min-w-[10rem] ${
|
||||
isCollapsed ? "left-full" : "-left-[75px]"
|
||||
} divide-y divide-custom-border-200 whitespace-nowrap rounded bg-custom-background-100 p-1 shadow-custom-shadow-xs`}
|
||||
ref={helpOptionsRef}
|
||||
>
|
||||
<div className="space-y-1 pb-2">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { mutate } from "swr";
|
||||
import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react";
|
||||
import { usePopper } from "react-popper";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker, useUser, useWorkspace } from "hooks/store";
|
||||
import { useApplication, useUser, useWorkspace } from "hooks/store";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
@@ -54,9 +54,8 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
const { workspaceSlug } = router.query;
|
||||
// store hooks
|
||||
const {
|
||||
theme: { sidebarCollapsed, toggleMobileSidebar },
|
||||
theme: { sidebarCollapsed, toggleSidebar },
|
||||
} = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { currentUser, updateCurrentUser, isUserInstanceAdmin, signOut } = useUser();
|
||||
const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace();
|
||||
// hooks
|
||||
@@ -98,7 +97,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
};
|
||||
const handleItemClick = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
toggleMobileSidebar();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
const workspacesList = Object.values(workspaces ?? {});
|
||||
|
||||
@@ -31,7 +31,7 @@ export const WorkspaceSidebarMenu = observer(() => {
|
||||
|
||||
const handleLinkClick = (itemKey: string) => {
|
||||
if (window.innerWidth < 768) {
|
||||
themeStore.toggleMobileSidebar();
|
||||
themeStore.toggleSidebar();
|
||||
}
|
||||
captureEvent(SIDEBAR_CLICKED, {
|
||||
destination: itemKey,
|
||||
@@ -52,10 +52,11 @@ export const WorkspaceSidebarMenu = observer(() => {
|
||||
disabled={!themeStore?.sidebarCollapsed}
|
||||
>
|
||||
<div
|
||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${link.highlight(router.asPath, `/${workspaceSlug}`)
|
||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||
} ${themeStore?.sidebarCollapsed ? "justify-center" : ""}`}
|
||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
||||
link.highlight(router.asPath, `/${workspaceSlug}`)
|
||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||
} ${themeStore?.sidebarCollapsed ? "justify-center" : ""}`}
|
||||
>
|
||||
{
|
||||
<link.Icon
|
||||
|
||||
@@ -58,6 +58,7 @@ export const WorkspaceSidebarQuickAction = observer(() => {
|
||||
const draftIssues = storedValue ?? {};
|
||||
if (workspaceSlug && draftIssues[workspaceSlug]) delete draftIssues[workspaceSlug];
|
||||
setValue(draftIssues);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -66,7 +67,7 @@ export const WorkspaceSidebarQuickAction = observer(() => {
|
||||
isOpen={isDraftIssueModalOpen}
|
||||
onClose={() => setIsDraftIssueModalOpen(false)}
|
||||
data={workspaceDraftIssue ?? {}}
|
||||
// storeType={storeType}
|
||||
onSubmit={() => removeWorkspaceDraftIssue()}
|
||||
isDraft={true}
|
||||
/>
|
||||
|
||||
|
||||
+80
-105
@@ -49,22 +49,6 @@ export const ISSUE_PRIORITIES: {
|
||||
{ key: "none", title: "None" },
|
||||
];
|
||||
|
||||
export const ISSUE_START_DATE_OPTIONS = [
|
||||
{ key: "last_week", title: "Last Week" },
|
||||
{ key: "2_weeks_from_now", title: "2 weeks from now" },
|
||||
{ key: "1_month_from_now", title: "1 month from now" },
|
||||
{ key: "2_months_from_now", title: "2 months from now" },
|
||||
{ key: "custom", title: "Custom" },
|
||||
];
|
||||
|
||||
export const ISSUE_DUE_DATE_OPTIONS = [
|
||||
{ key: "last_week", title: "Last Week" },
|
||||
{ key: "2_weeks_from_now", title: "2 weeks from now" },
|
||||
{ key: "1_month_from_now", title: "1 month from now" },
|
||||
{ key: "2_months_from_now", title: "2 months from now" },
|
||||
{ key: "custom", title: "Custom" },
|
||||
];
|
||||
|
||||
export const ISSUE_GROUP_BY_OPTIONS: {
|
||||
key: TIssueGroupByOptions;
|
||||
title: string;
|
||||
@@ -73,6 +57,8 @@ export const ISSUE_GROUP_BY_OPTIONS: {
|
||||
{ key: "state_detail.group", title: "State Groups" },
|
||||
{ key: "priority", title: "Priority" },
|
||||
{ key: "project", title: "Project" }, // required this on my issues
|
||||
{ key: "cycle", title: "Cycle" }, // required this on my issues
|
||||
{ key: "module", title: "Module" }, // required this on my issues
|
||||
{ key: "labels", title: "Labels" },
|
||||
{ key: "assignees", title: "Assignees" },
|
||||
{ key: "created_by", title: "Created By" },
|
||||
@@ -140,81 +126,6 @@ export const ISSUE_LAYOUTS: {
|
||||
{ key: "gantt_chart", title: "Gantt Chart Layout", icon: GanttChartSquare },
|
||||
];
|
||||
|
||||
export const ISSUE_LIST_FILTERS = [
|
||||
{ key: "mentions", title: "Mentions" },
|
||||
{ key: "priority", title: "Priority" },
|
||||
{ key: "state", title: "State" },
|
||||
{ key: "assignees", title: "Assignees" },
|
||||
{ key: "created_by", title: "Created By" },
|
||||
{ key: "labels", title: "Labels" },
|
||||
{ key: "start_date", title: "Start Date" },
|
||||
{ key: "due_date", title: "Due Date" },
|
||||
];
|
||||
|
||||
export const ISSUE_KANBAN_FILTERS = [
|
||||
{ key: "priority", title: "Priority" },
|
||||
{ key: "state", title: "State" },
|
||||
{ key: "assignees", title: "Assignees" },
|
||||
{ key: "created_by", title: "Created By" },
|
||||
{ key: "labels", title: "Labels" },
|
||||
{ key: "start_date", title: "Start Date" },
|
||||
{ key: "due_date", title: "Due Date" },
|
||||
];
|
||||
|
||||
export const ISSUE_CALENDER_FILTERS = [
|
||||
{ key: "priority", title: "Priority" },
|
||||
{ key: "state", title: "State" },
|
||||
{ key: "assignees", title: "Assignees" },
|
||||
{ key: "created_by", title: "Created By" },
|
||||
{ key: "labels", title: "Labels" },
|
||||
];
|
||||
|
||||
export const ISSUE_SPREADSHEET_FILTERS = [
|
||||
{ key: "priority", title: "Priority" },
|
||||
{ key: "state", title: "State" },
|
||||
{ key: "assignees", title: "Assignees" },
|
||||
{ key: "created_by", title: "Created By" },
|
||||
{ key: "labels", title: "Labels" },
|
||||
{ key: "start_date", title: "Start Date" },
|
||||
{ key: "due_date", title: "Due Date" },
|
||||
];
|
||||
|
||||
export const ISSUE_GANTT_FILTERS = [
|
||||
{ key: "priority", title: "Priority" },
|
||||
{ key: "state", title: "State" },
|
||||
{ key: "assignees", title: "Assignees" },
|
||||
{ key: "created_by", title: "Created By" },
|
||||
{ key: "labels", title: "Labels" },
|
||||
{ key: "start_date", title: "Start Date" },
|
||||
{ key: "due_date", title: "Due Date" },
|
||||
];
|
||||
|
||||
export const ISSUE_LIST_DISPLAY_FILTERS = [
|
||||
{ key: "group_by", title: "Group By" },
|
||||
{ key: "order_by", title: "Order By" },
|
||||
{ key: "issue_type", title: "Issue Type" },
|
||||
{ key: "sub_issue", title: "Sub Issue" },
|
||||
{ key: "show_empty_groups", title: "Show Empty Groups" },
|
||||
];
|
||||
|
||||
export const ISSUE_KANBAN_DISPLAY_FILTERS = [
|
||||
{ key: "group_by", title: "Group By" },
|
||||
{ key: "order_by", title: "Order By" },
|
||||
{ key: "issue_type", title: "Issue Type" },
|
||||
{ key: "sub_issue", title: "Sub Issue" },
|
||||
{ key: "show_empty_groups", title: "Show Empty Groups" },
|
||||
];
|
||||
|
||||
export const ISSUE_CALENDER_DISPLAY_FILTERS = [{ key: "issue_type", title: "Issue Type" }];
|
||||
|
||||
export const ISSUE_SPREADSHEET_DISPLAY_FILTERS = [{ key: "issue_type", title: "Issue Type" }];
|
||||
|
||||
export const ISSUE_GANTT_DISPLAY_FILTERS = [
|
||||
{ key: "order_by", title: "Order By" },
|
||||
{ key: "issue_type", title: "Issue Type" },
|
||||
{ key: "sub_issue", title: "Sub Issue" },
|
||||
];
|
||||
|
||||
export interface ILayoutDisplayFiltersOptions {
|
||||
filters: (keyof IIssueFilterOptions)[];
|
||||
display_properties: boolean;
|
||||
@@ -263,10 +174,30 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||
},
|
||||
archived_issues: {
|
||||
list: {
|
||||
filters: ["priority", "state", "assignees", "created_by", "labels", "start_date", "target_date"],
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"cycle",
|
||||
"module",
|
||||
"assignees",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
display_properties: true,
|
||||
display_filters: {
|
||||
group_by: ["state", "state_detail.group", "priority", "labels", "assignees", "created_by", null],
|
||||
group_by: [
|
||||
"state",
|
||||
"cycle",
|
||||
"module",
|
||||
"state_detail.group",
|
||||
"priority",
|
||||
"labels",
|
||||
"assignees",
|
||||
"created_by",
|
||||
null,
|
||||
],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
@@ -278,10 +209,10 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||
},
|
||||
draft_issues: {
|
||||
list: {
|
||||
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
|
||||
filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date"],
|
||||
display_properties: true,
|
||||
display_filters: {
|
||||
group_by: ["state_detail.group", "priority", "project", "labels", null],
|
||||
group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels", null],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
@@ -291,10 +222,10 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||
},
|
||||
},
|
||||
kanban: {
|
||||
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
|
||||
filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date"],
|
||||
display_properties: true,
|
||||
display_filters: {
|
||||
group_by: ["state_detail.group", "priority", "project", "labels"],
|
||||
group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels"],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
@@ -350,10 +281,21 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||
},
|
||||
issues: {
|
||||
list: {
|
||||
filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"],
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"cycle",
|
||||
"module",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
display_properties: true,
|
||||
display_filters: {
|
||||
group_by: ["state", "priority", "labels", "assignees", "created_by", null],
|
||||
group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
@@ -363,11 +305,22 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||
},
|
||||
},
|
||||
kanban: {
|
||||
filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"],
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"cycle",
|
||||
"module",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
display_properties: true,
|
||||
display_filters: {
|
||||
group_by: ["state", "priority", "labels", "assignees", "created_by"],
|
||||
sub_group_by: ["state", "priority", "labels", "assignees", "created_by", null],
|
||||
group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by"],
|
||||
sub_group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority", "target_date"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
@@ -377,7 +330,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||
},
|
||||
},
|
||||
calendar: {
|
||||
filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date"],
|
||||
filters: ["priority", "state", "cycle", "module", "assignees", "mentions", "created_by", "labels", "start_date"],
|
||||
display_properties: true,
|
||||
display_filters: {
|
||||
type: [null, "active", "backlog"],
|
||||
@@ -388,7 +341,18 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||
},
|
||||
},
|
||||
spreadsheet: {
|
||||
filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"],
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"cycle",
|
||||
"module",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
display_properties: true,
|
||||
display_filters: {
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
@@ -400,7 +364,18 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||
},
|
||||
},
|
||||
gantt_chart: {
|
||||
filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"],
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"cycle",
|
||||
"module",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
display_properties: false,
|
||||
display_filters: {
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types";
|
||||
import { LayersIcon, DoubleCircleIcon, UserGroupIcon, DiceIcon, ContrastIcon } from "@plane/ui";
|
||||
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip } from "lucide-react";
|
||||
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarCheck2, CalendarClock } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
import { ISvgIcons } from "@plane/ui/src/icons/type";
|
||||
import {
|
||||
@@ -60,7 +60,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
ascendingOrderTitle: "New",
|
||||
descendingOrderKey: "target_date",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: CalendarDays,
|
||||
icon: CalendarCheck2,
|
||||
Column: SpreadsheetDueDateColumn,
|
||||
},
|
||||
estimate: {
|
||||
@@ -114,7 +114,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
ascendingOrderTitle: "New",
|
||||
descendingOrderKey: "start_date",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: CalendarDays,
|
||||
icon: CalendarClock,
|
||||
Column: SpreadsheetStartDateColumn,
|
||||
},
|
||||
state: {
|
||||
|
||||
@@ -30,7 +30,7 @@ export const renderEmoji = (
|
||||
|
||||
if (typeof emoji === "object")
|
||||
return (
|
||||
<span style={{ color: emoji.color }} className="material-symbols-rounded !text-sm">
|
||||
<span style={{ fontSize: "16px", color: emoji.color }} className="material-symbols-rounded">
|
||||
{emoji.name}
|
||||
</span>
|
||||
);
|
||||
@@ -41,16 +41,13 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]:
|
||||
reactions: any,
|
||||
key: string
|
||||
) => {
|
||||
const groupedReactions = reactions.reduce(
|
||||
(acc: any, reaction: any) => {
|
||||
if (!acc[reaction[key]]) {
|
||||
acc[reaction[key]] = [];
|
||||
}
|
||||
acc[reaction[key]].push(reaction);
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: any[] }
|
||||
);
|
||||
const groupedReactions = reactions.reduce((acc: any, reaction: any) => {
|
||||
if (!acc[reaction[key]]) {
|
||||
acc[reaction[key]] = [];
|
||||
}
|
||||
acc[reaction[key]].push(reaction);
|
||||
return acc;
|
||||
}, {} as { [key: string]: any[] });
|
||||
|
||||
return groupedReactions;
|
||||
};
|
||||
|
||||
@@ -9,13 +9,13 @@ export const usePage = (pageId: string) => {
|
||||
const { projectPageMap, projectArchivedPageMap } = context.projectPages;
|
||||
|
||||
const { projectId, workspaceSlug } = context.app.router;
|
||||
if (!projectId || !workspaceSlug) throw new Error("usePage must be used within ProjectProvider");
|
||||
|
||||
if (projectPageMap[projectId] && projectPageMap[projectId][pageId]) {
|
||||
return projectPageMap[projectId][pageId];
|
||||
} else if (projectArchivedPageMap[projectId] && projectArchivedPageMap[projectId][pageId]) {
|
||||
return projectArchivedPageMap[projectId][pageId];
|
||||
} else {
|
||||
if (!projectId || !workspaceSlug) {
|
||||
console.log("usePage must be used within ProjectProvider");
|
||||
return;
|
||||
}
|
||||
|
||||
if (projectPageMap[projectId] && projectPageMap[projectId][pageId]) return projectPageMap[projectId][pageId];
|
||||
else if (projectArchivedPageMap[projectId] && projectArchivedPageMap[projectId][pageId])
|
||||
return projectArchivedPageMap[projectId][pageId];
|
||||
else return;
|
||||
};
|
||||
|
||||
@@ -6,11 +6,6 @@ import { CommandPalette } from "components/command-palette";
|
||||
import { AppSidebar } from "./sidebar";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// FIXME: remove this later
|
||||
import { useIssues } from "hooks/store/use-issues";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import useSWR from "swr";
|
||||
|
||||
export interface IAppLayout {
|
||||
children: ReactNode;
|
||||
header: ReactNode;
|
||||
@@ -20,22 +15,6 @@ export interface IAppLayout {
|
||||
export const AppLayout: FC<IAppLayout> = observer((props) => {
|
||||
const { children, header, withProjectWrapper = false } = props;
|
||||
|
||||
const workspaceSlug = "plane-demo";
|
||||
const projectId = "b16907a9-a55f-4f5b-b05e-7065a0869ba6";
|
||||
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_ARCHIVED_ISSUES_V3_${workspaceSlug}_${projectId}` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await issuesFilter?.fetchFilters(workspaceSlug, projectId);
|
||||
// await issues?.fetchIssues(workspaceSlug, projectId, issues?.groupedIssueIds ? "mutation" : "init-loader");
|
||||
}
|
||||
},
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommandPalette />
|
||||
|
||||
@@ -20,9 +20,9 @@ export const AppSidebar: FC<IAppSidebar> = observer(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClickDetector(ref, () => {
|
||||
if (themStore.mobileSidebarCollapsed === false) {
|
||||
if (themStore.sidebarCollapsed === false) {
|
||||
if (window.innerWidth < 768) {
|
||||
themStore.toggleMobileSidebar();
|
||||
themStore.toggleSidebar();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -31,8 +31,8 @@ export const AppSidebar: FC<IAppSidebar> = observer(() => {
|
||||
<div
|
||||
className={`inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300
|
||||
fixed md:relative
|
||||
${themStore.mobileSidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
sm:${themStore.mobileSidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
${themStore.sidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
sm:${themStore.sidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
md:ml-0 ${themStore.sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
||||
lg:ml-0 ${themStore.sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
||||
`}
|
||||
|
||||
@@ -40,7 +40,7 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
const { setToastAlert } = useToast();
|
||||
// store hooks
|
||||
const {
|
||||
theme: { sidebarCollapsed, toggleSidebar, toggleMobileSidebar },
|
||||
theme: { sidebarCollapsed, toggleSidebar },
|
||||
} = useApplication();
|
||||
const { currentUser, currentUserSettings, signOut } = useUser();
|
||||
const { workspaces } = useWorkspace();
|
||||
@@ -78,7 +78,7 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
|
||||
const handleItemClick = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
toggleMobileSidebar();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,8 +113,9 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
<div ref={ref} className="flex h-full w-full flex-col gap-y-4">
|
||||
<Link href={`/${redirectWorkspaceSlug}`} onClick={handleItemClick}>
|
||||
<div
|
||||
className={`flex flex-shrink-0 items-center gap-2 truncate px-4 pt-4 ${sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
className={`flex flex-shrink-0 items-center gap-2 truncate px-4 pt-4 ${
|
||||
sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="grid h-5 w-5 flex-shrink-0 place-items-center">
|
||||
<ChevronLeft className="h-5 w-5" strokeWidth={1} />
|
||||
@@ -136,10 +137,11 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
<Link key={link.key} href={link.href} className="block w-full" onClick={handleItemClick}>
|
||||
<Tooltip tooltipContent={link.label} position="right" className="ml-2" disabled={!sidebarCollapsed}>
|
||||
<div
|
||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${link.highlight(router.pathname)
|
||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
} ${sidebarCollapsed ? "justify-center" : ""}`}
|
||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
||||
link.highlight(router.pathname)
|
||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
} ${sidebarCollapsed ? "justify-center" : ""}`}
|
||||
>
|
||||
{<link.Icon className="h-4 w-4" />}
|
||||
{!sidebarCollapsed && link.label}
|
||||
@@ -160,17 +162,20 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
<Link
|
||||
key={workspace.id}
|
||||
href={`/${workspace.slug}`}
|
||||
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${sidebarCollapsed ? "justify-center" : `justify-between`
|
||||
}`}
|
||||
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${
|
||||
sidebarCollapsed ? "justify-center" : `justify-between`
|
||||
}`}
|
||||
onClick={handleItemClick}
|
||||
>
|
||||
<span
|
||||
className={`flex w-full flex-grow items-center gap-x-2 truncate rounded-md px-3 py-1 hover:bg-custom-sidebar-background-80 ${sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
className={`flex w-full flex-grow items-center gap-x-2 truncate rounded-md px-3 py-1 hover:bg-custom-sidebar-background-80 ${
|
||||
sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${!workspace?.logo && "rounded bg-custom-primary-500 text-white"
|
||||
}`}
|
||||
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${
|
||||
!workspace?.logo && "rounded bg-custom-primary-500 text-white"
|
||||
}`}
|
||||
>
|
||||
{workspace?.logo && workspace.logo !== "" ? (
|
||||
<img
|
||||
@@ -195,8 +200,9 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
<Link className="block w-full" key={link.key} href={link.href} onClick={handleItemClick}>
|
||||
<Tooltip tooltipContent={link.label} position="right" className="ml-2" disabled={!sidebarCollapsed}>
|
||||
<div
|
||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-custom-sidebar-text-200 outline-none hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80 ${sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-custom-sidebar-text-200 outline-none hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80 ${
|
||||
sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
{<link.Icon className="h-4 w-4" />}
|
||||
{!sidebarCollapsed && link.label}
|
||||
@@ -208,8 +214,9 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 flex-grow items-end px-6 py-2">
|
||||
<div
|
||||
className={`flex w-full ${sidebarCollapsed ? "flex-col justify-center gap-2" : "items-center justify-between gap-2"
|
||||
}`}
|
||||
className={`flex w-full ${
|
||||
sidebarCollapsed ? "flex-col justify-center gap-2" : "items-center justify-between gap-2"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -229,8 +236,9 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`ml-auto hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${sidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
className={`ml-auto hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${
|
||||
sidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => toggleSidebar()}
|
||||
>
|
||||
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${sidebarCollapsed ? "rotate-180" : ""}`} />
|
||||
|
||||
@@ -6,14 +6,12 @@ import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper";
|
||||
export interface IThemeStore {
|
||||
// observables
|
||||
theme: string | null;
|
||||
mobileSidebarCollapsed: boolean | undefined;
|
||||
sidebarCollapsed: boolean | undefined;
|
||||
profileSidebarCollapsed: boolean | undefined;
|
||||
workspaceAnalyticsSidebarCollapsed: boolean | undefined;
|
||||
issueDetailSidebarCollapsed: boolean | undefined;
|
||||
// actions
|
||||
toggleSidebar: (collapsed?: boolean) => void;
|
||||
toggleMobileSidebar: (collapsed?: boolean) => void;
|
||||
setTheme: (theme: any) => void;
|
||||
toggleProfileSidebar: (collapsed?: boolean) => void;
|
||||
toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void;
|
||||
@@ -22,7 +20,6 @@ export interface IThemeStore {
|
||||
|
||||
export class ThemeStore implements IThemeStore {
|
||||
// observables
|
||||
mobileSidebarCollapsed: boolean | undefined = true;
|
||||
sidebarCollapsed: boolean | undefined = undefined;
|
||||
theme: string | null = null;
|
||||
profileSidebarCollapsed: boolean | undefined = undefined;
|
||||
@@ -34,7 +31,6 @@ export class ThemeStore implements IThemeStore {
|
||||
constructor(_rootStore: any | null = null) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
mobileSidebarCollapsed: observable.ref,
|
||||
sidebarCollapsed: observable.ref,
|
||||
theme: observable.ref,
|
||||
profileSidebarCollapsed: observable.ref,
|
||||
@@ -42,7 +38,6 @@ export class ThemeStore implements IThemeStore {
|
||||
issueDetailSidebarCollapsed: observable.ref,
|
||||
// action
|
||||
toggleSidebar: action,
|
||||
toggleMobileSidebar: action,
|
||||
setTheme: action,
|
||||
toggleProfileSidebar: action,
|
||||
toggleWorkspaceAnalyticsSidebar: action,
|
||||
@@ -66,19 +61,6 @@ export class ThemeStore implements IThemeStore {
|
||||
localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle mobile sidebar collapsed state
|
||||
* @param collapsed
|
||||
*/
|
||||
toggleMobileSidebar = (collapsed?: boolean) => {
|
||||
if (collapsed === undefined) {
|
||||
this.mobileSidebarCollapsed = !this.mobileSidebarCollapsed;
|
||||
} else {
|
||||
this.mobileSidebarCollapsed = collapsed;
|
||||
}
|
||||
localStorage.setItem("mobile_sidebar_collapsed", this.mobileSidebarCollapsed.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the profile sidebar collapsed state
|
||||
* @param collapsed
|
||||
|
||||
@@ -84,6 +84,8 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
|
||||
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues");
|
||||
if (!filteredParams) return undefined;
|
||||
|
||||
if (filteredParams.includes("cycle")) filteredParams.splice(filteredParams.indexOf("cycle"), 1);
|
||||
|
||||
const filteredRouteParams: Partial<Record<TIssueParams, string | boolean>> = this.computedFilteredParams(
|
||||
userFilters?.filters as IIssueFilterOptions,
|
||||
userFilters?.displayFilters as IIssueDisplayFilterOptions,
|
||||
|
||||
@@ -74,6 +74,8 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
||||
mentions: filters?.mentions || undefined,
|
||||
created_by: filters?.created_by || undefined,
|
||||
labels: filters?.labels || undefined,
|
||||
cycle: filters?.cycle || undefined,
|
||||
module: filters?.module || undefined,
|
||||
start_date: filters?.start_date || undefined,
|
||||
target_date: filters?.target_date || undefined,
|
||||
project: filters.project || undefined,
|
||||
@@ -107,6 +109,8 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
||||
mentions: filters?.mentions || null,
|
||||
created_by: filters?.created_by || null,
|
||||
labels: filters?.labels || null,
|
||||
cycle: filters?.cycle || null,
|
||||
module: filters?.module || null,
|
||||
start_date: filters?.start_date || null,
|
||||
target_date: filters?.target_date || null,
|
||||
project: filters?.project || null,
|
||||
|
||||
@@ -36,6 +36,8 @@ export type TIssueHelperStore = {
|
||||
|
||||
const ISSUE_FILTER_DEFAULT_DATA: Record<TIssueDisplayFilterOptions, keyof TIssue> = {
|
||||
project: "project_id",
|
||||
cycle: "cycle_id",
|
||||
module: "module_ids",
|
||||
state: "state_id",
|
||||
"state_detail.group": "state_group" as keyof TIssue, // state_detail.group is only being used for state_group display,
|
||||
priority: "priority",
|
||||
@@ -157,6 +159,10 @@ export class IssueHelperStore implements TIssueHelperStore {
|
||||
return Object.keys(this.rootStore?.workSpaceMemberRolesMap || {});
|
||||
case "project":
|
||||
return Object.keys(this.rootStore?.projectMap || {});
|
||||
case "cycle":
|
||||
return Object.keys(this.rootStore?.cycleMap || {});
|
||||
case "module":
|
||||
return Object.keys(this.rootStore?.moduleMap || {});
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul
|
||||
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues");
|
||||
if (!filteredParams) return undefined;
|
||||
|
||||
if (filteredParams.includes("module")) filteredParams.splice(filteredParams.indexOf("module"), 1);
|
||||
|
||||
const filteredRouteParams: Partial<Record<TIssueParams, string | boolean>> = this.computedFilteredParams(
|
||||
userFilters?.filters as IIssueFilterOptions,
|
||||
userFilters?.displayFilters as IIssueDisplayFilterOptions,
|
||||
|
||||
@@ -148,7 +148,7 @@ export class ProjectStore implements IProjectStore {
|
||||
projects = sortBy(projects, "created_at");
|
||||
|
||||
const projectIds = projects
|
||||
.filter((project) => project.workspace === currentWorkspace.id && project.is_favorite)
|
||||
.filter((project) => project.workspace === currentWorkspace.id && project.is_member && project.is_favorite)
|
||||
.map((project) => project.id);
|
||||
return projectIds;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user