Merge branch 'develop' of https://github.com/makeplane/plane into chore/event-improvements
@ -1,82 +1,79 @@
|
|||||||
# 1-Click Self-Hosting
|
# One-click deploy
|
||||||
|
|
||||||
In this guide, we will walk you through the process of setting up a 1-click self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization.
|
Deployment methods for Plane have improved significantly to make self-managing super-easy. One of those is a single-line-command installation of Plane.
|
||||||
|
|
||||||
Let's get started!
|
This short guide will guide you through the process, the background tasks that run with the command for the Community, One, and Enterprise editions, and the post-deployment configuration options available to you.
|
||||||
|
|
||||||
## Installing Plane
|
### Requirements
|
||||||
|
|
||||||
Installing Plane is a very easy and minimal step process.
|
- Operating systems: Debian, Ubuntu, CentOS
|
||||||
|
- Supported CPU architectures: AMD64, ARM64, x86_64, AArch64
|
||||||
|
|
||||||
### Prerequisite
|
### Download the latest stable release
|
||||||
|
|
||||||
- Operating System (latest): Debian / Ubuntu / Centos
|
Run ↓ on any CLI.
|
||||||
- Supported CPU Architechture: AMD64 / ARM64 / x86_64 / aarch64
|
|
||||||
|
|
||||||
### Downloading Latest Stable Release
|
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh -
|
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh -
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
### Download the Preview release
|
||||||
<summary>Downloading Preview Release</summary>
|
|
||||||
|
`Preview` builds do not support ARM64, AArch64 CPU architectures
|
||||||
|
|
||||||
|
Run ↓ on any CLI.
|
||||||
|
|
||||||
```
|
```
|
||||||
export BRANCH=preview
|
export BRANCH=preview
|
||||||
|
|
||||||
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh -
|
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh -
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
NOTE: `Preview` builds do not support ARM64/AARCH64 CPU architecture
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
--
|
|
||||||
|
|
||||||
Expect this after a successful install
|
|
||||||
|
|
||||||
![Install Output](images/install.png)
|
|
||||||
|
|
||||||
Access the application on a browser via http://server-ip-address
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Get Control of your Plane Server Setup
|
### Successful installation
|
||||||
|
|
||||||
Plane App is available via the command `plane-app`. Running the command `plane-app --help` helps you to manage Plane
|
You should see ↓ if there are no hitches. That output will also list the IP address you can use to access your Plane instance.
|
||||||
|
|
||||||
|
![Install Output](images/install.png)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Manage your Plane instance
|
||||||
|
|
||||||
|
Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of all operators with `plane-app ---help`.
|
||||||
|
|
||||||
![Plane Help](images/help.png)
|
![Plane Help](images/help.png)
|
||||||
|
|
||||||
<ins>Basic Operations</ins>:
|
1. Basic operators
|
||||||
|
|
||||||
1. Start Server using `plane-app start`
|
1. `plane-app start` starts the Plane server.
|
||||||
1. Stop Server using `plane-app stop`
|
2. `plane-app restart` restarts the Plane server.
|
||||||
1. Restart Server using `plane-app restart`
|
3. `plane-app stop` stops the Plane server.
|
||||||
|
|
||||||
<ins>Advanced Operations</ins>:
|
2. Advanced operators
|
||||||
|
|
||||||
1. Configure Plane using `plane-app --configure`. This will give you options to modify
|
`plane-app --configure` will show advanced configurators.
|
||||||
|
|
||||||
- NGINX Port (default 80)
|
- Change your proxy or listening port
|
||||||
- Domain Name (default is the local server public IP address)
|
<br>Default: 80
|
||||||
- File Upload Size (default 5MB)
|
- Change your domain name
|
||||||
- External Postgres DB Url (optional - default empty)
|
<br>Default: Deployed server's public IP address
|
||||||
- External Redis URL (optional - default empty)
|
- File upload size
|
||||||
- AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket)
|
<br>Default: 5MB
|
||||||
|
- Specify external database address when using an external database
|
||||||
|
<br>Default: `Empty`
|
||||||
|
<br>`Default folder: /opt/plane/data/postgres`
|
||||||
|
- Specify external Redis URL when using external Redis
|
||||||
|
<br>Default: `Empty`
|
||||||
|
<br>`Default folder: /opt/plane/data/redis`
|
||||||
|
- Configure AWS S3 bucket
|
||||||
|
<br>Use only when you or your users want to use S3
|
||||||
|
<br>`Default folder: /opt/plane/data/minio`
|
||||||
|
|
||||||
1. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images)
|
3. Version operators
|
||||||
|
|
||||||
1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility.
|
1. `plane-app --upgrade` gets the latest stable version of `docker-compose.yaml`, `.env`, and Docker images
|
||||||
|
2. `plane-app --update-installer` updates the installer and the `plane-app` utility.
|
||||||
1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio.
|
3. `plane-app --uninstall` uninstalls the Plane application and all Docker containers from the server but leaves the data stored in
|
||||||
|
Postgres, Redis, and Minio alone.
|
||||||
1. Plane App can be reinstalled using `plane-app --install`.
|
4. `plane-app --install` installs the Plane app again.
|
||||||
|
|
||||||
<ins>Application Data is stored in the mentioned folders</ins>:
|
|
||||||
|
|
||||||
1. DB Data: /opt/plane/data/postgres
|
|
||||||
1. Redis Data: /opt/plane/data/redis
|
|
||||||
1. Minio Data: /opt/plane/data/minio
|
|
||||||
|
@ -4,18 +4,18 @@ import { findTableAncestor } from "src/lib/utils";
|
|||||||
import { UploadImage } from "src/types/upload-image";
|
import { UploadImage } from "src/types/upload-image";
|
||||||
|
|
||||||
export const toggleHeadingOne = (editor: Editor, range?: Range) => {
|
export const toggleHeadingOne = (editor: Editor, range?: Range) => {
|
||||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
|
if (range) editor.chain().focus().deleteRange(range).clearNodes().setNode("heading", { level: 1 }).run();
|
||||||
else editor.chain().focus().toggleHeading({ level: 1 }).run();
|
else editor.chain().focus().clearNodes().toggleHeading({ level: 1 }).run();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleHeadingTwo = (editor: Editor, range?: Range) => {
|
export const toggleHeadingTwo = (editor: Editor, range?: Range) => {
|
||||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
|
if (range) editor.chain().focus().deleteRange(range).clearNodes().setNode("heading", { level: 2 }).run();
|
||||||
else editor.chain().focus().toggleHeading({ level: 2 }).run();
|
else editor.chain().focus().clearNodes().toggleHeading({ level: 2 }).run();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleHeadingThree = (editor: Editor, range?: Range) => {
|
export const toggleHeadingThree = (editor: Editor, range?: Range) => {
|
||||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
|
if (range) editor.chain().focus().deleteRange(range).clearNodes().setNode("heading", { level: 3 }).run();
|
||||||
else editor.chain().focus().toggleHeading({ level: 3 }).run();
|
else editor.chain().focus().clearNodes().toggleHeading({ level: 3 }).run();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleBold = (editor: Editor, range?: Range) => {
|
export const toggleBold = (editor: Editor, range?: Range) => {
|
||||||
@ -37,10 +37,10 @@ export const toggleCodeBlock = (editor: Editor, range?: Range) => {
|
|||||||
// Check if code block is active then toggle code block
|
// Check if code block is active then toggle code block
|
||||||
if (editor.isActive("codeBlock")) {
|
if (editor.isActive("codeBlock")) {
|
||||||
if (range) {
|
if (range) {
|
||||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
editor.chain().focus().deleteRange(range).clearNodes().toggleCodeBlock().run();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
editor.chain().focus().toggleCodeBlock().run();
|
editor.chain().focus().clearNodes().toggleCodeBlock().run();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,32 +49,32 @@ export const toggleCodeBlock = (editor: Editor, range?: Range) => {
|
|||||||
|
|
||||||
if (isSelectionEmpty) {
|
if (isSelectionEmpty) {
|
||||||
if (range) {
|
if (range) {
|
||||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
editor.chain().focus().deleteRange(range).clearNodes().toggleCodeBlock().run();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
editor.chain().focus().toggleCodeBlock().run();
|
editor.chain().focus().clearNodes().toggleCodeBlock().run();
|
||||||
} else {
|
} else {
|
||||||
if (range) {
|
if (range) {
|
||||||
editor.chain().focus().deleteRange(range).toggleCode().run();
|
editor.chain().focus().deleteRange(range).clearNodes().toggleCode().run();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
editor.chain().focus().toggleCode().run();
|
editor.chain().focus().clearNodes().toggleCode().run();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleOrderedList = (editor: Editor, range?: Range) => {
|
export const toggleOrderedList = (editor: Editor, range?: Range) => {
|
||||||
if (range) editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
if (range) editor.chain().focus().deleteRange(range).clearNodes().toggleOrderedList().run();
|
||||||
else editor.chain().focus().toggleOrderedList().run();
|
else editor.chain().focus().clearNodes().toggleOrderedList().run();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleBulletList = (editor: Editor, range?: Range) => {
|
export const toggleBulletList = (editor: Editor, range?: Range) => {
|
||||||
if (range) editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
if (range) editor.chain().focus().deleteRange(range).clearNodes().toggleBulletList().run();
|
||||||
else editor.chain().focus().toggleBulletList().run();
|
else editor.chain().focus().clearNodes().toggleBulletList().run();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleTaskList = (editor: Editor, range?: Range) => {
|
export const toggleTaskList = (editor: Editor, range?: Range) => {
|
||||||
if (range) editor.chain().focus().deleteRange(range).toggleTaskList().run();
|
if (range) editor.chain().focus().deleteRange(range).clearNodes().toggleTaskList().run();
|
||||||
else editor.chain().focus().toggleTaskList().run();
|
else editor.chain().focus().clearNodes().toggleTaskList().run();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleStrike = (editor: Editor, range?: Range) => {
|
export const toggleStrike = (editor: Editor, range?: Range) => {
|
||||||
@ -83,8 +83,8 @@ export const toggleStrike = (editor: Editor, range?: Range) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const toggleBlockquote = (editor: Editor, range?: Range) => {
|
export const toggleBlockquote = (editor: Editor, range?: Range) => {
|
||||||
if (range) editor.chain().focus().deleteRange(range).toggleBlockquote().run();
|
if (range) editor.chain().focus().deleteRange(range).clearNodes().toggleBlockquote().run();
|
||||||
else editor.chain().focus().toggleBlockquote().run();
|
else editor.chain().focus().clearNodes().toggleBlockquote().run();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const insertTableCommand = (editor: Editor, range?: Range) => {
|
export const insertTableCommand = (editor: Editor, range?: Range) => {
|
||||||
@ -97,8 +97,8 @@ export const insertTableCommand = (editor: Editor, range?: Range) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (range) editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3 }).run();
|
if (range) editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3 }).run();
|
||||||
else editor.chain().focus().insertTable({ rows: 3, cols: 3 }).run();
|
else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3 }).run();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unsetLinkEditor = (editor: Editor) => {
|
export const unsetLinkEditor = (editor: Editor) => {
|
||||||
|
@ -85,7 +85,10 @@ const getSuggestionItems =
|
|||||||
searchTerms: ["p", "paragraph"],
|
searchTerms: ["p", "paragraph"],
|
||||||
icon: <CaseSensitive className="h-3.5 w-3.5" />,
|
icon: <CaseSensitive className="h-3.5 w-3.5" />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
|
if (range) {
|
||||||
|
editor.chain().focus().deleteRange(range).clearNodes().run();
|
||||||
|
}
|
||||||
|
editor.chain().focus().clearNodes().run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -25,16 +25,20 @@ type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
|
|||||||
|
|
||||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||||
const items: BubbleMenuItem[] = [
|
const items: BubbleMenuItem[] = [
|
||||||
|
...(props.editor.isActive("code")
|
||||||
|
? []
|
||||||
|
: [
|
||||||
BoldItem(props.editor),
|
BoldItem(props.editor),
|
||||||
ItalicItem(props.editor),
|
ItalicItem(props.editor),
|
||||||
UnderLineItem(props.editor),
|
UnderLineItem(props.editor),
|
||||||
StrikeThroughItem(props.editor),
|
StrikeThroughItem(props.editor),
|
||||||
|
]),
|
||||||
CodeItem(props.editor),
|
CodeItem(props.editor),
|
||||||
];
|
];
|
||||||
|
|
||||||
const bubbleMenuProps: EditorBubbleMenuProps = {
|
const bubbleMenuProps: EditorBubbleMenuProps = {
|
||||||
...props,
|
...props,
|
||||||
shouldShow: ({ view, state, editor }) => {
|
shouldShow: ({ state, editor }) => {
|
||||||
const { selection } = state;
|
const { selection } = state;
|
||||||
|
|
||||||
const { empty } = selection;
|
const { empty } = selection;
|
||||||
@ -64,6 +68,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
|||||||
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
||||||
|
|
||||||
const [isSelecting, setIsSelecting] = useState(false);
|
const [isSelecting, setIsSelecting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleMouseDown() {
|
function handleMouseDown() {
|
||||||
function handleMouseMove() {
|
function handleMouseMove() {
|
||||||
@ -108,14 +113,16 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!props.editor.isActive("code") && (
|
||||||
<LinkSelector
|
<LinkSelector
|
||||||
editor={props.editor!!}
|
editor={props.editor}
|
||||||
isOpen={isLinkSelectorOpen}
|
isOpen={isLinkSelectorOpen}
|
||||||
setIsOpen={() => {
|
setIsOpen={() => {
|
||||||
setIsLinkSelectorOpen(!isLinkSelectorOpen);
|
setIsLinkSelectorOpen(!isLinkSelectorOpen);
|
||||||
setIsNodeSelectorOpen(false);
|
setIsNodeSelectorOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<button
|
<button
|
||||||
|
@ -84,8 +84,8 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
|
|||||||
className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90"
|
className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
|
||||||
onLinkSubmit();
|
onLinkSubmit();
|
||||||
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
|
@ -26,7 +26,7 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
|
|||||||
{
|
{
|
||||||
name: "Text",
|
name: "Text",
|
||||||
icon: TextIcon,
|
icon: TextIcon,
|
||||||
command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(),
|
command: () => editor.chain().focus().clearNodes().run(),
|
||||||
isActive: () => editor.isActive("paragraph") && !editor.isActive("bulletList") && !editor.isActive("orderedList"),
|
isActive: () => editor.isActive("paragraph") && !editor.isActive("bulletList") && !editor.isActive("orderedList"),
|
||||||
},
|
},
|
||||||
HeadingOneItem(editor),
|
HeadingOneItem(editor),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export type TIssueReaction = {
|
export type TIssueReaction = {
|
||||||
actor_id: string;
|
actor: string;
|
||||||
id: string;
|
id: string;
|
||||||
issue_id: string;
|
issue: string;
|
||||||
reaction: string;
|
reaction: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
openDropdown();
|
isOpen ? closeDropdown() : openDropdown();
|
||||||
if (menuButtonOnClick) menuButtonOnClick();
|
if (menuButtonOnClick) menuButtonOnClick();
|
||||||
}}
|
}}
|
||||||
className={customButtonClassName}
|
className={customButtonClassName}
|
||||||
@ -132,7 +132,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
openDropdown();
|
isOpen ? closeDropdown() : openDropdown();
|
||||||
if (menuButtonOnClick) menuButtonOnClick();
|
if (menuButtonOnClick) menuButtonOnClick();
|
||||||
}}
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -158,7 +158,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
} ${buttonClassName}`}
|
} ${buttonClassName}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
openDropdown();
|
isOpen ? closeDropdown() : openDropdown();
|
||||||
if (menuButtonOnClick) menuButtonOnClick();
|
if (menuButtonOnClick) menuButtonOnClick();
|
||||||
}}
|
}}
|
||||||
tabIndex={customButtonTabIndex}
|
tabIndex={customButtonTabIndex}
|
||||||
|
@ -4,9 +4,19 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// icons
|
||||||
import { FolderPlus, Search, Settings } from "lucide-react";
|
import { FolderPlus, Search, Settings } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useApplication, useEventTracker, useProject } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
|
import useDebounce from "hooks/use-debounce";
|
||||||
|
// services
|
||||||
|
import { IssueService } from "services/issue";
|
||||||
|
import { WorkspaceService } from "services/workspace.service";
|
||||||
|
// ui
|
||||||
import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import { EmptyState } from "components/empty-state";
|
||||||
import {
|
import {
|
||||||
CommandPaletteThemeActions,
|
CommandPaletteThemeActions,
|
||||||
ChangeIssueAssignee,
|
ChangeIssueAssignee,
|
||||||
@ -18,20 +28,15 @@ import {
|
|||||||
CommandPaletteWorkspaceSettingsActions,
|
CommandPaletteWorkspaceSettingsActions,
|
||||||
CommandPaletteSearchResults,
|
CommandPaletteSearchResults,
|
||||||
} from "components/command-palette";
|
} from "components/command-palette";
|
||||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
|
||||||
import { useApplication, useEventTracker, useProject } from "hooks/store";
|
|
||||||
import { usePlatformOS } from "hooks/use-platform-os";
|
|
||||||
// services
|
|
||||||
import useDebounce from "hooks/use-debounce";
|
|
||||||
import { IssueService } from "services/issue";
|
|
||||||
import { WorkspaceService } from "services/workspace.service";
|
|
||||||
// types
|
// types
|
||||||
import { IWorkspaceSearchResults } from "@plane/types";
|
import { IWorkspaceSearchResults } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { E_COMMAND_PALETTE } from "constants/event-tracker";
|
import { E_COMMAND_PALETTE } from "constants/event-tracker";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
|
// constants
|
||||||
|
import { EmptyStateType } from "constants/empty-state";
|
||||||
|
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
// services
|
|
||||||
const workspaceService = new WorkspaceService();
|
const workspaceService = new WorkspaceService();
|
||||||
const issueService = new IssueService();
|
const issueService = new IssueService();
|
||||||
|
|
||||||
@ -246,7 +251,9 @@ export const CommandModal: React.FC = observer(() => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
||||||
<div className="my-4 text-center text-sm text-custom-text-200">No results found.</div>
|
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||||
|
<EmptyState type={EmptyStateType.COMMAND_K_SEARCH_EMPTY_STATE} layout="screen-simple" />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isLoading || isSearching) && (
|
{(isLoading || isSearching) && (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, FC } from "react";
|
import React, { useCallback, useEffect, FC, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -25,24 +25,29 @@ import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store
|
|||||||
import { IssueService } from "services/issue";
|
import { IssueService } from "services/issue";
|
||||||
// constants
|
// constants
|
||||||
import { E_SHORTCUT_KEY } from "constants/event-tracker";
|
import { E_SHORTCUT_KEY } from "constants/event-tracker";
|
||||||
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
const issueService = new IssueService();
|
const issueService = new IssueService();
|
||||||
|
|
||||||
export const CommandPalette: FC = observer(() => {
|
export const CommandPalette: FC = observer(() => {
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query;
|
||||||
|
// store hooks
|
||||||
const {
|
const {
|
||||||
commandPalette,
|
commandPalette,
|
||||||
theme: { toggleSidebar },
|
theme: { toggleSidebar },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
const { currentUser } = useUser();
|
const {
|
||||||
|
currentUser,
|
||||||
|
membership: { currentWorkspaceRole, currentProjectRole },
|
||||||
|
} = useUser();
|
||||||
const {
|
const {
|
||||||
issues: { removeIssue },
|
issues: { removeIssue },
|
||||||
} = useIssues(EIssuesStoreType.PROJECT);
|
} = useIssues(EIssuesStoreType.PROJECT);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
toggleCommandPaletteModal,
|
toggleCommandPaletteModal,
|
||||||
isCreateIssueModalOpen,
|
isCreateIssueModalOpen,
|
||||||
@ -93,6 +98,105 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
});
|
});
|
||||||
}, [issueId]);
|
}, [issueId]);
|
||||||
|
|
||||||
|
// auth
|
||||||
|
const canPerformProjectCreateActions = useCallback(
|
||||||
|
(showToast: boolean = true) => {
|
||||||
|
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
|
if (!isAllowed && showToast)
|
||||||
|
setToast({
|
||||||
|
type: TOAST_TYPE.ERROR,
|
||||||
|
title: "You don't have permission to perform this action.",
|
||||||
|
});
|
||||||
|
|
||||||
|
return isAllowed;
|
||||||
|
},
|
||||||
|
[currentProjectRole]
|
||||||
|
);
|
||||||
|
const canPerformWorkspaceCreateActions = useCallback(
|
||||||
|
(showToast: boolean = true) => {
|
||||||
|
const isAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
console.log("currentWorkspaceRole", currentWorkspaceRole);
|
||||||
|
console.log("isAllowed", isAllowed);
|
||||||
|
if (!isAllowed && showToast)
|
||||||
|
setToast({
|
||||||
|
type: TOAST_TYPE.ERROR,
|
||||||
|
title: "You don't have permission to perform this action.",
|
||||||
|
});
|
||||||
|
return isAllowed;
|
||||||
|
},
|
||||||
|
[currentWorkspaceRole]
|
||||||
|
);
|
||||||
|
|
||||||
|
const shortcutsList: {
|
||||||
|
global: Record<string, { title: string; description: string; action: () => void }>;
|
||||||
|
workspace: Record<string, { title: string; description: string; action: () => void }>;
|
||||||
|
project: Record<string, { title: string; description: string; action: () => void }>;
|
||||||
|
} = useMemo(
|
||||||
|
() => ({
|
||||||
|
global: {
|
||||||
|
c: {
|
||||||
|
title: "Create a new issue",
|
||||||
|
description: "Create a new issue in the current project",
|
||||||
|
action: () => toggleCreateIssueModal(true),
|
||||||
|
},
|
||||||
|
h: {
|
||||||
|
title: "Show shortcuts",
|
||||||
|
description: "Show all the available shortcuts",
|
||||||
|
action: () => toggleShortcutModal(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
p: {
|
||||||
|
title: "Create a new project",
|
||||||
|
description: "Create a new project in the current workspace",
|
||||||
|
action: () => toggleCreateProjectModal(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
d: {
|
||||||
|
title: "Create a new page",
|
||||||
|
description: "Create a new page in the current project",
|
||||||
|
action: () => toggleCreatePageModal(true),
|
||||||
|
},
|
||||||
|
m: {
|
||||||
|
title: "Create a new module",
|
||||||
|
description: "Create a new module in the current project",
|
||||||
|
action: () => toggleCreateModuleModal(true),
|
||||||
|
},
|
||||||
|
q: {
|
||||||
|
title: "Create a new cycle",
|
||||||
|
description: "Create a new cycle in the current project",
|
||||||
|
action: () => toggleCreateCycleModal(true),
|
||||||
|
},
|
||||||
|
v: {
|
||||||
|
title: "Create a new view",
|
||||||
|
description: "Create a new view in the current project",
|
||||||
|
action: () => toggleCreateViewModal(true),
|
||||||
|
},
|
||||||
|
backspace: {
|
||||||
|
title: "Bulk delete issues",
|
||||||
|
description: "Bulk delete issues in the current project",
|
||||||
|
action: () => toggleBulkDeleteIssueModal(true),
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
title: "Bulk delete issues",
|
||||||
|
description: "Bulk delete issues in the current project",
|
||||||
|
action: () => toggleBulkDeleteIssueModal(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
toggleBulkDeleteIssueModal,
|
||||||
|
toggleCreateCycleModal,
|
||||||
|
toggleCreateIssueModal,
|
||||||
|
toggleCreateModuleModal,
|
||||||
|
toggleCreatePageModal,
|
||||||
|
toggleCreateProjectModal,
|
||||||
|
toggleCreateViewModal,
|
||||||
|
toggleShortcutModal,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
const { key, ctrlKey, metaKey, altKey } = e;
|
const { key, ctrlKey, metaKey, altKey } = e;
|
||||||
@ -104,7 +208,7 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
if (
|
if (
|
||||||
e.target instanceof HTMLTextAreaElement ||
|
e.target instanceof HTMLTextAreaElement ||
|
||||||
e.target instanceof HTMLInputElement ||
|
e.target instanceof HTMLInputElement ||
|
||||||
(e.target as Element).classList?.contains("ProseMirror")
|
(e.target as Element)?.classList?.contains("ProseMirror")
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -121,42 +225,37 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
}
|
}
|
||||||
} else if (!isAnyModalOpen) {
|
} else if (!isAnyModalOpen) {
|
||||||
setTrackElement(E_SHORTCUT_KEY);
|
setTrackElement(E_SHORTCUT_KEY);
|
||||||
if (keyPressed === "c") {
|
if (Object.keys(shortcutsList.global).includes(keyPressed)) shortcutsList.global[keyPressed].action();
|
||||||
toggleCreateIssueModal(true);
|
// workspace authorized actions
|
||||||
} else if (keyPressed === "p") {
|
else if (
|
||||||
toggleCreateProjectModal(true);
|
Object.keys(shortcutsList.workspace).includes(keyPressed) &&
|
||||||
} else if (keyPressed === "h") {
|
workspaceSlug &&
|
||||||
toggleShortcutModal(true);
|
canPerformWorkspaceCreateActions()
|
||||||
} else if (keyPressed === "v" && workspaceSlug && projectId) {
|
)
|
||||||
toggleCreateViewModal(true);
|
shortcutsList.workspace[keyPressed].action();
|
||||||
} else if (keyPressed === "d" && workspaceSlug && projectId) {
|
// project authorized actions
|
||||||
toggleCreatePageModal(true);
|
else if (
|
||||||
} else if (keyPressed === "q" && workspaceSlug && projectId) {
|
Object.keys(shortcutsList.project).includes(keyPressed) &&
|
||||||
toggleCreateCycleModal(true);
|
projectId &&
|
||||||
} else if (keyPressed === "m" && workspaceSlug && projectId) {
|
canPerformProjectCreateActions()
|
||||||
toggleCreateModuleModal(true);
|
) {
|
||||||
} else if (keyPressed === "backspace" || keyPressed === "delete") {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toggleBulkDeleteIssueModal(true);
|
// actions that can be performed only inside a project
|
||||||
|
shortcutsList.project[keyPressed].action();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
canPerformProjectCreateActions,
|
||||||
|
canPerformWorkspaceCreateActions,
|
||||||
copyIssueUrlToClipboard,
|
copyIssueUrlToClipboard,
|
||||||
toggleCreateProjectModal,
|
isAnyModalOpen,
|
||||||
toggleCreateViewModal,
|
projectId,
|
||||||
toggleCreatePageModal,
|
setTrackElement,
|
||||||
toggleShortcutModal,
|
shortcutsList,
|
||||||
toggleCreateCycleModal,
|
|
||||||
toggleCreateModuleModal,
|
|
||||||
toggleBulkDeleteIssueModal,
|
|
||||||
toggleCommandPaletteModal,
|
toggleCommandPaletteModal,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
toggleCreateIssueModal,
|
|
||||||
projectId,
|
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
isAnyModalOpen,
|
|
||||||
setTrackElement,
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -171,18 +270,11 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShortcutsModal
|
<ShortcutsModal isOpen={isShortcutModalOpen} onClose={() => toggleShortcutModal(false)} />
|
||||||
isOpen={isShortcutModalOpen}
|
|
||||||
onClose={() => {
|
|
||||||
toggleShortcutModal(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{workspaceSlug && (
|
{workspaceSlug && (
|
||||||
<CreateProjectModal
|
<CreateProjectModal
|
||||||
isOpen={isCreateProjectModalOpen}
|
isOpen={isCreateProjectModalOpen}
|
||||||
onClose={() => {
|
onClose={() => toggleCreateProjectModal(false)}
|
||||||
toggleCreateProjectModal(false);
|
|
||||||
}}
|
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -196,9 +288,7 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
/>
|
/>
|
||||||
<CreateUpdateModuleModal
|
<CreateUpdateModuleModal
|
||||||
isOpen={isCreateModuleModalOpen}
|
isOpen={isCreateModuleModalOpen}
|
||||||
onClose={() => {
|
onClose={() => toggleCreateModuleModal(false)}
|
||||||
toggleCreateModuleModal(false);
|
|
||||||
}}
|
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
/>
|
/>
|
||||||
@ -238,9 +328,7 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
|
|
||||||
<BulkDeleteIssuesModal
|
<BulkDeleteIssuesModal
|
||||||
isOpen={isBulkDeleteIssueModalOpen}
|
isOpen={isBulkDeleteIssueModalOpen}
|
||||||
onClose={() => {
|
onClose={() => toggleBulkDeleteIssueModal(false)}
|
||||||
toggleBulkDeleteIssueModal(false);
|
|
||||||
}}
|
|
||||||
user={currentUser}
|
user={currentUser}
|
||||||
/>
|
/>
|
||||||
<CommandModal />
|
<CommandModal />
|
||||||
|
@ -5,22 +5,22 @@ import { SubmitHandler, useForm } from "react-hook-form";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import { Search } from "lucide-react";
|
|
||||||
import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
|
||||||
|
|
||||||
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
|
||||||
import { useIssues, useProject } from "hooks/store";
|
|
||||||
import { IssueService } from "services/issue";
|
import { IssueService } from "services/issue";
|
||||||
// ui
|
// ui
|
||||||
|
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
|
import { Search } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IUser, TIssue } from "@plane/types";
|
import { IUser, TIssue } from "@plane/types";
|
||||||
// fetch keys
|
|
||||||
// store hooks
|
// store hooks
|
||||||
|
import { useIssues, useProject } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item";
|
import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item";
|
||||||
|
import { EmptyState } from "components/empty-state";
|
||||||
// constants
|
// constants
|
||||||
|
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||||
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
import { EmptyStateType } from "constants/empty-state";
|
||||||
|
|
||||||
type FormInput = {
|
type FormInput = {
|
||||||
delete_issue_ids: string[];
|
delete_issue_ids: string[];
|
||||||
@ -178,12 +178,15 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||||
<LayersIcon height="56" width="56" />
|
<EmptyState
|
||||||
<h3 className="text-custom-text-200">
|
type={
|
||||||
No issues found. Create a new issue with{" "}
|
query === ""
|
||||||
<pre className="inline rounded bg-custom-background-80 px-2 py-1">C</pre>.
|
? EmptyStateType.ISSUE_RELATION_EMPTY_STATE
|
||||||
</h3>
|
: EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE
|
||||||
|
}
|
||||||
|
layout="screen-simple"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Combobox.Options>
|
</Combobox.Options>
|
||||||
|
@ -2,12 +2,14 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
import { Rocket, Search, X } from "lucide-react";
|
import { Rocket, Search, X } from "lucide-react";
|
||||||
// services
|
// services
|
||||||
import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
import { ProjectService } from "services/project";
|
||||||
|
// hooks
|
||||||
import useDebounce from "hooks/use-debounce";
|
import useDebounce from "hooks/use-debounce";
|
||||||
import { usePlatformOS } from "hooks/use-platform-os";
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
import { ProjectService } from "services/project";
|
// components
|
||||||
|
import { IssueSearchModalEmptyState } from "./issue-search-modal-empty-state";
|
||||||
// ui
|
// ui
|
||||||
|
import { Button, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
|
import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
|
||||||
|
|
||||||
@ -192,15 +194,12 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
|||||||
</h5>
|
</h5>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isSearching && issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
<IssueSearchModalEmptyState
|
||||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
debouncedSearchTerm={debouncedSearchTerm}
|
||||||
<LayersIcon height="52" width="52" />
|
isSearching={isSearching}
|
||||||
<h3 className="text-custom-text-200">
|
issues={issues}
|
||||||
No issues found. Create a new issue with{" "}
|
searchTerm={searchTerm}
|
||||||
<pre className="inline rounded bg-custom-background-80 px-2 py-1 text-sm">C</pre>.
|
/>
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isSearching ? (
|
{isSearching ? (
|
||||||
<Loader className="space-y-3 p-3">
|
<Loader className="space-y-3 p-3">
|
||||||
|
@ -4,3 +4,4 @@ export * from "./gpt-assistant-popover";
|
|||||||
export * from "./link-modal";
|
export * from "./link-modal";
|
||||||
export * from "./user-image-upload-modal";
|
export * from "./user-image-upload-modal";
|
||||||
export * from "./workspace-image-upload-modal";
|
export * from "./workspace-image-upload-modal";
|
||||||
|
export * from "./issue-search-modal-empty-state";
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
import React from "react";
|
||||||
|
// components
|
||||||
|
import { EmptyState } from "components/empty-state";
|
||||||
|
// types
|
||||||
|
import { ISearchIssueResponse } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { EmptyStateType } from "constants/empty-state";
|
||||||
|
|
||||||
|
interface EmptyStateProps {
|
||||||
|
issues: ISearchIssueResponse[];
|
||||||
|
searchTerm: string;
|
||||||
|
debouncedSearchTerm: string;
|
||||||
|
isSearching: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IssueSearchModalEmptyState: React.FC<EmptyStateProps> = ({
|
||||||
|
issues,
|
||||||
|
searchTerm,
|
||||||
|
debouncedSearchTerm,
|
||||||
|
isSearching,
|
||||||
|
}) => {
|
||||||
|
const renderEmptyState = (type: EmptyStateType) => (
|
||||||
|
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||||
|
<EmptyState type={type} layout="screen-simple" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const emptyState =
|
||||||
|
issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && !isSearching
|
||||||
|
? renderEmptyState(EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE)
|
||||||
|
: issues.length === 0
|
||||||
|
? renderEmptyState(EmptyStateType.ISSUE_RELATION_EMPTY_STATE)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return emptyState;
|
||||||
|
};
|
@ -22,12 +22,14 @@ export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
|
|||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<div className="flex h-full w-full justify-between">
|
<div className="flex h-full w-full justify-between">
|
||||||
<div className="h-full w-full flex flex-col p-8 space-y-8 vertical-scrollbar scrollbar-lg">
|
<div className="h-full w-full flex flex-col p-8 space-y-8 vertical-scrollbar scrollbar-lg">
|
||||||
|
{cycleIds.length > 0 && (
|
||||||
<CyclesBoardMap
|
<CyclesBoardMap
|
||||||
cycleIds={cycleIds}
|
cycleIds={cycleIds}
|
||||||
peekCycle={peekCycle}
|
peekCycle={peekCycle}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{completedCycleIds.length !== 0 && (
|
{completedCycleIds.length !== 0 && (
|
||||||
<Disclosure as="div" className="space-y-4">
|
<Disclosure as="div" className="space-y-4">
|
||||||
<Disclosure.Button className="bg-custom-background-80 font-semibold text-sm py-1 px-2 rounded flex items-center gap-1">
|
<Disclosure.Button className="bg-custom-background-80 font-semibold text-sm py-1 px-2 rounded flex items-center gap-1">
|
||||||
|
@ -109,7 +109,7 @@ export const CycleMobileHeader = () => {
|
|||||||
onClose={() => setAnalyticsModal(false)}
|
onClose={() => setAnalyticsModal(false)}
|
||||||
cycleDetails={cycleDetails ?? undefined}
|
cycleDetails={cycleDetails ?? undefined}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-evenly py-2 border-b border-custom-border-200">
|
<div className="flex justify-evenly py-2 border-b border-custom-border-200 md:hidden bg-custom-background-100">
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
maxHeight={"md"}
|
maxHeight={"md"}
|
||||||
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||||
|
@ -38,7 +38,7 @@ export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspa
|
|||||||
{peekCycle && (
|
{peekCycle && (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="flex h-full w-full max-w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-3.5 duration-300 fixed md:relative right-0 z-[9]"
|
className="flex h-full w-full max-w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 duration-300 fixed md:relative right-0 z-[9]"
|
||||||
style={{
|
style={{
|
||||||
boxShadow:
|
boxShadow:
|
||||||
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
|
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
|
||||||
|
52
web/components/cycles/cycles-list-mobile-header.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { observer } from "mobx-react";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu } from "@plane/ui";
|
||||||
|
// icon
|
||||||
|
import { List } from "lucide-react";
|
||||||
|
// constants
|
||||||
|
import { CYCLE_VIEW_LAYOUTS } from "constants/cycle";
|
||||||
|
// hooks
|
||||||
|
import { useCycleFilter, useProject } from "hooks/store";
|
||||||
|
|
||||||
|
const CyclesListMobileHeader = observer(() => {
|
||||||
|
const { currentProjectDetails } = useProject();
|
||||||
|
// hooks
|
||||||
|
const { updateDisplayFilters } = useCycleFilter();
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center sm:hidden">
|
||||||
|
<CustomMenu
|
||||||
|
maxHeight={"md"}
|
||||||
|
className="flex flex-grow justify-center text-custom-text-200 text-sm py-2 border-b border-custom-border-200 bg-custom-sidebar-background-100"
|
||||||
|
// placement="bottom-start"
|
||||||
|
customButton={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<List className="h-4 w-4" />
|
||||||
|
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm"
|
||||||
|
closeOnSelect
|
||||||
|
>
|
||||||
|
{CYCLE_VIEW_LAYOUTS.map((layout) => {
|
||||||
|
if (layout.key == "gantt") return;
|
||||||
|
return (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={layout.key}
|
||||||
|
onClick={() => {
|
||||||
|
updateDisplayFilters(currentProjectDetails!.id, {
|
||||||
|
layout: layout.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>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default CyclesListMobileHeader;
|
@ -42,6 +42,8 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||||||
useOutsideClickDetector(inputRef, () => {
|
useOutsideClickDetector(inputRef, () => {
|
||||||
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||||
});
|
});
|
||||||
|
// derived values
|
||||||
|
const activeLayout = currentProjectDisplayFilters?.layout ?? "list";
|
||||||
|
|
||||||
const handleFilters = useCallback(
|
const handleFilters = useCallback(
|
||||||
(key: keyof TCycleFilters, value: string | string[]) => {
|
(key: keyof TCycleFilters, value: string | string[]) => {
|
||||||
@ -140,9 +142,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
||||||
currentProjectDisplayFilters?.layout == layout.key
|
activeLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
||||||
? "bg-custom-background-100 shadow-custom-shadow-2xs"
|
|
||||||
: ""
|
|
||||||
}`}
|
}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
updateDisplayFilters(projectId, {
|
updateDisplayFilters(projectId, {
|
||||||
@ -153,9 +153,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||||||
<layout.icon
|
<layout.icon
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
className={`h-3.5 w-3.5 ${
|
className={`h-3.5 w-3.5 ${
|
||||||
currentProjectDisplayFilters?.layout == layout.key
|
activeLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
? "text-custom-text-100"
|
|
||||||
: "text-custom-text-200"
|
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -6,6 +6,7 @@ import { DateRangeDropdown, ProjectDropdown } from "components/dropdowns";
|
|||||||
// ui
|
// ui
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||||
|
import { shouldRenderProject } from "helpers/project.helper";
|
||||||
// types
|
// types
|
||||||
import { ICycle } from "@plane/types";
|
import { ICycle } from "@plane/types";
|
||||||
|
|
||||||
@ -66,6 +67,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
|||||||
setActiveProject(val);
|
setActiveProject(val);
|
||||||
}}
|
}}
|
||||||
buttonVariant="background-with-text"
|
buttonVariant="background-with-text"
|
||||||
|
renderCondition={(project) => shouldRenderProject(project)}
|
||||||
tabIndex={7}
|
tabIndex={7}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -178,7 +178,11 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
|||||||
<Info className="h-4 w-4 text-custom-text-400" />
|
<Info className="h-4 w-4 text-custom-text-400" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
||||||
|
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex w-full flex-shrink-0 items-center justify-between gap-2.5 overflow-hidden md:w-auto md:flex-shrink-0 md:justify-end">
|
||||||
{currentCycle && (
|
{currentCycle && (
|
||||||
<div
|
<div
|
||||||
className="relative flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs"
|
className="relative flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs"
|
||||||
@ -192,11 +196,6 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
|||||||
: `${currentCycle.label}`}
|
: `${currentCycle.label}`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<div className="relative flex w-full flex-shrink-0 items-center justify-between gap-2.5 overflow-hidden md:w-auto md:flex-shrink-0 md:justify-end">
|
|
||||||
<div className="text-xs text-custom-text-300">
|
|
||||||
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative flex flex-shrink-0 items-center gap-3">
|
<div className="relative flex flex-shrink-0 items-center gap-3">
|
||||||
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
|
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
|
||||||
|
@ -15,6 +15,7 @@ import { ProjectLogo } from "components/project";
|
|||||||
// types
|
// types
|
||||||
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||||
import { TDropdownProps } from "./types";
|
import { TDropdownProps } from "./types";
|
||||||
|
import { IProject } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
|
|
||||||
type Props = TDropdownProps & {
|
type Props = TDropdownProps & {
|
||||||
@ -23,6 +24,7 @@ type Props = TDropdownProps & {
|
|||||||
dropdownArrowClassName?: string;
|
dropdownArrowClassName?: string;
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
renderCondition?: (project: IProject) => boolean;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,6 +43,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
onClose,
|
onClose,
|
||||||
placeholder = "Project",
|
placeholder = "Project",
|
||||||
placement,
|
placement,
|
||||||
|
renderCondition,
|
||||||
showTooltip = false,
|
showTooltip = false,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
value,
|
value,
|
||||||
@ -71,7 +74,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const options = joinedProjectIds?.map((projectId) => {
|
const options = joinedProjectIds?.map((projectId) => {
|
||||||
const projectDetails = getProjectById(projectId);
|
const projectDetails = getProjectById(projectId);
|
||||||
|
if (renderCondition && projectDetails && !renderCondition(projectDetails)) return;
|
||||||
return {
|
return {
|
||||||
value: projectId,
|
value: projectId,
|
||||||
query: `${projectDetails?.name}`,
|
query: `${projectDetails?.name}`,
|
||||||
@ -89,7 +92,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const filteredOptions =
|
const filteredOptions =
|
||||||
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
|
query === "" ? options : options?.filter((o) => o?.query.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
|
||||||
const selectedProject = value ? getProjectById(value) : null;
|
const selectedProject = value ? getProjectById(value) : null;
|
||||||
|
|
||||||
@ -205,7 +208,9 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||||
{filteredOptions ? (
|
{filteredOptions ? (
|
||||||
filteredOptions.length > 0 ? (
|
filteredOptions.length > 0 ? (
|
||||||
filteredOptions.map((option) => (
|
filteredOptions.map((option) => {
|
||||||
|
if (!option) return;
|
||||||
|
return (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
@ -222,7 +227,8 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
|
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
|
||||||
)
|
)
|
||||||
|
@ -16,7 +16,7 @@ import { cn } from "helpers/common.helper";
|
|||||||
export type EmptyStateProps = {
|
export type EmptyStateProps = {
|
||||||
type: EmptyStateType;
|
type: EmptyStateType;
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
layout?: "widget-simple" | "screen-detailed" | "screen-simple";
|
layout?: "screen-detailed" | "screen-simple";
|
||||||
additionalPath?: string;
|
additionalPath?: string;
|
||||||
primaryButtonOnClick?: () => void;
|
primaryButtonOnClick?: () => void;
|
||||||
primaryButtonLink?: string;
|
primaryButtonLink?: string;
|
||||||
@ -149,6 +149,28 @@ export const EmptyState: React.FC<EmptyStateProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{layout === "screen-simple" && (
|
||||||
|
<div className="text-center flex flex-col gap-2.5 items-center">
|
||||||
|
<div className="h-28 w-28">
|
||||||
|
<Image
|
||||||
|
src={resolvedEmptyStatePath}
|
||||||
|
alt={key || "button image"}
|
||||||
|
width={96}
|
||||||
|
height={96}
|
||||||
|
layout="responsive"
|
||||||
|
lazyBoundary="100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{description ? (
|
||||||
|
<>
|
||||||
|
<h3 className="text-lg font-medium text-custom-text-300 whitespace-pre-line">{title}</h3>
|
||||||
|
<p className="text-base font-medium text-custom-text-400 whitespace-pre-line">{description}</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<h3 className="text-sm font-medium text-custom-text-400 whitespace-pre-line">{title}</h3>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -9,8 +9,6 @@ import { ArrowRight, Plus, PanelRight } from "lucide-react";
|
|||||||
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui";
|
||||||
import { ProjectAnalyticsModal } from "components/analytics";
|
import { ProjectAnalyticsModal } from "components/analytics";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { CycleMobileHeader } from "components/cycles/cycle-mobile-header";
|
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
@ -206,9 +204,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
cycleDetails={cycleDetails ?? undefined}
|
cycleDetails={cycleDetails ?? undefined}
|
||||||
/>
|
/>
|
||||||
<div className="relative z-[15] w-full items-center gap-x-2 gap-y-4">
|
<div className="relative z-[15] w-full items-center gap-x-2 gap-y-4">
|
||||||
<div className="flex justify-between border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="flex justify-between bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<Breadcrumbs onBack={router.back}>
|
<Breadcrumbs onBack={router.back}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
@ -258,8 +255,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
{issueCount && issueCount > 0 ? (
|
{issueCount && issueCount > 0 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
tooltipContent={`There are ${issueCount} ${
|
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"
|
||||||
issueCount > 1 ? "issues" : "issue"
|
|
||||||
} in this cycle`}
|
} in this cycle`}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
@ -356,9 +352,6 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
<PanelRight className={cn("w-4 h-4", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
|
<PanelRight className={cn("w-4 h-4", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="block sm:block md:hidden">
|
|
||||||
<CycleMobileHeader />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
import { FC, useCallback } from "react";
|
import { FC } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { List, Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui";
|
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { CYCLE_VIEW_LAYOUTS } from "constants/cycle";
|
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
|
||||||
import { TCycleLayoutOptions } from "@plane/types";
|
|
||||||
import { ProjectLogo } from "components/project";
|
import { ProjectLogo } from "components/project";
|
||||||
// constants
|
// constants
|
||||||
import { E_CYCLES } from "constants/event-tracker";
|
import { E_CYCLES } from "constants/event-tracker";
|
||||||
@ -35,20 +31,11 @@ export const CyclesHeader: FC = observer(() => {
|
|||||||
const canUserCreateCycle =
|
const canUserCreateCycle =
|
||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
const { setValue: setCycleLayout } = useLocalStorage<TCycleLayoutOptions>("cycle_layout", "list");
|
|
||||||
|
|
||||||
const handleCurrentLayout = useCallback(
|
|
||||||
(_layout: TCycleLayoutOptions) => {
|
|
||||||
setCycleLayout(_layout);
|
|
||||||
},
|
|
||||||
[setCycleLayout]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 items-center justify-between gap-x-2 gap-y-4">
|
<div className="relative z-10 items-center justify-between gap-x-2 gap-y-4">
|
||||||
<div className="flex border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="flex bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs onBack={router.back}>
|
<Breadcrumbs onBack={router.back}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
@ -92,35 +79,6 @@ export const CyclesHeader: FC = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center sm:hidden">
|
|
||||||
<CustomMenu
|
|
||||||
maxHeight={"md"}
|
|
||||||
className="flex flex-grow justify-center text-custom-text-200 text-sm py-2 border-b border-custom-border-200 bg-custom-sidebar-background-100"
|
|
||||||
// placement="bottom-start"
|
|
||||||
customButton={
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<List className="h-4 w-4" />
|
|
||||||
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm"
|
|
||||||
closeOnSelect
|
|
||||||
>
|
|
||||||
{CYCLE_VIEW_LAYOUTS.map((layout) => (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
key={layout.key}
|
|
||||||
onClick={() => {
|
|
||||||
// handleLayoutChange(ISSUE_LAYOUTS[index].key);
|
|
||||||
handleCurrentLayout(layout.key as TCycleLayoutOptions);
|
|
||||||
}}
|
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,6 @@ import { usePlatformOS } from "hooks/use-platform-os";
|
|||||||
import { List, PlusIcon, Sheet } from "lucide-react";
|
import { List, PlusIcon, Sheet } from "lucide-react";
|
||||||
import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateWorkspaceViewModal } from "components/workspace";
|
import { CreateUpdateWorkspaceViewModal } from "components/workspace";
|
||||||
@ -147,9 +146,8 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
|
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
|
||||||
<div className="relative z-[15] flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-[15] flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="relative flex gap-2">
|
<div className="relative flex gap-2">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
@ -175,8 +173,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
|
|||||||
<span>
|
<span>
|
||||||
<Tooltip tooltipContent={layout.title} isMobile={isMobile}>
|
<Tooltip tooltipContent={layout.title} isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${activeLayout === layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
||||||
activeLayout === layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<layout.icon
|
<layout.icon
|
||||||
|
@ -8,9 +8,7 @@ import { ArrowRight, PanelRight, Plus } from "lucide-react";
|
|||||||
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui";
|
||||||
import { ProjectAnalyticsModal } from "components/analytics";
|
import { ProjectAnalyticsModal } from "components/analytics";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||||
import { ModuleMobileHeader } from "components/modules/module-mobile-header";
|
|
||||||
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
@ -206,9 +204,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
moduleDetails={moduleDetails ?? undefined}
|
moduleDetails={moduleDetails ?? undefined}
|
||||||
/>
|
/>
|
||||||
<div className="relative z-[15] items-center gap-x-2 gap-y-4">
|
<div className="relative z-[15] items-center gap-x-2 gap-y-4">
|
||||||
<div className="flex justify-between border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="flex justify-between bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<Breadcrumbs onBack={router.back}>
|
<Breadcrumbs onBack={router.back}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
@ -258,8 +255,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
{issueCount && issueCount > 0 ? (
|
{issueCount && issueCount > 0 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
tooltipContent={`There are ${issueCount} ${
|
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"
|
||||||
issueCount > 1 ? "issues" : "issue"
|
|
||||||
} in this module`}
|
} in this module`}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
@ -365,7 +361,6 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ModuleMobileHeader />
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { GanttChartSquare, LayoutGrid, List, ListFilter, Plus, Search, X } from "lucide-react";
|
import { ListFilter, Plus, Search, X } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useEventTracker, useMember, useModuleFilter, useProject, useUser } from "hooks/store";
|
import { useApplication, useEventTracker, useMember, useModuleFilter, useProject, useUser } from "hooks/store";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { ProjectLogo } from "components/project";
|
import { ProjectLogo } from "components/project";
|
||||||
// constants
|
// constants
|
||||||
import { MODULE_VIEW_LAYOUTS } from "constants/module";
|
import { MODULE_VIEW_LAYOUTS } from "constants/module";
|
||||||
@ -18,7 +17,7 @@ import { usePlatformOS } from "hooks/use-platform-os";
|
|||||||
import { ModuleFiltersSelection, ModuleOrderByDropdown } from "components/modules";
|
import { ModuleFiltersSelection, ModuleOrderByDropdown } from "components/modules";
|
||||||
import { FiltersDropdown } from "components/issues";
|
import { FiltersDropdown } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, Tooltip, DiceIcon, CustomMenu } from "@plane/ui";
|
import { Breadcrumbs, Button, Tooltip, DiceIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
@ -90,10 +89,8 @@ export const ModulesListHeader: React.FC = observer(() => {
|
|||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs onBack={router.back}>
|
<Breadcrumbs onBack={router.back}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
@ -226,41 +223,5 @@ export const ModulesListHeader: React.FC = observer(() => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center md:hidden">
|
|
||||||
<CustomMenu
|
|
||||||
maxHeight={"md"}
|
|
||||||
className="flex flex-grow justify-center text-custom-text-200 text-sm py-2 border-b border-custom-border-200 bg-custom-sidebar-background-100"
|
|
||||||
// placement="bottom-start"
|
|
||||||
customButton={
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
{displayFilters?.layout === "gantt" ? (
|
|
||||||
<GanttChartSquare className="w-3 h-3" />
|
|
||||||
) : displayFilters?.layout === "board" ? (
|
|
||||||
<LayoutGrid className="w-3 h-3" />
|
|
||||||
) : (
|
|
||||||
<List className="w-3 h-3" />
|
|
||||||
)}
|
|
||||||
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm"
|
|
||||||
closeOnSelect
|
|
||||||
>
|
|
||||||
{MODULE_VIEW_LAYOUTS.map((layout) => (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
key={layout.key}
|
|
||||||
onClick={() => {
|
|
||||||
if (!projectId) return;
|
|
||||||
updateDisplayFilters(projectId.toString(), { layout: layout.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>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,6 @@ import { FileText, Plus } from "lucide-react";
|
|||||||
import { Breadcrumbs, Button } from "@plane/ui";
|
import { Breadcrumbs, Button } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
// components
|
// components
|
||||||
import { useApplication, usePage, useProject } from "hooks/store";
|
import { useApplication, usePage, useProject } from "hooks/store";
|
||||||
import { ProjectLogo } from "components/project";
|
import { ProjectLogo } from "components/project";
|
||||||
@ -28,9 +27,8 @@ export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
|
|||||||
const pageDetails = usePage(pageId as string);
|
const pageDetails = usePage(pageId as string);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
@ -6,7 +6,6 @@ import { FileText, Plus } from "lucide-react";
|
|||||||
import { Breadcrumbs, Button } from "@plane/ui";
|
import { Breadcrumbs, Button } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
// components
|
// components
|
||||||
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
||||||
@ -32,9 +31,8 @@ export const PagesHeader = observer(() => {
|
|||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
@ -5,7 +5,6 @@ import useSWR from "swr";
|
|||||||
// hooks
|
// hooks
|
||||||
import { Breadcrumbs, LayersIcon } from "@plane/ui";
|
import { Breadcrumbs, LayersIcon } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||||
import { useProject } from "hooks/store";
|
import { useProject } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
@ -40,9 +39,8 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ArrowLeft } from "lucide-react";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { usePlatformOS } from "hooks/use-platform-os";
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// constants
|
// constants
|
||||||
@ -9,7 +8,6 @@ import { usePlatformOS } from "hooks/use-platform-os";
|
|||||||
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues";
|
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues";
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||||
// helpers
|
// helpers
|
||||||
@ -77,20 +75,10 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-14 w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-14 w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div className="block md:hidden">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
>
|
|
||||||
<ArrowLeft fontSize={14} strokeWidth={2} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<Breadcrumbs>
|
<Breadcrumbs onBack={router.back}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
link={
|
link={
|
||||||
|
@ -6,7 +6,7 @@ import { usePlatformOS } from "hooks/use-platform-os";
|
|||||||
// components
|
// components
|
||||||
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
// helper
|
// helper
|
||||||
@ -82,9 +82,8 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
@ -7,7 +7,6 @@ import { Plus } from "lucide-react";
|
|||||||
import { Breadcrumbs, Button, LayersIcon } from "@plane/ui";
|
import { Breadcrumbs, Button, LayersIcon } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { CreateInboxIssueModal } from "components/inbox";
|
import { CreateInboxIssueModal } from "components/inbox";
|
||||||
// helper
|
// helper
|
||||||
import { useProject } from "hooks/store";
|
import { useProject } from "hooks/store";
|
||||||
@ -23,9 +22,8 @@ export const ProjectInboxHeader: FC = observer(() => {
|
|||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
@ -5,7 +5,6 @@ import { useRouter } from "next/router";
|
|||||||
import { PanelRight } from "lucide-react";
|
import { PanelRight } from "lucide-react";
|
||||||
import { Breadcrumbs, LayersIcon } from "@plane/ui";
|
import { Breadcrumbs, LayersIcon } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
import { useApplication, useIssueDetail, useProject } from "hooks/store";
|
import { useApplication, useIssueDetail, useProject } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
@ -30,9 +29,8 @@ export const ProjectIssueDetailsHeader: FC = observer(() => {
|
|||||||
const isSidebarCollapsed = themeStore.issueDetailSidebarCollapsed;
|
const isSidebarCollapsed = themeStore.issueDetailSidebarCollapsed;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs onBack={router.back}>
|
<Breadcrumbs onBack={router.back}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
@ -7,9 +7,7 @@ import { usePlatformOS } from "hooks/use-platform-os";
|
|||||||
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
|
||||||
import { ProjectAnalyticsModal } from "components/analytics";
|
import { ProjectAnalyticsModal } from "components/analytics";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||||
import { IssuesMobileHeader } from "components/issues/issues-mobile-header";
|
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import {
|
import {
|
||||||
@ -163,9 +161,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
projectDetails={currentProjectDetails ?? undefined}
|
projectDetails={currentProjectDetails ?? undefined}
|
||||||
/>
|
/>
|
||||||
<div className="relative z-[15] items-center gap-x-2 gap-y-4">
|
<div className="relative z-[15] items-center gap-x-2 gap-y-4">
|
||||||
<div className="flex items-center gap-2 p-4 border-b border-custom-border-200 bg-custom-sidebar-background-100">
|
<div className="flex items-center gap-2 p-4 bg-custom-sidebar-background-100">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<Breadcrumbs onBack={() => router.back()}>
|
<Breadcrumbs onBack={() => router.back()}>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
@ -287,9 +284,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="block md:hidden">
|
|
||||||
<IssuesMobileHeader />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,6 @@ import { useRouter } from "next/router";
|
|||||||
import { Breadcrumbs, CustomMenu } from "@plane/ui";
|
import { Breadcrumbs, CustomMenu } from "@plane/ui";
|
||||||
// helper
|
// helper
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project";
|
import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProject, useUser } from "hooks/store";
|
import { useProject, useUser } from "hooks/store";
|
||||||
@ -31,8 +30,7 @@ export const ProjectSettingHeader: FC<IProjectSettingHeader> = observer((props)
|
|||||||
if (currentProjectRole && currentProjectRole <= EUserProjectRoles.VIEWER) return null;
|
if (currentProjectRole && currentProjectRole <= EUserProjectRoles.VIEWER) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<div>
|
<div>
|
||||||
<div className="z-50">
|
<div className="z-50">
|
||||||
|
@ -8,7 +8,6 @@ import { Plus } from "lucide-react";
|
|||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
// types
|
// types
|
||||||
@ -175,9 +174,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-[15] flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-[15] flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -5,7 +5,6 @@ import { Plus } from "lucide-react";
|
|||||||
// components
|
// components
|
||||||
import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui";
|
import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
// constants
|
// constants
|
||||||
@ -32,9 +31,8 @@ export const ProjectViewsHeader: React.FC = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
@ -6,7 +6,6 @@ import { useApplication, useEventTracker, useMember, useProject, useProjectFilte
|
|||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button } from "@plane/ui";
|
import { Breadcrumbs, Button } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -78,9 +77,8 @@ export const ProjectsHeader = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
@ -7,7 +7,6 @@ import { ChevronDown, PanelRight } from "lucide-react";
|
|||||||
import { Breadcrumbs, CustomMenu } from "@plane/ui";
|
import { Breadcrumbs, CustomMenu } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
// components
|
// components
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "constants/profile";
|
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "constants/profile";
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
import { useApplication, useUser } from "hooks/store";
|
import { useApplication, useUser } from "hooks/store";
|
||||||
@ -35,9 +34,8 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
|||||||
const { theme: themStore } = useApplication();
|
const { theme: themStore } = useApplication();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div className="flex justify-between w-full">
|
<div className="flex justify-between w-full">
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
@ -3,13 +3,11 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { Crown } from "lucide-react";
|
import { Crown } from "lucide-react";
|
||||||
import { Breadcrumbs, ContrastIcon } from "@plane/ui";
|
import { Breadcrumbs, ContrastIcon } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
// icons
|
// icons
|
||||||
|
|
||||||
export const WorkspaceActiveCycleHeader = observer(() => (
|
export const WorkspaceActiveCycleHeader = observer(() => (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
@ -6,7 +6,6 @@ import { BarChart2, PanelRight } from "lucide-react";
|
|||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
import { useApplication } from "hooks/store";
|
import { useApplication } from "hooks/store";
|
||||||
|
|
||||||
@ -34,10 +33,9 @@ export const WorkspaceAnalyticsHeader = observer(() => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
|
className={`relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4`}
|
||||||
>
|
>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
@ -8,7 +8,6 @@ import githubWhiteImage from "/public/logos/github-white.png";
|
|||||||
// components
|
// components
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
// constants
|
// constants
|
||||||
import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "constants/event-tracker";
|
import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "constants/event-tracker";
|
||||||
import { useEventTracker } from "hooks/store";
|
import { useEventTracker } from "hooks/store";
|
||||||
@ -20,9 +19,8 @@ export const WorkspaceDashboardHeader = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative z-[15] flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-[15] flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
@ -7,7 +7,6 @@ import { Breadcrumbs, CustomMenu } from "@plane/ui";
|
|||||||
// hooks
|
// hooks
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
|
||||||
import { WORKSPACE_SETTINGS_LINKS } from "constants/workspace";
|
import { WORKSPACE_SETTINGS_LINKS } from "constants/workspace";
|
||||||
|
|
||||||
export interface IWorkspaceSettingHeader {
|
export interface IWorkspaceSettingHeader {
|
||||||
@ -23,7 +22,6 @@ export const WorkspaceSettingHeader: FC<IWorkspaceSettingHeader> = observer((pro
|
|||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
|
@ -2,15 +2,19 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
|
// hooks
|
||||||
|
import { useProject, useProjectState } from "hooks/store";
|
||||||
// icons
|
// icons
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
|
// components
|
||||||
|
import { EmptyState } from "components/empty-state";
|
||||||
// ui
|
// ui
|
||||||
import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// fetch-keys
|
|
||||||
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
|
||||||
import { useProject, useProjectState } from "hooks/store";
|
|
||||||
// services
|
// services
|
||||||
import { IssueService } from "services/issue";
|
import { IssueService } from "services/issue";
|
||||||
|
// constants
|
||||||
|
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||||
|
import { EmptyStateType } from "constants/empty-state";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -158,12 +162,15 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||||
<LayersIcon height="56" width="56" />
|
<EmptyState
|
||||||
<h3 className="text-sm text-custom-text-200">
|
type={
|
||||||
No issues found. Create a new issue with{" "}
|
query === ""
|
||||||
<pre className="inline rounded bg-custom-background-80 px-2 py-1">C</pre>.
|
? EmptyStateType.ISSUE_RELATION_EMPTY_STATE
|
||||||
</h3>
|
: EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE
|
||||||
|
}
|
||||||
|
layout="screen-simple"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Combobox.Options>
|
</Combobox.Options>
|
||||||
|
@ -21,16 +21,14 @@ export const IssueAttachmentsList: FC<TIssueAttachmentsList> = observer((props)
|
|||||||
const {
|
const {
|
||||||
attachment: { getAttachmentsByIssueId },
|
attachment: { getAttachmentsByIssueId },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
// derived values
|
||||||
const issueAttachments = getAttachmentsByIssueId(issueId);
|
const issueAttachments = getAttachmentsByIssueId(issueId);
|
||||||
|
|
||||||
if (!issueAttachments) return <></>;
|
if (!issueAttachments) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{issueAttachments &&
|
{issueAttachments?.map((attachmentId) => (
|
||||||
issueAttachments.length > 0 &&
|
|
||||||
issueAttachments.map((attachmentId) => (
|
|
||||||
<IssueAttachmentsDetail
|
<IssueAttachmentsDetail
|
||||||
key={attachmentId}
|
key={attachmentId}
|
||||||
attachmentId={attachmentId}
|
attachmentId={attachmentId}
|
||||||
|
@ -69,7 +69,7 @@ export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||||
Delete Attachment
|
Delete attachment
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-custom-text-200">
|
<p className="text-sm text-custom-text-200">
|
||||||
@ -94,7 +94,7 @@ export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
|
|||||||
}}
|
}}
|
||||||
disabled={loader}
|
disabled={loader}
|
||||||
>
|
>
|
||||||
{loader ? "Deleting..." : "Delete"}
|
{loader ? "Deleting" : "Delete"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
@ -79,7 +79,7 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
|
|||||||
const reactionUsers = (reactionIds?.[reaction] || [])
|
const reactionUsers = (reactionIds?.[reaction] || [])
|
||||||
.map((reactionId) => {
|
.map((reactionId) => {
|
||||||
const reactionDetails = getReactionById(reactionId);
|
const reactionDetails = getReactionById(reactionId);
|
||||||
return reactionDetails ? getUserDetails(reactionDetails.actor_id)?.display_name : null;
|
return reactionDetails ? getUserDetails(reactionDetails.actor)?.display_name : null;
|
||||||
})
|
})
|
||||||
.filter((displayName): displayName is string => !!displayName);
|
.filter((displayName): displayName is string => !!displayName);
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
|
import useSize from "hooks/use-window-size";
|
||||||
// components
|
// components
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components/issues";
|
import { CalendarHeader, CalendarIssueBlocks, CalendarWeekDays, CalendarWeekHeader } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import {
|
import {
|
||||||
IIssueDisplayFilterOptions,
|
IIssueDisplayFilterOptions,
|
||||||
@ -15,6 +17,9 @@ import {
|
|||||||
TIssueMap,
|
TIssueMap,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
import { ICalendarWeek } from "./types";
|
import { ICalendarWeek } from "./types";
|
||||||
|
// helpers
|
||||||
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
// constants
|
// constants
|
||||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
@ -24,6 +29,7 @@ import { ICycleIssuesFilter } from "store/issue/cycle";
|
|||||||
import { IModuleIssuesFilter } from "store/issue/module";
|
import { IModuleIssuesFilter } from "store/issue/module";
|
||||||
import { IProjectIssuesFilter } from "store/issue/project";
|
import { IProjectIssuesFilter } from "store/issue/project";
|
||||||
import { IProjectViewIssuesFilter } from "store/issue/project-views";
|
import { IProjectViewIssuesFilter } from "store/issue/project-views";
|
||||||
|
import { MONTHS_LIST } from "constants/calendar";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter;
|
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter;
|
||||||
@ -62,6 +68,8 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
|||||||
updateFilters,
|
updateFilters,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
// states
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
issues: { viewFlags },
|
issues: { viewFlags },
|
||||||
@ -70,6 +78,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const [windowWidth] = useSize();
|
||||||
|
|
||||||
const { enableIssueCreation } = viewFlags || {};
|
const { enableIssueCreation } = viewFlags || {};
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
@ -78,18 +87,30 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const allWeeksOfActiveMonth = issueCalendarView.allWeeksOfActiveMonth;
|
const allWeeksOfActiveMonth = issueCalendarView.allWeeksOfActiveMonth;
|
||||||
|
|
||||||
if (!calendarPayload)
|
const formattedDatePayload = renderFormattedPayloadDate(selectedDate) ?? undefined;
|
||||||
|
|
||||||
|
if (!calendarPayload || !formattedDatePayload)
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full w-full place-items-center">
|
<div className="grid h-full w-full place-items-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||||
<CalendarHeader issuesFilterStore={issuesFilterStore} updateFilters={updateFilters} />
|
<CalendarHeader
|
||||||
<div className="flex h-full w-full vertical-scrollbar scrollbar-lg flex-col">
|
setSelectedDate={setSelectedDate}
|
||||||
|
issuesFilterStore={issuesFilterStore}
|
||||||
|
updateFilters={updateFilters}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn("flex md:h-full w-full flex-col overflow-y-auto", {
|
||||||
|
"vertical-scrollbar scrollbar-lg": windowWidth > 768,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<CalendarWeekHeader isLoading={!issues} showWeekends={showWeekends} />
|
<CalendarWeekHeader isLoading={!issues} showWeekends={showWeekends} />
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
{layout === "month" && (
|
{layout === "month" && (
|
||||||
@ -97,6 +118,8 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
|||||||
{allWeeksOfActiveMonth &&
|
{allWeeksOfActiveMonth &&
|
||||||
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
|
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
|
||||||
<CalendarWeekDays
|
<CalendarWeekDays
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
setSelectedDate={setSelectedDate}
|
||||||
issuesFilterStore={issuesFilterStore}
|
issuesFilterStore={issuesFilterStore}
|
||||||
key={weekIndex}
|
key={weekIndex}
|
||||||
week={week}
|
week={week}
|
||||||
@ -115,6 +138,8 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
{layout === "week" && (
|
{layout === "week" && (
|
||||||
<CalendarWeekDays
|
<CalendarWeekDays
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
setSelectedDate={setSelectedDate}
|
||||||
issuesFilterStore={issuesFilterStore}
|
issuesFilterStore={issuesFilterStore}
|
||||||
week={issueCalendarView.allDaysOfActiveWeek}
|
week={issueCalendarView.allDaysOfActiveWeek}
|
||||||
issues={issues}
|
issues={issues}
|
||||||
@ -129,6 +154,28 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* mobile view */}
|
||||||
|
<div className="md:hidden">
|
||||||
|
<p className="p-4 text-xl font-semibold">
|
||||||
|
{`${selectedDate.getDate()} ${
|
||||||
|
MONTHS_LIST[selectedDate.getMonth() + 1].title
|
||||||
|
}, ${selectedDate.getFullYear()}`}
|
||||||
|
</p>
|
||||||
|
<CalendarIssueBlocks
|
||||||
|
date={selectedDate}
|
||||||
|
issues={issues}
|
||||||
|
issueIdList={issueIdList}
|
||||||
|
quickActions={quickActions}
|
||||||
|
enableQuickIssueCreate
|
||||||
|
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||||
|
quickAddCallback={quickAddCallback}
|
||||||
|
addIssuesToView={addIssuesToView}
|
||||||
|
viewId={viewId}
|
||||||
|
readOnly={readOnly}
|
||||||
|
isDragDisabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Droppable } from "@hello-pangea/dnd";
|
import { Droppable } from "@hello-pangea/dnd";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { CalendarIssueBlocks, ICalendarDate, CalendarQuickAddIssueForm } from "components/issues";
|
import { CalendarIssueBlocks, ICalendarDate } from "components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
// constants
|
// constants
|
||||||
import { MONTHS_LIST } from "constants/calendar";
|
import { MONTHS_LIST } from "constants/calendar";
|
||||||
// types
|
// types
|
||||||
@ -31,6 +31,8 @@ type Props = {
|
|||||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||||
viewId?: string;
|
viewId?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
selectedDate: Date;
|
||||||
|
setSelectedDate: (date: Date) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||||
@ -46,8 +48,10 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
|||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
viewId,
|
viewId,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
|
selectedDate,
|
||||||
|
setSelectedDate,
|
||||||
} = props;
|
} = props;
|
||||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
|
||||||
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
|
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
|
||||||
|
|
||||||
const formattedDatePayload = renderFormattedPayloadDate(date.date);
|
const formattedDatePayload = renderFormattedPayloadDate(date.date);
|
||||||
@ -57,13 +61,14 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
|||||||
const totalIssues = issueIdList?.length ?? 0;
|
const totalIssues = issueIdList?.length ?? 0;
|
||||||
|
|
||||||
const isToday = date.date.toDateString() === new Date().toDateString();
|
const isToday = date.date.toDateString() === new Date().toDateString();
|
||||||
|
const isSelectedDate = date.date.toDateString() == selectedDate.toDateString();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="group relative flex h-full w-full flex-col bg-custom-background-90">
|
<div className="group relative flex h-full w-full flex-col bg-custom-background-90">
|
||||||
{/* header */}
|
{/* header */}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-end flex-shrink-0 px-2 py-1.5 text-right text-xs ${
|
className={`hidden md:flex items-center justify-end flex-shrink-0 px-2 py-1.5 text-right text-xs ${
|
||||||
calendarLayout === "month" // if month layout, highlight current month days
|
calendarLayout === "month" // if month layout, highlight current month days
|
||||||
? date.is_current_month
|
? date.is_current_month
|
||||||
? "font-medium"
|
? "font-medium"
|
||||||
@ -86,7 +91,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* content */}
|
{/* content */}
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full hidden md:block">
|
||||||
<Droppable droppableId={formattedDatePayload} isDropDisabled={readOnly}>
|
<Droppable droppableId={formattedDatePayload} isDropDisabled={readOnly}>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
@ -99,46 +104,45 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
<CalendarIssueBlocks
|
<CalendarIssueBlocks
|
||||||
|
date={date.date}
|
||||||
issues={issues}
|
issues={issues}
|
||||||
issueIdList={issueIdList}
|
issueIdList={issueIdList}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
showAllIssues={showAllIssues}
|
|
||||||
isDragDisabled={readOnly}
|
isDragDisabled={readOnly}
|
||||||
/>
|
|
||||||
|
|
||||||
{enableQuickIssueCreate && !disableIssueCreation && !readOnly && (
|
|
||||||
<div className="px-2 py-1">
|
|
||||||
<CalendarQuickAddIssueForm
|
|
||||||
formKey="target_date"
|
|
||||||
groupId={formattedDatePayload}
|
|
||||||
prePopulatedData={{
|
|
||||||
target_date: renderFormattedPayloadDate(date.date) ?? undefined,
|
|
||||||
}}
|
|
||||||
quickAddCallback={quickAddCallback}
|
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
|
disableIssueCreation={disableIssueCreation}
|
||||||
|
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||||
|
quickAddCallback={quickAddCallback}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
onOpen={() => setShowAllIssues(true)}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalIssues > 4 && (
|
|
||||||
<div className="flex items-center px-2.5 py-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="w-min whitespace-nowrap rounded text-xs px-1.5 py-1 text-custom-text-400 font-medium hover:bg-custom-background-80 hover:text-custom-text-300"
|
|
||||||
onClick={() => setShowAllIssues((prevData) => !prevData)}
|
|
||||||
>
|
|
||||||
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile view content */}
|
||||||
|
<div
|
||||||
|
onClick={() => setSelectedDate(date.date)}
|
||||||
|
className={cn(
|
||||||
|
"text-sm py-2.5 h-full w-full font-medium mx-auto flex flex-col justify-start items-center md:hidden cursor-pointer",
|
||||||
|
{
|
||||||
|
"bg-custom-background-100": date.date.getDay() !== 0 && date.date.getDay() !== 6,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn("h-6 w-6 rounded-full flex items-center justify-center ", {
|
||||||
|
"bg-custom-primary-100 text-white": isSelectedDate,
|
||||||
|
"bg-custom-primary-100/10 text-custom-primary-100 ": isToday && !isSelectedDate,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{date.date.getDate()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totalIssues > 0 && <div className="flex flex-shrink-0 h-1.5 w-1.5 bg-custom-primary-100 rounded mt-1" />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -4,9 +4,10 @@ import { useRouter } from "next/router";
|
|||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
|
import useSize from "hooks/use-window-size";
|
||||||
// ui
|
// ui
|
||||||
// icons
|
// icons
|
||||||
import { Check, ChevronUp } from "lucide-react";
|
import { Check, ChevronUp, MoreVerticalIcon } from "lucide-react";
|
||||||
import { ToggleSwitch } from "@plane/ui";
|
import { ToggleSwitch } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import {
|
import {
|
||||||
@ -41,6 +42,7 @@ export const CalendarOptionsDropdown: React.FC<ICalendarHeader> = observer((prop
|
|||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
|
|
||||||
const issueCalendarView = useCalendarView();
|
const issueCalendarView = useCalendarView();
|
||||||
|
const [windowWidth] = useSize();
|
||||||
|
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
@ -60,7 +62,7 @@ export const CalendarOptionsDropdown: React.FC<ICalendarHeader> = observer((prop
|
|||||||
const calendarLayout = issuesFilterStore.issueFilters?.displayFilters?.calendar?.layout ?? "month";
|
const calendarLayout = issuesFilterStore.issueFilters?.displayFilters?.calendar?.layout ?? "month";
|
||||||
const showWeekends = issuesFilterStore.issueFilters?.displayFilters?.calendar?.show_weekends ?? false;
|
const showWeekends = issuesFilterStore.issueFilters?.displayFilters?.calendar?.show_weekends ?? false;
|
||||||
|
|
||||||
const handleLayoutChange = (layout: TCalendarLayouts) => {
|
const handleLayoutChange = (layout: TCalendarLayouts, closePopover: any) => {
|
||||||
if (!projectId || !updateFilters) return;
|
if (!projectId || !updateFilters) return;
|
||||||
|
|
||||||
updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, {
|
updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, {
|
||||||
@ -75,6 +77,7 @@ export const CalendarOptionsDropdown: React.FC<ICalendarHeader> = observer((prop
|
|||||||
? issueCalendarView.calendarFilters.activeMonthDate
|
? issueCalendarView.calendarFilters.activeMonthDate
|
||||||
: issueCalendarView.calendarFilters.activeWeekDate
|
: issueCalendarView.calendarFilters.activeWeekDate
|
||||||
);
|
);
|
||||||
|
if (windowWidth <= 768) closePopover(); // close the popover on mobile
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleWeekends = () => {
|
const handleToggleWeekends = () => {
|
||||||
@ -92,13 +95,12 @@ export const CalendarOptionsDropdown: React.FC<ICalendarHeader> = observer((prop
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
{({ open }) => (
|
{({ open, close: closePopover }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button as={React.Fragment}>
|
<Popover.Button as={React.Fragment}>
|
||||||
<button
|
<button type="button" ref={setReferenceElement}>
|
||||||
type="button"
|
<div
|
||||||
ref={setReferenceElement}
|
className={`hidden md:flex items-center gap-1.5 rounded bg-custom-background-80 px-2.5 py-1 text-xs outline-none hover:bg-custom-background-80 ${
|
||||||
className={`flex items-center gap-1.5 rounded bg-custom-background-80 px-2.5 py-1 text-xs outline-none hover:bg-custom-background-80 ${
|
|
||||||
open ? "text-custom-text-100" : "text-custom-text-200"
|
open ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -108,6 +110,10 @@ export const CalendarOptionsDropdown: React.FC<ICalendarHeader> = observer((prop
|
|||||||
>
|
>
|
||||||
<ChevronUp width={12} strokeWidth={2} />
|
<ChevronUp width={12} strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="md:hidden">
|
||||||
|
<MoreVerticalIcon className="h-4 text-custom-text-200" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
<Transition
|
<Transition
|
||||||
@ -132,7 +138,7 @@ export const CalendarOptionsDropdown: React.FC<ICalendarHeader> = observer((prop
|
|||||||
key={layout}
|
key={layout}
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center justify-between gap-2 rounded px-1 py-1.5 text-left text-xs hover:bg-custom-background-80"
|
className="flex w-full items-center justify-between gap-2 rounded px-1 py-1.5 text-left text-xs hover:bg-custom-background-80"
|
||||||
onClick={() => handleLayoutChange(layoutDetails.key)}
|
onClick={() => handleLayoutChange(layoutDetails.key, closePopover)}
|
||||||
>
|
>
|
||||||
{layoutDetails.title}
|
{layoutDetails.title}
|
||||||
{calendarLayout === layout && <Check size={12} strokeWidth={2} />}
|
{calendarLayout === layout && <Check size={12} strokeWidth={2} />}
|
||||||
@ -144,7 +150,12 @@ export const CalendarOptionsDropdown: React.FC<ICalendarHeader> = observer((prop
|
|||||||
onClick={handleToggleWeekends}
|
onClick={handleToggleWeekends}
|
||||||
>
|
>
|
||||||
Show weekends
|
Show weekends
|
||||||
<ToggleSwitch value={showWeekends} onChange={() => {}} />
|
<ToggleSwitch
|
||||||
|
value={showWeekends}
|
||||||
|
onChange={() => {
|
||||||
|
if (windowWidth <= 768) closePopover(); // close the popover on mobile
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,10 +24,11 @@ interface ICalendarHeader {
|
|||||||
filterType: EIssueFilterType,
|
filterType: EIssueFilterType,
|
||||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
setSelectedDate: (date: Date) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CalendarHeader: React.FC<ICalendarHeader> = observer((props) => {
|
export const CalendarHeader: React.FC<ICalendarHeader> = observer((props) => {
|
||||||
const { issuesFilterStore, updateFilters } = props;
|
const { issuesFilterStore, updateFilters, setSelectedDate } = props;
|
||||||
|
|
||||||
const issueCalendarView = useCalendarView();
|
const issueCalendarView = useCalendarView();
|
||||||
|
|
||||||
@ -91,6 +92,7 @@ export const CalendarHeader: React.FC<ICalendarHeader> = observer((props) => {
|
|||||||
activeMonthDate: firstDayOfCurrentMonth,
|
activeMonthDate: firstDayOfCurrentMonth,
|
||||||
activeWeekDate: today,
|
activeWeekDate: today,
|
||||||
});
|
});
|
||||||
|
setSelectedDate(today);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -5,6 +5,8 @@ export * from "./types.d";
|
|||||||
export * from "./day-tile";
|
export * from "./day-tile";
|
||||||
export * from "./header";
|
export * from "./header";
|
||||||
export * from "./issue-blocks";
|
export * from "./issue-blocks";
|
||||||
|
export * from "./issue-block-root";
|
||||||
|
export * from "./issue-block";
|
||||||
export * from "./week-days";
|
export * from "./week-days";
|
||||||
export * from "./week-header";
|
export * from "./week-header";
|
||||||
export * from "./quick-add-issue-form";
|
export * from "./quick-add-issue-form";
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
// components
|
||||||
|
import { CalendarIssueBlock } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { TIssue, TIssueMap } from "@plane/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issues: TIssueMap | undefined;
|
||||||
|
issueId: string;
|
||||||
|
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||||
|
isDragging?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarIssueBlockRoot: React.FC<Props> = (props) => {
|
||||||
|
const { issues, issueId, quickActions, isDragging } = props;
|
||||||
|
|
||||||
|
if (!issues?.[issueId]) return null;
|
||||||
|
|
||||||
|
const issue = issues?.[issueId];
|
||||||
|
|
||||||
|
return <CalendarIssueBlock isDragging={isDragging} issue={issue} quickActions={quickActions} />;
|
||||||
|
};
|
113
web/components/issues/issue-layouts/calendar/issue-block.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { useState, useRef } from "react";
|
||||||
|
import { MoreHorizontal } from "lucide-react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
// components
|
||||||
|
import { Tooltip, ControlLink } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
// types
|
||||||
|
import { TIssue } from "@plane/types";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: TIssue;
|
||||||
|
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||||
|
isDragging?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarIssueBlock: React.FC<Props> = observer((props) => {
|
||||||
|
const { issue, quickActions, isDragging = false } = props;
|
||||||
|
// hooks
|
||||||
|
const {
|
||||||
|
router: { workspaceSlug, projectId },
|
||||||
|
} = useApplication();
|
||||||
|
const { getProjectIdentifierById } = useProject();
|
||||||
|
const { getProjectStates } = useProjectState();
|
||||||
|
const { peekIssue, setPeekIssue } = useIssueDetail();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
// states
|
||||||
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
|
|
||||||
|
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const stateColor = getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || "";
|
||||||
|
|
||||||
|
const handleIssuePeekOverview = (issue: TIssue) =>
|
||||||
|
workspaceSlug &&
|
||||||
|
issue &&
|
||||||
|
issue.project_id &&
|
||||||
|
issue.id &&
|
||||||
|
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
|
||||||
|
|
||||||
|
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
||||||
|
|
||||||
|
const customActionButton = (
|
||||||
|
<div
|
||||||
|
ref={menuActionRef}
|
||||||
|
className={`w-full cursor-pointer rounded p-1 text-custom-sidebar-text-400 hover:bg-custom-background-80 ${
|
||||||
|
isMenuActive ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ControlLink
|
||||||
|
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||||
|
target="_blank"
|
||||||
|
onClick={() => handleIssuePeekOverview(issue)}
|
||||||
|
className="w-full cursor-pointer text-sm text-custom-text-100"
|
||||||
|
disabled={!!issue?.tempId}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{issue?.tempId !== undefined && (
|
||||||
|
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"group/calendar-block flex h-10 md:h-8 w-full items-center justify-between gap-1.5 rounded border-b md:border-[0.5px] border-custom-border-200 hover:border-custom-border-400 md:px-1 px-4 py-1.5 ",
|
||||||
|
{
|
||||||
|
"bg-custom-background-90 shadow-custom-shadow-rg border-custom-primary-100": isDragging,
|
||||||
|
},
|
||||||
|
{ "bg-custom-background-100 hover:bg-custom-background-90": !isDragging },
|
||||||
|
{
|
||||||
|
"border border-custom-primary-70 hover:border-custom-primary-70": peekIssue?.issueId === issue.id,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex h-full items-center gap-1.5 truncate">
|
||||||
|
<span
|
||||||
|
className="h-full w-0.5 flex-shrink-0 rounded"
|
||||||
|
style={{
|
||||||
|
backgroundColor: stateColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex-shrink-0 text-sm md:text-xs text-custom-text-300">
|
||||||
|
{getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
|
||||||
|
</div>
|
||||||
|
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
||||||
|
<div className="truncate text-sm font-medium md:font-normal md:text-xs">{issue.name}</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`flex-shrink-0 md:hidden h-5 w-5 group-hover/calendar-block:block ${
|
||||||
|
isMenuActive ? "!block" : ""
|
||||||
|
}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{quickActions(issue, customActionButton)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</ControlLink>
|
||||||
|
);
|
||||||
|
});
|
@ -1,74 +1,62 @@
|
|||||||
import { useState, useRef } from "react";
|
import { useState } from "react";
|
||||||
import { Draggable } from "@hello-pangea/dnd";
|
import { Draggable } from "@hello-pangea/dnd";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { MoreHorizontal } from "lucide-react";
|
|
||||||
// components
|
// components
|
||||||
import { Tooltip, ControlLink } from "@plane/ui";
|
import { CalendarQuickAddIssueForm, CalendarIssueBlockRoot } from "components/issues";
|
||||||
// hooks
|
|
||||||
import { cn } from "helpers/common.helper";
|
|
||||||
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
|
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|
||||||
import { usePlatformOS } from "hooks/use-platform-os";
|
|
||||||
// helpers
|
// helpers
|
||||||
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { TIssue, TIssueMap } from "@plane/types";
|
import { TIssue, TIssueMap } from "@plane/types";
|
||||||
|
import useSize from "hooks/use-window-size";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
date: Date;
|
||||||
issues: TIssueMap | undefined;
|
issues: TIssueMap | undefined;
|
||||||
issueIdList: string[] | null;
|
issueIdList: string[] | null;
|
||||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||||
showAllIssues?: boolean;
|
|
||||||
isDragDisabled?: boolean;
|
isDragDisabled?: boolean;
|
||||||
|
enableQuickIssueCreate?: boolean;
|
||||||
|
disableIssueCreation?: boolean;
|
||||||
|
quickAddCallback?: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
data: TIssue,
|
||||||
|
viewId?: string
|
||||||
|
) => Promise<TIssue | undefined>;
|
||||||
|
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||||
|
viewId?: string;
|
||||||
|
readOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||||
const { issues, issueIdList, quickActions, showAllIssues = false, isDragDisabled = false } = props;
|
|
||||||
// hooks
|
|
||||||
const {
|
const {
|
||||||
router: { workspaceSlug, projectId },
|
date,
|
||||||
} = useApplication();
|
issues,
|
||||||
const { getProjectIdentifierById } = useProject();
|
issueIdList,
|
||||||
const { getProjectStates } = useProjectState();
|
quickActions,
|
||||||
const { peekIssue, setPeekIssue } = useIssueDetail();
|
isDragDisabled = false,
|
||||||
const { isMobile } = usePlatformOS();
|
enableQuickIssueCreate,
|
||||||
|
disableIssueCreation,
|
||||||
|
quickAddCallback,
|
||||||
|
addIssuesToView,
|
||||||
|
viewId,
|
||||||
|
readOnly,
|
||||||
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||||
|
// hooks
|
||||||
|
const [windowWidth] = useSize();
|
||||||
|
|
||||||
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
const formattedDatePayload = renderFormattedPayloadDate(date);
|
||||||
|
const totalIssues = issueIdList?.length ?? 0;
|
||||||
|
|
||||||
const handleIssuePeekOverview = (issue: TIssue) =>
|
if (!formattedDatePayload) return null;
|
||||||
workspaceSlug &&
|
|
||||||
issue &&
|
|
||||||
issue.project_id &&
|
|
||||||
issue.id &&
|
|
||||||
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
|
|
||||||
|
|
||||||
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
|
||||||
|
|
||||||
const customActionButton = (
|
|
||||||
<div
|
|
||||||
ref={menuActionRef}
|
|
||||||
className={`w-full cursor-pointer rounded p-1 text-custom-sidebar-text-400 hover:bg-custom-background-80 ${
|
|
||||||
isMenuActive ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{issueIdList?.slice(0, showAllIssues ? issueIdList.length : 4).map((issueId, index) => {
|
{issueIdList?.slice(0, showAllIssues || windowWidth <= 768 ? issueIdList.length : 4).map((issueId, index) =>
|
||||||
if (!issues?.[issueId]) return null;
|
windowWidth > 768 ? (
|
||||||
|
<Draggable key={issueId} draggableId={issueId} index={index} isDragDisabled={isDragDisabled}>
|
||||||
const issue = issues?.[issueId];
|
|
||||||
|
|
||||||
const stateColor =
|
|
||||||
getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Draggable key={issue.id} draggableId={issue.id} index={index} isDragDisabled={isDragDisabled}>
|
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className="relative cursor-pointer p-1 px-2"
|
className="relative cursor-pointer p-1 px-2"
|
||||||
@ -76,63 +64,46 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
|||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
<ControlLink
|
<CalendarIssueBlockRoot
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
issues={issues}
|
||||||
target="_blank"
|
issueId={issueId}
|
||||||
onClick={() => handleIssuePeekOverview(issue)}
|
quickActions={quickActions}
|
||||||
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
isDragging={snapshot.isDragging}
|
||||||
disabled={!!issue?.tempId}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{issue?.tempId !== undefined && (
|
|
||||||
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"group/calendar-block flex h-8 w-full items-center justify-between gap-1.5 rounded border-[0.5px] border-custom-border-200 hover:border-custom-border-400 px-1 py-1.5 ",
|
|
||||||
{
|
|
||||||
"bg-custom-background-90 shadow-custom-shadow-rg border-custom-primary-100":
|
|
||||||
snapshot.isDragging,
|
|
||||||
},
|
|
||||||
{ "bg-custom-background-100 hover:bg-custom-background-90": !snapshot.isDragging },
|
|
||||||
{
|
|
||||||
"border border-custom-primary-70 hover:border-custom-primary-70":
|
|
||||||
peekIssue?.issueId === issue.id,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex h-full items-center gap-1.5">
|
|
||||||
<span
|
|
||||||
className="h-full w-0.5 flex-shrink-0 rounded"
|
|
||||||
style={{
|
|
||||||
backgroundColor: stateColor,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex-shrink-0 text-xs text-custom-text-300">
|
|
||||||
{getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
|
|
||||||
</div>
|
|
||||||
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
|
||||||
<div className="truncate text-xs">{issue.name}</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`hidden h-5 w-5 group-hover/calendar-block:block ${isMenuActive ? "!block" : ""}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{quickActions(issue, customActionButton)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</ControlLink>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
);
|
) : (
|
||||||
})}
|
<CalendarIssueBlockRoot key={issueId} issues={issues} issueId={issueId} quickActions={quickActions} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{enableQuickIssueCreate && !disableIssueCreation && !readOnly && (
|
||||||
|
<div className="px-1 md:px-2 py-1 border-custom-border-200 border-b md:border-none">
|
||||||
|
<CalendarQuickAddIssueForm
|
||||||
|
formKey="target_date"
|
||||||
|
groupId={formattedDatePayload}
|
||||||
|
prePopulatedData={{
|
||||||
|
target_date: formattedDatePayload,
|
||||||
|
}}
|
||||||
|
quickAddCallback={quickAddCallback}
|
||||||
|
addIssuesToView={addIssuesToView}
|
||||||
|
viewId={viewId}
|
||||||
|
onOpen={() => setShowAllIssues(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{totalIssues > 4 && (
|
||||||
|
<div className="hidden md:flex items-center px-2.5 py-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-min whitespace-nowrap rounded text-xs px-1.5 py-1 text-custom-text-400 font-medium hover:bg-custom-background-80 hover:text-custom-text-300"
|
||||||
|
onClick={() => setShowAllIssues(!showAllIssues)}
|
||||||
|
>
|
||||||
|
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -50,7 +50,7 @@ const Inputs = (props: any) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4 className="text-xs leading-5 text-custom-text-400">{projectDetails?.identifier ?? "..."}</h4>
|
<h4 className="text-sm md:text-xs leading-5 text-custom-text-400">{projectDetails?.identifier ?? "..."}</h4>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@ -58,7 +58,7 @@ const Inputs = (props: any) => {
|
|||||||
{...register("name", {
|
{...register("name", {
|
||||||
required: "Issue title is required.",
|
required: "Issue title is required.",
|
||||||
})}
|
})}
|
||||||
className="w-full rounded-md bg-transparent py-1.5 pr-2 text-xs font-medium leading-5 text-custom-text-200 outline-none"
|
className="w-full rounded-md bg-transparent py-1.5 pr-2 text-sm md:text-xs font-medium leading-5 text-custom-text-200 outline-none"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -221,7 +221,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmitHandler)}
|
onSubmit={handleSubmit(onSubmitHandler)}
|
||||||
className="z-50 flex w-full items-center gap-x-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-2 shadow-custom-shadow-2xs transition-opacity"
|
className="z-50 flex w-full items-center gap-x-2 rounded md:border-[0.5px] border-custom-border-200 bg-custom-background-100 px-2 md:shadow-custom-shadow-2xs transition-opacity"
|
||||||
>
|
>
|
||||||
<Inputs formKey={formKey} register={register} setFocus={setFocus} projectDetails={projectDetail} />
|
<Inputs formKey={formKey} register={register} setFocus={setFocus} projectDetails={projectDetail} />
|
||||||
</form>
|
</form>
|
||||||
@ -230,7 +230,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
{!isOpen && (
|
{!isOpen && (
|
||||||
<div
|
<div
|
||||||
className={cn("hidden rounded border-[0.5px] border-custom-border-200 group-hover:block", {
|
className={cn("md:hidden rounded md:border-[0.5px] border-custom-border-200 md:group-hover:block", {
|
||||||
block: isMenuOpen,
|
block: isMenuOpen,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -28,6 +28,8 @@ type Props = {
|
|||||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||||
viewId?: string;
|
viewId?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
selectedDate: Date;
|
||||||
|
setSelectedDate: (date: Date) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||||
@ -43,6 +45,8 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
|||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
viewId,
|
viewId,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
|
selectedDate,
|
||||||
|
setSelectedDate,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
|
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
|
||||||
@ -52,7 +56,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`grid divide-x-[0.5px] divide-custom-border-200 ${showWeekends ? "grid-cols-7" : "grid-cols-5"} ${
|
className={`grid md:divide-x-[0.5px] divide-custom-border-200 ${showWeekends ? "grid-cols-7" : "grid-cols-5"} ${
|
||||||
calendarLayout === "month" ? "" : "h-full"
|
calendarLayout === "month" ? "" : "h-full"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -61,6 +65,8 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CalendarDayTile
|
<CalendarDayTile
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
setSelectedDate={setSelectedDate}
|
||||||
issuesFilterStore={issuesFilterStore}
|
issuesFilterStore={issuesFilterStore}
|
||||||
key={renderFormattedPayloadDate(date.date)}
|
key={renderFormattedPayloadDate(date.date)}
|
||||||
date={date}
|
date={date}
|
||||||
|
@ -13,7 +13,7 @@ export const CalendarWeekHeader: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative sticky top-0 z-[1] grid divide-x-[0.5px] divide-custom-border-200 text-sm font-medium ${
|
className={`relative sticky top-0 z-[1] grid md:divide-x-[0.5px] divide-custom-border-200 text-sm font-medium ${
|
||||||
showWeekends ? "grid-cols-7" : "grid-cols-5"
|
showWeekends ? "grid-cols-7" : "grid-cols-5"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -24,7 +24,7 @@ export const CalendarWeekHeader: React.FC<Props> = observer((props) => {
|
|||||||
if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null;
|
if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={day.shortTitle} className="flex h-11 items-center justify-end bg-custom-background-90 px-4">
|
<div key={day.shortTitle} className="flex h-11 items-center justify-center md:justify-end bg-custom-background-90 px-4">
|
||||||
{day.shortTitle}
|
{day.shortTitle}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -63,12 +63,14 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {});
|
const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {});
|
||||||
|
|
||||||
const emptyStateType = isCompletedCycleSnapshotAvailable
|
const isCompletedAndEmpty = isCompletedCycleSnapshotAvailable || cycleDetails?.status.toLowerCase() === "completed";
|
||||||
|
|
||||||
|
const emptyStateType = isCompletedAndEmpty
|
||||||
? EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES
|
? EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES
|
||||||
: isEmptyFilters
|
: isEmptyFilters
|
||||||
? EmptyStateType.PROJECT_EMPTY_FILTER
|
? EmptyStateType.PROJECT_EMPTY_FILTER
|
||||||
: EmptyStateType.PROJECT_CYCLE_NO_ISSUES;
|
: EmptyStateType.PROJECT_CYCLE_NO_ISSUES;
|
||||||
const additionalPath = isCompletedCycleSnapshotAvailable ? undefined : activeLayout ?? "list";
|
const additionalPath = isCompletedAndEmpty ? undefined : activeLayout ?? "list";
|
||||||
const emptyStateSize = isEmptyFilters ? "lg" : "sm";
|
const emptyStateSize = isEmptyFilters ? "lg" : "sm";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -87,7 +89,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
|||||||
additionalPath={additionalPath}
|
additionalPath={additionalPath}
|
||||||
size={emptyStateSize}
|
size={emptyStateSize}
|
||||||
primaryButtonOnClick={
|
primaryButtonOnClick={
|
||||||
!isCompletedCycleSnapshotAvailable && !isEmptyFilters
|
!isCompletedAndEmpty && !isEmptyFilters
|
||||||
? () => {
|
? () => {
|
||||||
setTrackElement(E_CYCLE_ISSUES_EMPTY_STATE);
|
setTrackElement(E_CYCLE_ISSUES_EMPTY_STATE);
|
||||||
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
|
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
|
||||||
@ -95,9 +97,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
secondaryButtonOnClick={
|
secondaryButtonOnClick={
|
||||||
!isCompletedCycleSnapshotAvailable && isEmptyFilters
|
!isCompletedAndEmpty && isEmptyFilters ? handleClearAllFilters : () => setCycleIssuesListModal(true)
|
||||||
? handleClearAllFilters
|
|
||||||
: () => setCycleIssuesListModal(true)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,6 +58,7 @@ export interface IGroupByKanBan {
|
|||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
isDragStarted?: boolean;
|
isDragStarted?: boolean;
|
||||||
showEmptyGroup?: boolean;
|
showEmptyGroup?: boolean;
|
||||||
|
subGroupIssueHeaderCount?: (listId: string) => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||||
@ -83,6 +84,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
isDragStarted,
|
isDragStarted,
|
||||||
showEmptyGroup = true,
|
showEmptyGroup = true,
|
||||||
|
subGroupIssueHeaderCount,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const member = useMember();
|
const member = useMember();
|
||||||
@ -105,44 +107,57 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
|
|
||||||
if (!list) return null;
|
if (!list) return null;
|
||||||
|
|
||||||
const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0);
|
const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
|
||||||
|
|
||||||
const groupList = showEmptyGroup ? list : groupWithIssues;
|
|
||||||
|
|
||||||
const visibilityGroupBy = (_list: IGroupByColumn) => {
|
|
||||||
if (sub_group_by) {
|
if (sub_group_by) {
|
||||||
if (kanbanFilters?.sub_group_by.includes(_list.id)) return true;
|
const groupVisibility = {
|
||||||
return false;
|
showGroup: true,
|
||||||
|
showIssues: true,
|
||||||
|
};
|
||||||
|
if (!showEmptyGroup) {
|
||||||
|
groupVisibility.showGroup = subGroupIssueHeaderCount ? subGroupIssueHeaderCount(_list.id) > 0 : true;
|
||||||
|
}
|
||||||
|
return groupVisibility;
|
||||||
} else {
|
} else {
|
||||||
if (kanbanFilters?.group_by.includes(_list.id)) return true;
|
const groupVisibility = {
|
||||||
return false;
|
showGroup: true,
|
||||||
|
showIssues: true,
|
||||||
|
};
|
||||||
|
if (!showEmptyGroup) {
|
||||||
|
if ((issueIds as TGroupedIssues)?.[_list.id]?.length > 0) groupVisibility.showGroup = true;
|
||||||
|
else groupVisibility.showGroup = false;
|
||||||
|
}
|
||||||
|
if (kanbanFilters?.group_by.includes(_list.id)) groupVisibility.showIssues = false;
|
||||||
|
return groupVisibility;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isGroupByCreatedBy = group_by === "created_by";
|
const isGroupByCreatedBy = group_by === "created_by";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative flex w-full gap-2 ${sub_group_by ? "h-full" : "h-full"}`}>
|
<div className={`relative w-full flex gap-2 ${sub_group_by ? "h-full" : "h-full"}`}>
|
||||||
{groupList &&
|
{list &&
|
||||||
groupList.length > 0 &&
|
list.length > 0 &&
|
||||||
groupList.map((_list: IGroupByColumn) => {
|
list.map((subList: IGroupByColumn) => {
|
||||||
const groupByVisibilityToggle = visibilityGroupBy(_list);
|
const groupByVisibilityToggle = visibilityGroupBy(subList);
|
||||||
|
|
||||||
|
if (groupByVisibilityToggle.showGroup === false) return <></>;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={_list.id}
|
key={subList.id}
|
||||||
className={`group relative flex flex-shrink-0 flex-col ${groupByVisibilityToggle ? `` : `w-[350px]`}`}
|
className={`relative flex flex-shrink-0 flex-col group ${
|
||||||
|
groupByVisibilityToggle.showIssues ? `w-[350px]` : ``
|
||||||
|
} `}
|
||||||
>
|
>
|
||||||
{sub_group_by === null && (
|
{sub_group_by === null && (
|
||||||
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
|
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
|
||||||
<HeaderGroupByCard
|
<HeaderGroupByCard
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
column_id={_list.id}
|
column_id={subList.id}
|
||||||
icon={_list.icon}
|
icon={subList.icon}
|
||||||
title={_list.name}
|
title={subList.name}
|
||||||
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
|
count={(issueIds as TGroupedIssues)?.[subList.id]?.length || 0}
|
||||||
issuePayload={_list.payload}
|
issuePayload={subList.payload}
|
||||||
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
|
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
|
||||||
storeType={storeType}
|
storeType={storeType}
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
@ -152,9 +167,9 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!groupByVisibilityToggle && (
|
{groupByVisibilityToggle.showIssues && (
|
||||||
<KanbanGroup
|
<KanbanGroup
|
||||||
groupId={_list.id}
|
groupId={subList.id}
|
||||||
issuesMap={issuesMap}
|
issuesMap={issuesMap}
|
||||||
issueIds={issueIds}
|
issueIds={issueIds}
|
||||||
peekIssueId={peekIssue?.issueId ?? ""}
|
peekIssueId={peekIssue?.issueId ?? ""}
|
||||||
@ -170,7 +185,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
disableIssueCreation={disableIssueCreation}
|
disableIssueCreation={disableIssueCreation}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
groupByVisibilityToggle={groupByVisibilityToggle}
|
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
isDragStarted={isDragStarted}
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
@ -208,6 +222,7 @@ export interface IKanBan {
|
|||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
isDragStarted?: boolean;
|
isDragStarted?: boolean;
|
||||||
|
subGroupIssueHeaderCount?: (listId: string) => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanBan: React.FC<IKanBan> = observer((props) => {
|
export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||||
@ -232,6 +247,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
isDragStarted,
|
isDragStarted,
|
||||||
showEmptyGroup,
|
showEmptyGroup,
|
||||||
|
subGroupIssueHeaderCount,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const issueKanBanView = useKanbanView();
|
const issueKanBanView = useKanbanView();
|
||||||
@ -259,6 +275,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
isDragStarted={isDragStarted}
|
isDragStarted={isDragStarted}
|
||||||
showEmptyGroup={showEmptyGroup}
|
showEmptyGroup={showEmptyGroup}
|
||||||
|
subGroupIssueHeaderCount={subGroupIssueHeaderCount}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -36,7 +36,7 @@ interface IKanbanGroup {
|
|||||||
viewId?: string;
|
viewId?: string;
|
||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
groupByVisibilityToggle: boolean;
|
groupByVisibilityToggle?: boolean;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
isDragStarted?: boolean;
|
isDragStarted?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ interface ISubGroupSwimlaneHeader {
|
|||||||
kanbanFilters: TIssueKanbanFilters;
|
kanbanFilters: TIssueKanbanFilters;
|
||||||
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
||||||
storeType: KanbanStoreType;
|
storeType: KanbanStoreType;
|
||||||
|
showEmptyGroup: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
|
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
|
||||||
@ -39,6 +40,22 @@ const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: st
|
|||||||
return headerCount;
|
return headerCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const visibilitySubGroupByGroupCount = (
|
||||||
|
issueIds: TSubGroupedIssues,
|
||||||
|
_list: IGroupByColumn,
|
||||||
|
showEmptyGroup: boolean
|
||||||
|
): boolean => {
|
||||||
|
let subGroupHeaderVisibility = true;
|
||||||
|
|
||||||
|
if (showEmptyGroup) subGroupHeaderVisibility = true;
|
||||||
|
else {
|
||||||
|
if (getSubGroupHeaderIssuesCount(issueIds, _list.id) > 0) subGroupHeaderVisibility = true;
|
||||||
|
else subGroupHeaderVisibility = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return subGroupHeaderVisibility;
|
||||||
|
};
|
||||||
|
|
||||||
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||||
issueIds,
|
issueIds,
|
||||||
sub_group_by,
|
sub_group_by,
|
||||||
@ -47,11 +64,21 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
|||||||
list,
|
list,
|
||||||
kanbanFilters,
|
kanbanFilters,
|
||||||
handleKanbanFilters,
|
handleKanbanFilters,
|
||||||
|
showEmptyGroup,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="relative flex h-max min-h-full w-full items-center gap-2">
|
<div className="relative flex h-max min-h-full w-full items-center gap-2">
|
||||||
{list &&
|
{list &&
|
||||||
list.length > 0 &&
|
list.length > 0 &&
|
||||||
list.map((_list: IGroupByColumn) => (
|
list.map((_list: IGroupByColumn) => {
|
||||||
|
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(
|
||||||
|
issueIds as TSubGroupedIssues,
|
||||||
|
_list,
|
||||||
|
showEmptyGroup
|
||||||
|
);
|
||||||
|
|
||||||
|
if (subGroupByVisibilityToggle === false) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
|
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
|
||||||
<HeaderGroupByCard
|
<HeaderGroupByCard
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
@ -66,7 +93,8 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
|||||||
storeType={storeType}
|
storeType={storeType}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -127,11 +155,28 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
return issueCount;
|
return issueCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const visibilitySubGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
|
||||||
|
const subGroupVisibility = {
|
||||||
|
showGroup: true,
|
||||||
|
showIssues: true,
|
||||||
|
};
|
||||||
|
if (showEmptyGroup) subGroupVisibility.showGroup = true;
|
||||||
|
else {
|
||||||
|
if (calculateIssueCount(_list.id) > 0) subGroupVisibility.showGroup = true;
|
||||||
|
else subGroupVisibility.showGroup = false;
|
||||||
|
}
|
||||||
|
if (kanbanFilters?.sub_group_by.includes(_list.id)) subGroupVisibility.showIssues = false;
|
||||||
|
return subGroupVisibility;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-max min-h-full w-full">
|
<div className="relative h-max min-h-full w-full">
|
||||||
{list &&
|
{list &&
|
||||||
list.length > 0 &&
|
list.length > 0 &&
|
||||||
list.map((_list: any) => (
|
list.map((_list: any) => {
|
||||||
|
const subGroupByVisibilityToggle = visibilitySubGroupBy(_list);
|
||||||
|
if (subGroupByVisibilityToggle.showGroup === false) return <></>;
|
||||||
|
return (
|
||||||
<div key={_list.id} className="flex flex-shrink-0 flex-col">
|
<div key={_list.id} className="flex flex-shrink-0 flex-col">
|
||||||
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
|
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
|
||||||
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
|
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
|
||||||
@ -147,7 +192,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
<div className="w-full border-b border-dashed border-custom-border-400" />
|
<div className="w-full border-b border-dashed border-custom-border-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!kanbanFilters?.sub_group_by.includes(_list.id) && (
|
{subGroupByVisibilityToggle.showIssues && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<KanBan
|
<KanBan
|
||||||
issuesMap={issuesMap}
|
issuesMap={issuesMap}
|
||||||
@ -156,6 +201,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
sub_group_id={_list.id}
|
sub_group_id={_list.id}
|
||||||
|
storeType={storeType}
|
||||||
updateIssue={updateIssue}
|
updateIssue={updateIssue}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
kanbanFilters={kanbanFilters}
|
kanbanFilters={kanbanFilters}
|
||||||
@ -168,12 +214,15 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
isDragStarted={isDragStarted}
|
isDragStarted={isDragStarted}
|
||||||
storeType={storeType}
|
subGroupIssueHeaderCount={(groupByListId: string) =>
|
||||||
|
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -267,6 +316,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
handleKanbanFilters={handleKanbanFilters}
|
handleKanbanFilters={handleKanbanFilters}
|
||||||
list={groupByList}
|
list={groupByList}
|
||||||
storeType={storeType}
|
storeType={storeType}
|
||||||
|
showEmptyGroup={showEmptyGroup}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -55,7 +55,10 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
|
|||||||
const handleCreateDraftIssue = async () => {
|
const handleCreateDraftIssue = async () => {
|
||||||
if (!changesMade || !workspaceSlug || !projectId) return;
|
if (!changesMade || !workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const payload = { ...changesMade };
|
const payload = {
|
||||||
|
...changesMade,
|
||||||
|
name: changesMade.name?.trim() === "" ? "Untitled" : changesMade.name?.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
await issueDraftService
|
await issueDraftService
|
||||||
.createDraftIssue(workspaceSlug.toString(), projectId.toString(), payload)
|
.createDraftIssue(workspaceSlug.toString(), projectId.toString(), payload)
|
||||||
|
@ -30,6 +30,7 @@ import { FileService } from "services/file.service";
|
|||||||
// ui
|
// ui
|
||||||
// helpers
|
// helpers
|
||||||
import { getChangedIssuefields } from "helpers/issue.helper";
|
import { getChangedIssuefields } from "helpers/issue.helper";
|
||||||
|
import { shouldRenderProject } from "helpers/project.helper";
|
||||||
// types
|
// types
|
||||||
import type { TIssue, ISearchIssueResponse } from "@plane/types";
|
import type { TIssue, ISearchIssueResponse } from "@plane/types";
|
||||||
|
|
||||||
@ -304,7 +305,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||||||
handleFormChange();
|
handleFormChange();
|
||||||
}}
|
}}
|
||||||
buttonVariant="border-with-text"
|
buttonVariant="border-with-text"
|
||||||
// TODO: update tabIndex logic
|
renderCondition={(project) => shouldRenderProject(project)}
|
||||||
tabIndex={getTabIndex("project_id")}
|
tabIndex={getTabIndex("project_id")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import router from "next/router";
|
import router from "next/router";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// components
|
// components
|
||||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
@ -13,7 +14,7 @@ import { useIssues, useLabel, useMember, useProject, useProjectState } from "hoo
|
|||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "./issue-layouts";
|
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "./issue-layouts";
|
||||||
|
|
||||||
export const IssuesMobileHeader = () => {
|
export const IssuesMobileHeader = observer(() => {
|
||||||
const layouts = [
|
const layouts = [
|
||||||
{ key: "list", title: "List", icon: List },
|
{ key: "list", title: "List", icon: List },
|
||||||
{ key: "kanban", title: "Kanban", icon: Kanban },
|
{ key: "kanban", title: "Kanban", icon: Kanban },
|
||||||
@ -87,7 +88,7 @@ export const IssuesMobileHeader = () => {
|
|||||||
onClose={() => setAnalyticsModal(false)}
|
onClose={() => setAnalyticsModal(false)}
|
||||||
projectDetails={currentProjectDetails ?? undefined}
|
projectDetails={currentProjectDetails ?? undefined}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-evenly border-b border-custom-border-200 py-2">
|
<div className="flex justify-evenly border-b border-custom-border-200 py-2 z-[13] bg-custom-background-100">
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
maxHeight={"md"}
|
maxHeight={"md"}
|
||||||
className="flex flex-grow justify-center text-sm text-custom-text-200"
|
className="flex flex-grow justify-center text-sm text-custom-text-200"
|
||||||
@ -164,4 +165,4 @@ export const IssuesMobileHeader = () => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -3,14 +3,16 @@ import { useRouter } from "next/router";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import { Rocket, Search } from "lucide-react";
|
|
||||||
import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
|
||||||
import useDebounce from "hooks/use-debounce";
|
|
||||||
import { ProjectService } from "services/project";
|
import { ProjectService } from "services/project";
|
||||||
// hooks
|
// hooks
|
||||||
|
import useDebounce from "hooks/use-debounce";
|
||||||
import { usePlatformOS } from "hooks/use-platform-os";
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
|
// components
|
||||||
|
import { IssueSearchModalEmptyState } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
|
import { Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
|
import { Rocket, Search } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { ISearchIssueResponse } from "@plane/types";
|
import { ISearchIssueResponse } from "@plane/types";
|
||||||
|
|
||||||
@ -151,15 +153,12 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
</h5>
|
</h5>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isSearching && issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
<IssueSearchModalEmptyState
|
||||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
debouncedSearchTerm={debouncedSearchTerm}
|
||||||
<LayersIcon height="52" width="52" />
|
isSearching={isSearching}
|
||||||
<h3 className="text-custom-text-200">
|
issues={issues}
|
||||||
No issues found. Create a new issue with{" "}
|
searchTerm={searchTerm}
|
||||||
<pre className="inline rounded bg-custom-background-80 px-2 py-1 text-sm">C</pre>.
|
/>
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isSearching ? (
|
{isSearching ? (
|
||||||
<Loader className="space-y-3 p-3">
|
<Loader className="space-y-3 p-3">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { MoveRight, MoveDiagonal, Link2, Trash2, RotateCcw } from "lucide-react";
|
import { MoveRight, MoveDiagonal, Link2, Trash2, RotateCcw } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import {
|
import {
|
||||||
@ -78,8 +78,6 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
|||||||
handleRestoreIssue,
|
handleRestoreIssue,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
} = props;
|
} = props;
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const { currentUser } = useUser();
|
const { currentUser } = useUser();
|
||||||
const {
|
const {
|
||||||
@ -106,15 +104,6 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const redirectToIssueDetail = () => {
|
|
||||||
router.push({ pathname: `/${issueLink}` });
|
|
||||||
removeRoutePeekId();
|
|
||||||
captureEvent(ISSUE_OPENED, {
|
|
||||||
...elementFromPath(router.asPath),
|
|
||||||
element: "peek",
|
|
||||||
mode: "detail",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
// auth
|
// auth
|
||||||
const isArchivingAllowed = !isArchived && !disabled;
|
const isArchivingAllowed = !isArchived && !disabled;
|
||||||
const isInArchivableGroup =
|
const isInArchivableGroup =
|
||||||
@ -132,9 +121,18 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
|||||||
<MoveRight className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
<MoveRight className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button onClick={redirectToIssueDetail}>
|
<Link
|
||||||
|
href={`/${issueLink}`}
|
||||||
|
onClick={() => {
|
||||||
|
removeRoutePeekId();
|
||||||
|
captureEvent(ISSUE_OPENED, {
|
||||||
|
element: "peek",
|
||||||
|
mode: "detail",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
<MoveDiagonal className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
<MoveDiagonal className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||||
</button>
|
</Link>
|
||||||
{currentMode && (
|
{currentMode && (
|
||||||
<div className="flex flex-shrink-0 items-center gap-2">
|
<div className="flex flex-shrink-0 items-center gap-2">
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
export * from "./header";
|
||||||
|
export * from "./issue-attachments";
|
||||||
export * from "./issue-detail";
|
export * from "./issue-detail";
|
||||||
export * from "./properties";
|
export * from "./properties";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
export * from "./view";
|
export * from "./view";
|
||||||
export * from "./header";
|
|
||||||
|
111
web/components/issues/peek-overview/issue-attachments.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
// hooks
|
||||||
|
import { useEventTracker, useIssueDetail } from "hooks/store";
|
||||||
|
// components
|
||||||
|
import { IssueAttachmentUpload, IssueAttachmentsList, TAttachmentOperations } from "components/issues";
|
||||||
|
// ui
|
||||||
|
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
disabled: boolean;
|
||||||
|
issueId: string;
|
||||||
|
projectId: string;
|
||||||
|
workspaceSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PeekOverviewIssueAttachments: React.FC<Props> = (props) => {
|
||||||
|
const { disabled, issueId, projectId, workspaceSlug } = props;
|
||||||
|
// store hooks
|
||||||
|
const { captureIssueEvent } = useEventTracker();
|
||||||
|
const {
|
||||||
|
attachment: { createAttachment, removeAttachment },
|
||||||
|
} = useIssueDetail();
|
||||||
|
|
||||||
|
const handleAttachmentOperations: TAttachmentOperations = useMemo(
|
||||||
|
() => ({
|
||||||
|
create: async (data: FormData) => {
|
||||||
|
try {
|
||||||
|
const attachmentUploadPromise = createAttachment(workspaceSlug, projectId, issueId, data);
|
||||||
|
setPromiseToast(attachmentUploadPromise, {
|
||||||
|
loading: "Uploading attachment...",
|
||||||
|
success: {
|
||||||
|
title: "Attachment uploaded",
|
||||||
|
message: () => "The attachment has been successfully uploaded",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: "Attachment not uploaded",
|
||||||
|
message: () => "The attachment could not be uploaded",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await attachmentUploadPromise;
|
||||||
|
captureIssueEvent({
|
||||||
|
eventName: "Issue attachment added",
|
||||||
|
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||||
|
updates: {
|
||||||
|
changed_property: "attachment",
|
||||||
|
change_details: res.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
captureIssueEvent({
|
||||||
|
eventName: "Issue attachment added",
|
||||||
|
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remove: async (attachmentId: string) => {
|
||||||
|
try {
|
||||||
|
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
|
||||||
|
await removeAttachment(workspaceSlug, projectId, issueId, attachmentId);
|
||||||
|
setToast({
|
||||||
|
message: "The attachment has been successfully removed",
|
||||||
|
type: TOAST_TYPE.SUCCESS,
|
||||||
|
title: "Attachment removed",
|
||||||
|
});
|
||||||
|
captureIssueEvent({
|
||||||
|
eventName: "Issue attachment deleted",
|
||||||
|
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||||
|
updates: {
|
||||||
|
changed_property: "attachment",
|
||||||
|
change_details: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
captureIssueEvent({
|
||||||
|
eventName: "Issue attachment deleted",
|
||||||
|
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||||
|
updates: {
|
||||||
|
changed_property: "attachment",
|
||||||
|
change_details: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setToast({
|
||||||
|
message: "The Attachment could not be removed",
|
||||||
|
type: TOAST_TYPE.ERROR,
|
||||||
|
title: "Attachment not removed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[workspaceSlug, projectId, issueId, captureIssueEvent, createAttachment, removeAttachment]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h6 className="text-sm font-medium">Attachments</h6>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2 mt-3">
|
||||||
|
<IssueAttachmentUpload
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
disabled={disabled}
|
||||||
|
handleAttachmentOperations={handleAttachmentOperations}
|
||||||
|
/>
|
||||||
|
<IssueAttachmentsList
|
||||||
|
issueId={issueId}
|
||||||
|
disabled={disabled}
|
||||||
|
handleAttachmentOperations={handleAttachmentOperations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -55,7 +55,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="space-y-2">
|
||||||
<span className="text-base font-medium text-custom-text-400">
|
<span className="text-base font-medium text-custom-text-400">
|
||||||
{projectDetails?.identifier}-{issue?.sequence_id}
|
{projectDetails?.identifier}-{issue?.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
@ -89,6 +89,6 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
|||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -61,7 +61,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
maxDate?.setDate(maxDate.getDate());
|
maxDate?.setDate(maxDate.getDate());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-1">
|
<div>
|
||||||
<h6 className="text-sm font-medium">Properties</h6>
|
<h6 className="text-sm font-medium">Properties</h6>
|
||||||
{/* TODO: render properties using a common component */}
|
{/* TODO: render properties using a common component */}
|
||||||
<div className={`w-full space-y-2 mt-3 ${disabled ? "opacity-60" : ""}`}>
|
<div className={`w-full space-y-2 mt-3 ${disabled ? "opacity-60" : ""}`}>
|
||||||
|
@ -11,13 +11,15 @@ import {
|
|||||||
PeekOverviewProperties,
|
PeekOverviewProperties,
|
||||||
TIssueOperations,
|
TIssueOperations,
|
||||||
ArchiveIssueModal,
|
ArchiveIssueModal,
|
||||||
|
PeekOverviewIssueAttachments,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail, useUser } from "hooks/store";
|
||||||
import useKeypress from "hooks/use-keypress";
|
import useKeypress from "hooks/use-keypress";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// store hooks
|
// store hooks
|
||||||
import { IssueActivity } from "../issue-detail/issue-activity";
|
import { IssueActivity } from "../issue-detail/issue-activity";
|
||||||
|
import { SubIssuesRoot } from "../sub-issues";
|
||||||
|
|
||||||
interface IIssueView {
|
interface IIssueView {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -37,6 +39,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
// ref
|
// ref
|
||||||
const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
|
const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
|
||||||
// store hooks
|
// store hooks
|
||||||
|
const { currentUser } = useUser();
|
||||||
const {
|
const {
|
||||||
setPeekIssue,
|
setPeekIssue,
|
||||||
isAnyModalOpen,
|
isAnyModalOpen,
|
||||||
@ -147,7 +150,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
issue && (
|
issue && (
|
||||||
<>
|
<>
|
||||||
{["side-peek", "modal"].includes(peekMode) ? (
|
{["side-peek", "modal"].includes(peekMode) ? (
|
||||||
<div className="relative flex flex-col gap-3 px-8 py-5">
|
<div className="relative flex flex-col gap-3 px-8 py-5 space-y-3">
|
||||||
<PeekOverviewIssueDetails
|
<PeekOverviewIssueDetails
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@ -158,6 +161,23 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{currentUser && (
|
||||||
|
<SubIssuesRoot
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
parentIssueId={issueId}
|
||||||
|
currentUser={currentUser}
|
||||||
|
disabled={disabled || is_archived}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PeekOverviewIssueAttachments
|
||||||
|
disabled={disabled || is_archived}
|
||||||
|
issueId={issueId}
|
||||||
|
projectId={projectId}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
|
||||||
<PeekOverviewProperties
|
<PeekOverviewProperties
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@ -169,9 +189,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={`vertical-scrollbar flex h-full w-full overflow-auto`}>
|
<div className="vertical-scrollbar flex h-full w-full overflow-auto">
|
||||||
<div className="relative h-full w-full space-y-6 overflow-auto p-4 py-5">
|
<div className="relative h-full w-full space-y-6 overflow-auto p-4 py-5">
|
||||||
<div>
|
<div className="space-y-3">
|
||||||
<PeekOverviewIssueDetails
|
<PeekOverviewIssueDetails
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@ -182,6 +202,23 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{currentUser && (
|
||||||
|
<SubIssuesRoot
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
parentIssueId={issueId}
|
||||||
|
currentUser={currentUser}
|
||||||
|
disabled={disabled || is_archived}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PeekOverviewIssueAttachments
|
||||||
|
disabled={disabled || is_archived}
|
||||||
|
issueId={issueId}
|
||||||
|
projectId={projectId}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
|
||||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
|
import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
|
||||||
// components
|
// components
|
||||||
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
|
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
|
||||||
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
|
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
|
||||||
@ -11,6 +11,7 @@ import { IssueProperty } from "./properties";
|
|||||||
// ui
|
// ui
|
||||||
// types
|
// types
|
||||||
import { TSubIssueOperations } from "./root";
|
import { TSubIssueOperations } from "./root";
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
// import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
// import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
||||||
|
|
||||||
export interface ISubIssues {
|
export interface ISubIssues {
|
||||||
@ -90,11 +91,12 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
|||||||
setSubIssueHelpers(parentIssueId, "issue_visibility", issueId);
|
setSubIssueHelpers(parentIssueId, "issue_visibility", issueId);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{subIssueHelpers.issue_visibility.includes(issue.id) ? (
|
<ChevronRight
|
||||||
<ChevronDown width={14} strokeWidth={2} />
|
className={cn("h-3 w-3 transition-all", {
|
||||||
) : (
|
"rotate-90": subIssueHelpers.issue_visibility.includes(issue.id),
|
||||||
<ChevronRight width={14} strokeWidth={2} />
|
})}
|
||||||
)}
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Plus, ChevronRight, ChevronDown, Loader } from "lucide-react";
|
import { Plus, ChevronRight, Loader, Pencil } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
import { CircularProgressIndicator, CustomMenu, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
import { ExistingIssuesListModal } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
@ -11,9 +11,9 @@ import { useEventTracker, useIssueDetail } from "hooks/store";
|
|||||||
// components
|
// components
|
||||||
import { IUser, TIssue } from "@plane/types";
|
import { IUser, TIssue } from "@plane/types";
|
||||||
import { IssueList } from "./issues-list";
|
import { IssueList } from "./issues-list";
|
||||||
import { ProgressBar } from "./progressbar";
|
|
||||||
// constants
|
// constants
|
||||||
import { E_ISSUE_DETAILS } from "constants/event-tracker";
|
import { E_ISSUE_DETAILS } from "constants/event-tracker";
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
// ui
|
// ui
|
||||||
// helpers
|
// helpers
|
||||||
// types
|
// types
|
||||||
@ -55,6 +55,10 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
updateSubIssue,
|
updateSubIssue,
|
||||||
removeSubIssue,
|
removeSubIssue,
|
||||||
deleteSubIssue,
|
deleteSubIssue,
|
||||||
|
isCreateIssueModalOpen,
|
||||||
|
toggleCreateIssueModal,
|
||||||
|
isSubIssuesModalOpen,
|
||||||
|
toggleSubIssuesModal,
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const { setTrackElement, captureIssueEvent } = useEventTracker();
|
const { setTrackElement, captureIssueEvent } = useEventTracker();
|
||||||
// state
|
// state
|
||||||
@ -323,55 +327,81 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
<>
|
<>
|
||||||
{subIssues && subIssues?.length > 0 ? (
|
{subIssues && subIssues?.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="relative flex items-center gap-4 text-xs">
|
<div className="relative flex items-center justify-between gap-2 text-xs">
|
||||||
<div
|
<div className="flex items-center gap-2">
|
||||||
className="flex cursor-pointer select-none items-center gap-1 rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80"
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1 rounded py-1 px-2 transition-all hover:bg-custom-background-80 font-medium"
|
||||||
onClick={handleFetchSubIssues}
|
onClick={handleFetchSubIssues}
|
||||||
>
|
>
|
||||||
<div className="flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center">
|
<div className="flex flex-shrink-0 items-center justify-center">
|
||||||
{subIssueHelpers.preview_loader.includes(parentIssueId) ? (
|
{subIssueHelpers.preview_loader.includes(parentIssueId) ? (
|
||||||
<Loader width={14} strokeWidth={2} className="animate-spin" />
|
<Loader strokeWidth={2} className="h-3 w-3 animate-spin" />
|
||||||
) : subIssueHelpers.issue_visibility.includes(parentIssueId) ? (
|
|
||||||
<ChevronDown width={16} strokeWidth={2} />
|
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight width={14} strokeWidth={2} />
|
<ChevronRight
|
||||||
|
className={cn("h-3 w-3 transition-all", {
|
||||||
|
"rotate-90": subIssueHelpers.issue_visibility.includes(parentIssueId),
|
||||||
|
})}
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>Sub-issues</div>
|
<div>Sub-issues</div>
|
||||||
<div>({subIssues?.length || 0})</div>
|
</button>
|
||||||
</div>
|
<div className="flex items-center gap-2 text-custom-text-300">
|
||||||
|
<CircularProgressIndicator
|
||||||
<div className="w-full max-w-[250px] select-none">
|
size={16}
|
||||||
<ProgressBar
|
percentage={
|
||||||
total={subIssues?.length || 0}
|
subIssuesDistribution?.completed?.length && subIssues.length
|
||||||
done={
|
? (subIssuesDistribution?.completed?.length / subIssues.length) * 100
|
||||||
((subIssuesDistribution?.cancelled ?? []).length || 0) +
|
: 0
|
||||||
((subIssuesDistribution?.completed ?? []).length || 0)
|
|
||||||
}
|
}
|
||||||
|
strokeWidth={3}
|
||||||
/>
|
/>
|
||||||
|
<span>
|
||||||
|
{subIssuesDistribution?.completed?.length ?? 0}/{subIssues.length} Done
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<div className="ml-auto flex flex-shrink-0 select-none items-center gap-2">
|
<CustomMenu
|
||||||
<div
|
label={
|
||||||
className="cursor-pointer rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80"
|
<>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
Add sub-issue
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
buttonClassName="whitespace-nowrap"
|
||||||
|
placement="bottom-end"
|
||||||
|
noBorder
|
||||||
|
noChevron
|
||||||
|
>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement(E_ISSUE_DETAILS);
|
setTrackElement(E_ISSUE_DETAILS);
|
||||||
handleIssueCrudState("create", parentIssueId, null);
|
handleIssueCrudState("create", parentIssueId, null);
|
||||||
|
toggleCreateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add sub-issue
|
<div className="flex items-center gap-2">
|
||||||
|
<Pencil className="h-3 w-3" />
|
||||||
|
<span>Create new</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</CustomMenu.MenuItem>
|
||||||
className="cursor-pointer rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80"
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement(E_ISSUE_DETAILS);
|
setTrackElement(E_ISSUE_DETAILS);
|
||||||
handleIssueCrudState("existing", parentIssueId, null);
|
handleIssueCrudState("existing", parentIssueId, null);
|
||||||
|
toggleSubIssuesModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add an existing issue
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<LayersIcon className="h-3 w-3" />
|
||||||
|
<span>Add existing</span>
|
||||||
</div>
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -392,8 +422,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
) : (
|
) : (
|
||||||
!disabled && (
|
!disabled && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="py-2 text-xs italic text-custom-text-300">No Sub-Issues yet</div>
|
<div className="text-xs italic text-custom-text-300">No sub-issues yet</div>
|
||||||
<div>
|
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
@ -410,44 +439,57 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement(E_ISSUE_DETAILS);
|
setTrackElement(E_ISSUE_DETAILS);
|
||||||
handleIssueCrudState("create", parentIssueId, null);
|
handleIssueCrudState("create", parentIssueId, null);
|
||||||
|
toggleCreateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Create new
|
<div className="flex items-center gap-2">
|
||||||
|
<Pencil className="h-3 w-3" />
|
||||||
|
<span>Create new</span>
|
||||||
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement(E_ISSUE_DETAILS);
|
setTrackElement(E_ISSUE_DETAILS);
|
||||||
handleIssueCrudState("existing", parentIssueId, null);
|
handleIssueCrudState("existing", parentIssueId, null);
|
||||||
|
toggleSubIssuesModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add an existing issue
|
<div className="flex items-center gap-2">
|
||||||
|
<LayersIcon className="h-3 w-3" />
|
||||||
|
<span>Add existing</span>
|
||||||
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* issue create, add from existing , update and delete modals */}
|
{/* issue create, add from existing , update and delete modals */}
|
||||||
{issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && (
|
{issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && isCreateIssueModalOpen && (
|
||||||
<CreateUpdateIssueModal
|
<CreateUpdateIssueModal
|
||||||
isOpen={issueCrudState?.create?.toggle}
|
isOpen={issueCrudState?.create?.toggle}
|
||||||
data={{
|
data={{
|
||||||
parent_id: issueCrudState?.create?.parentIssueId,
|
parent_id: issueCrudState?.create?.parentIssueId,
|
||||||
}}
|
}}
|
||||||
onClose={() => handleIssueCrudState("create", null, null)}
|
onClose={() => {
|
||||||
|
handleIssueCrudState("create", null, null);
|
||||||
|
toggleCreateIssueModal(false);
|
||||||
|
}}
|
||||||
onSubmit={async (_issue: TIssue) => {
|
onSubmit={async (_issue: TIssue) => {
|
||||||
await subIssueOperations.addSubIssue(workspaceSlug, projectId, parentIssueId, [_issue.id]);
|
await subIssueOperations.addSubIssue(workspaceSlug, projectId, parentIssueId, [_issue.id]);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && (
|
{issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && isSubIssuesModalOpen && (
|
||||||
<ExistingIssuesListModal
|
<ExistingIssuesListModal
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
isOpen={issueCrudState?.existing?.toggle}
|
isOpen={issueCrudState?.existing?.toggle}
|
||||||
handleClose={() => handleIssueCrudState("existing", null, null)}
|
handleClose={() => {
|
||||||
|
handleIssueCrudState("existing", null, null);
|
||||||
|
toggleSubIssuesModal(false);
|
||||||
|
}}
|
||||||
searchParams={{ sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId }}
|
searchParams={{ sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId }}
|
||||||
handleOnSubmit={(_issue) =>
|
handleOnSubmit={(_issue) =>
|
||||||
subIssueOperations.addSubIssue(
|
subIssueOperations.addSubIssue(
|
||||||
|
@ -7,6 +7,7 @@ import { ModuleStatusSelect } from "components/modules";
|
|||||||
// ui
|
// ui
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||||
|
import { shouldRenderProject } from "helpers/project.helper";
|
||||||
// types
|
// types
|
||||||
import { IModule } from "@plane/types";
|
import { IModule } from "@plane/types";
|
||||||
|
|
||||||
@ -78,6 +79,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
|
|||||||
setActiveProject(val);
|
setActiveProject(val);
|
||||||
}}
|
}}
|
||||||
buttonVariant="border-with-text"
|
buttonVariant="border-with-text"
|
||||||
|
renderCondition={(project) => shouldRenderProject(project)}
|
||||||
tabIndex={10}
|
tabIndex={10}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
40
web/components/modules/moduels-list-mobile-header.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { CustomMenu } from "@plane/ui";
|
||||||
|
import { MODULE_VIEW_LAYOUTS } from "constants/module";
|
||||||
|
import { useModuleFilter, useProject } from "hooks/store";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
|
||||||
|
const ModulesListMobileHeader = observer(() => {
|
||||||
|
const { currentProjectDetails } = useProject();
|
||||||
|
const { updateDisplayFilters } = useModuleFilter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center md:hidden">
|
||||||
|
<CustomMenu
|
||||||
|
maxHeight={"md"}
|
||||||
|
className="flex flex-grow justify-center text-custom-text-200 text-sm py-2 border-b border-custom-border-200 bg-custom-sidebar-background-100"
|
||||||
|
// 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 items-center text-custom-text-200 text-sm"
|
||||||
|
closeOnSelect
|
||||||
|
>
|
||||||
|
{MODULE_VIEW_LAYOUTS.map((layout) => {
|
||||||
|
if (layout.key == "gantt") return;
|
||||||
|
return (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={layout.key}
|
||||||
|
onClick={() => {
|
||||||
|
updateDisplayFilters(currentProjectDetails!.id.toString(), { layout: layout.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>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ModulesListMobileHeader;
|
@ -1,14 +1,21 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
import router from "next/router";
|
import router from "next/router";
|
||||||
|
// icons
|
||||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||||
|
// ui
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
|
// components
|
||||||
import { ProjectAnalyticsModal } from "components/analytics";
|
import { ProjectAnalyticsModal } from "components/analytics";
|
||||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues";
|
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues";
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue";
|
// hooks
|
||||||
import { useIssues, useLabel, useMember, useModule, useProjectState } from "hooks/store";
|
import { useIssues, useLabel, useMember, useModule, useProjectState } from "hooks/store";
|
||||||
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue";
|
||||||
|
|
||||||
export const ModuleMobileHeader = () => {
|
export const ModuleMobileHeader = observer(() => {
|
||||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||||
const { getModuleById } = useModule();
|
const { getModuleById } = useModule();
|
||||||
const layouts = [
|
const layouts = [
|
||||||
@ -83,7 +90,7 @@ export const ModuleMobileHeader = () => {
|
|||||||
onClose={() => setAnalyticsModal(false)}
|
onClose={() => setAnalyticsModal(false)}
|
||||||
moduleDetails={moduleDetails ?? undefined}
|
moduleDetails={moduleDetails ?? undefined}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-evenly border-b border-custom-border-200 py-2">
|
<div className="flex justify-evenly border-b border-custom-border-200 bg-custom-background-100 py-2">
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
maxHeight={"md"}
|
maxHeight={"md"}
|
||||||
className="flex flex-grow justify-center text-sm text-custom-text-200"
|
className="flex flex-grow justify-center text-sm text-custom-text-200"
|
||||||
@ -161,4 +168,4 @@ export const ModuleMobileHeader = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -39,7 +39,7 @@ export const ModulePeekOverview: React.FC<Props> = observer(({ projectId, worksp
|
|||||||
{peekModule && (
|
{peekModule && (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="flex h-full w-full max-w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-3.5 duration-300 absolute md:relative right-0 z-[9]"
|
className="flex h-full w-full max-w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 duration-300 absolute md:relative right-0 z-[9]"
|
||||||
style={{
|
style={{
|
||||||
boxShadow:
|
boxShadow:
|
||||||
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
|
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
import { Bell } from "lucide-react";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { Tooltip } from "@plane/ui";
|
|
||||||
import { EmptyState } from "components/common";
|
|
||||||
import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications";
|
|
||||||
import { NotificationsLoader } from "components/ui";
|
|
||||||
import { getNumberCount } from "helpers/string.helper";
|
|
||||||
import { useApplication } from "hooks/store";
|
import { useApplication } from "hooks/store";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
import useUserNotification from "hooks/use-user-notifications";
|
import useUserNotification from "hooks/use-user-notifications";
|
||||||
import { usePlatformOS } from "hooks/use-platform-os";
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
|
// icons
|
||||||
|
import { Bell } from "lucide-react";
|
||||||
// components
|
// components
|
||||||
// images
|
import { Tooltip } from "@plane/ui";
|
||||||
import emptyNotification from "public/empty-state/notification.svg";
|
import { EmptyState } from "components/empty-state";
|
||||||
|
import { NotificationsLoader } from "components/ui";
|
||||||
|
import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications";
|
||||||
|
// constants
|
||||||
|
import { EmptyStateType } from "constants/empty-state";
|
||||||
// helpers
|
// helpers
|
||||||
|
import { getNumberCount } from "helpers/string.helper";
|
||||||
|
|
||||||
export const NotificationPopover = observer(() => {
|
export const NotificationPopover = observer(() => {
|
||||||
// states
|
// states
|
||||||
@ -59,6 +60,16 @@ export const NotificationPopover = observer(() => {
|
|||||||
if (selectedNotificationForSnooze === null) setIsActive(false);
|
if (selectedNotificationForSnooze === null) setIsActive(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const currentTabEmptyState = snoozed
|
||||||
|
? EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE
|
||||||
|
: archived
|
||||||
|
? EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE
|
||||||
|
: selectedTab === "created"
|
||||||
|
? EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE
|
||||||
|
: selectedTab === "watching"
|
||||||
|
? EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE
|
||||||
|
: EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SnoozeNotificationModal
|
<SnoozeNotificationModal
|
||||||
@ -70,7 +81,13 @@ export const NotificationPopover = observer(() => {
|
|||||||
/>
|
/>
|
||||||
<Popover ref={notificationPopoverRef} className="md:relative w-full">
|
<Popover ref={notificationPopoverRef} className="md:relative w-full">
|
||||||
<>
|
<>
|
||||||
<Tooltip tooltipContent="Notifications" position="right" className="ml-2" disabled={!isSidebarCollapsed} isMobile={isMobile}>
|
<Tooltip
|
||||||
|
tooltipContent="Notifications"
|
||||||
|
position="right"
|
||||||
|
className="ml-2"
|
||||||
|
disabled={!isSidebarCollapsed}
|
||||||
|
isMobile={isMobile}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className={`group relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
className={`group relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
||||||
isActive
|
isActive
|
||||||
@ -186,11 +203,7 @@ export const NotificationPopover = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid h-full w-full scale-75 place-items-center overflow-hidden">
|
<div className="grid h-full w-full scale-75 place-items-center overflow-hidden">
|
||||||
<EmptyState
|
<EmptyState type={currentTabEmptyState} layout="screen-simple" />
|
||||||
title="You're updated with all the notifications"
|
|
||||||
description="You have read all the notifications."
|
|
||||||
image={emptyNotification}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,15 +1,8 @@
|
|||||||
import { useEffect, Fragment, FC, useState } from "react";
|
import { useEffect, Fragment, FC, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// ui
|
|
||||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
|
||||||
// components
|
// components
|
||||||
import { CreateProjectForm } from "./create-project-form";
|
import { CreateProjectForm } from "./create-project-form";
|
||||||
import { ProjectFeatureUpdate } from "./project-feature-update";
|
import { ProjectFeatureUpdate } from "./project-feature-update";
|
||||||
// constants
|
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
|
||||||
// hooks
|
|
||||||
import { useUser } from "hooks/store";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -23,32 +16,11 @@ enum EProjectCreationSteps {
|
|||||||
FEATURE_SELECTION = "FEATURE_SELECTION",
|
FEATURE_SELECTION = "FEATURE_SELECTION",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IIsGuestCondition {
|
export const CreateProjectModal: FC<Props> = (props) => {
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const IsGuestCondition: FC<IIsGuestCondition> = ({ onClose }) => {
|
|
||||||
useEffect(() => {
|
|
||||||
onClose();
|
|
||||||
setToast({
|
|
||||||
title: "Error",
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
message: "You don't have permission to create project.",
|
|
||||||
});
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CreateProjectModal: FC<Props> = observer((props) => {
|
|
||||||
const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props;
|
const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props;
|
||||||
// states
|
// states
|
||||||
const [currentStep, setCurrentStep] = useState<EProjectCreationSteps>(EProjectCreationSteps.CREATE_PROJECT);
|
const [currentStep, setCurrentStep] = useState<EProjectCreationSteps>(EProjectCreationSteps.CREATE_PROJECT);
|
||||||
const [createdProjectId, setCreatedProjectId] = useState<string | null>(null);
|
const [createdProjectId, setCreatedProjectId] = useState<string | null>(null);
|
||||||
// hooks
|
|
||||||
const {
|
|
||||||
membership: { currentWorkspaceRole },
|
|
||||||
} = useUser();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@ -57,9 +29,6 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
|||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
if (currentWorkspaceRole && isOpen)
|
|
||||||
if (currentWorkspaceRole < EUserWorkspaceRoles.MEMBER) return <IsGuestCondition onClose={onClose} />;
|
|
||||||
|
|
||||||
const handleNextStep = (projectId: string) => {
|
const handleNextStep = (projectId: string) => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
setCreatedProjectId(projectId);
|
setCreatedProjectId(projectId);
|
||||||
@ -111,4 +80,4 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
@ -55,7 +55,8 @@ export const WorkspaceSidebarMenu = observer(() => {
|
|||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
>
|
>
|
||||||
<div
|
<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}`)
|
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"
|
? "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"
|
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||||
} ${themeStore?.sidebarCollapsed ? "justify-center" : ""}`}
|
} ${themeStore?.sidebarCollapsed ? "justify-center" : ""}`}
|
||||||
@ -67,7 +68,7 @@ export const WorkspaceSidebarMenu = observer(() => {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<p className="leading-5">{!themeStore?.sidebarCollapsed && link.label}</p>
|
{!themeStore?.sidebarCollapsed && <p className="leading-5">{link.label}</p>}
|
||||||
{!themeStore?.sidebarCollapsed && link.key === "active-cycles" && (
|
{!themeStore?.sidebarCollapsed && link.key === "active-cycles" && (
|
||||||
<Crown className="h-3.5 w-3.5 text-amber-400" />
|
<Crown className="h-3.5 w-3.5 text-amber-400" />
|
||||||
)}
|
)}
|
||||||
|
@ -59,7 +59,6 @@ export enum EmptyStateType {
|
|||||||
PROJECT_DRAFT_NO_ISSUES = "project-draft-no-issues",
|
PROJECT_DRAFT_NO_ISSUES = "project-draft-no-issues",
|
||||||
VIEWS_EMPTY_SEARCH = "views-empty-search",
|
VIEWS_EMPTY_SEARCH = "views-empty-search",
|
||||||
PROJECTS_EMPTY_SEARCH = "projects-empty-search",
|
PROJECTS_EMPTY_SEARCH = "projects-empty-search",
|
||||||
COMMANDK_EMPTY_SEARCH = "commandK-empty-search",
|
|
||||||
MEMBERS_EMPTY_SEARCH = "members-empty-search",
|
MEMBERS_EMPTY_SEARCH = "members-empty-search",
|
||||||
PROJECT_MODULE_ISSUES = "project-module-issues",
|
PROJECT_MODULE_ISSUES = "project-module-issues",
|
||||||
PROJECT_MODULE = "project-module",
|
PROJECT_MODULE = "project-module",
|
||||||
@ -71,6 +70,18 @@ export enum EmptyStateType {
|
|||||||
PROJECT_PAGE_SHARED = "project-page-shared",
|
PROJECT_PAGE_SHARED = "project-page-shared",
|
||||||
PROJECT_PAGE_ARCHIVED = "project-page-archived",
|
PROJECT_PAGE_ARCHIVED = "project-page-archived",
|
||||||
PROJECT_PAGE_RECENT = "project-page-recent",
|
PROJECT_PAGE_RECENT = "project-page-recent",
|
||||||
|
|
||||||
|
COMMAND_K_SEARCH_EMPTY_STATE = "command-k-search-empty-state",
|
||||||
|
ISSUE_RELATION_SEARCH_EMPTY_STATE = "issue-relation-search-empty-state",
|
||||||
|
ISSUE_RELATION_EMPTY_STATE = "issue-relation-empty-state",
|
||||||
|
ISSUE_COMMENT_EMPTY_STATE = "issue-comment-empty-state",
|
||||||
|
|
||||||
|
NOTIFICATION_MY_ISSUE_EMPTY_STATE = "notification-my-issues-empty-state",
|
||||||
|
NOTIFICATION_CREATED_EMPTY_STATE = "notification-created-empty-state",
|
||||||
|
NOTIFICATION_SUBSCRIBED_EMPTY_STATE = "notification-subscribed-empty-state",
|
||||||
|
NOTIFICATION_ARCHIVED_EMPTY_STATE = "notification-archived-empty-state",
|
||||||
|
NOTIFICATION_SNOOZED_EMPTY_STATE = "notification-snoozed-empty-state",
|
||||||
|
NOTIFICATION_UNREAD_EMPTY_STATE = "notification-unread-empty-state",
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyStateDetails = {
|
const emptyStateDetails = {
|
||||||
@ -384,11 +395,6 @@ const emptyStateDetails = {
|
|||||||
description: "No projects detected with the matching criteria. Create a new project instead.",
|
description: "No projects detected with the matching criteria. Create a new project instead.",
|
||||||
path: "/empty-state/search/project",
|
path: "/empty-state/search/project",
|
||||||
},
|
},
|
||||||
[EmptyStateType.COMMANDK_EMPTY_SEARCH]: {
|
|
||||||
key: EmptyStateType.COMMANDK_EMPTY_SEARCH,
|
|
||||||
title: "No results found. ",
|
|
||||||
path: "/empty-state/search/search",
|
|
||||||
},
|
|
||||||
[EmptyStateType.MEMBERS_EMPTY_SEARCH]: {
|
[EmptyStateType.MEMBERS_EMPTY_SEARCH]: {
|
||||||
key: EmptyStateType.MEMBERS_EMPTY_SEARCH,
|
key: EmptyStateType.MEMBERS_EMPTY_SEARCH,
|
||||||
title: "No matching members",
|
title: "No matching members",
|
||||||
@ -504,6 +510,66 @@ const emptyStateDetails = {
|
|||||||
accessType: "project",
|
accessType: "project",
|
||||||
access: EUserProjectRoles.MEMBER,
|
access: EUserProjectRoles.MEMBER,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[EmptyStateType.COMMAND_K_SEARCH_EMPTY_STATE]: {
|
||||||
|
key: EmptyStateType.COMMAND_K_SEARCH_EMPTY_STATE,
|
||||||
|
title: "No results found",
|
||||||
|
path: "/empty-state/search/search",
|
||||||
|
},
|
||||||
|
[EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE]: {
|
||||||
|
key: EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE,
|
||||||
|
title: "No maching issues found",
|
||||||
|
path: "/empty-state/search/search",
|
||||||
|
},
|
||||||
|
[EmptyStateType.ISSUE_RELATION_EMPTY_STATE]: {
|
||||||
|
key: EmptyStateType.ISSUE_RELATION_EMPTY_STATE,
|
||||||
|
title: "No issues found",
|
||||||
|
path: "/empty-state/search/issues",
|
||||||
|
},
|
||||||
|
[EmptyStateType.ISSUE_COMMENT_EMPTY_STATE]: {
|
||||||
|
key: EmptyStateType.ISSUE_COMMENT_EMPTY_STATE,
|
||||||
|
title: "No comments yet",
|
||||||
|
description: "Comments can be used as a discussion and follow-up space for the issues",
|
||||||
|
path: "/empty-state/search/comments",
|
||||||
|
},
|
||||||
|
|
||||||
|
[EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE]: {
|
||||||
|
key: EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE,
|
||||||
|
title: "No issues assigned",
|
||||||
|
description: "Updates for issues assigned to you can be \n seen here",
|
||||||
|
path: "/empty-state/search/notification",
|
||||||
|
},
|
||||||
|
|
||||||
|
[EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE]: {
|
||||||
|
key: EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE,
|
||||||
|
title: "No updates to issues",
|
||||||
|
description: "Updates to issues created by you can be \n seen here",
|
||||||
|
path: "/empty-state/search/notification",
|
||||||
|
},
|
||||||
|
[EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE]: {
|
||||||
|
key: EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE,
|
||||||
|
title: "No updates to issues",
|
||||||
|
description: "Updates to any issue you are \n subscribed to can be seen here",
|
||||||
|
path: "/empty-state/search/notification",
|
||||||
|
},
|
||||||
|
[EmptyStateType.NOTIFICATION_UNREAD_EMPTY_STATE]: {
|
||||||
|
key: EmptyStateType.NOTIFICATION_UNREAD_EMPTY_STATE,
|
||||||
|
title: "No unread notifications",
|
||||||
|
description: "Congratulations, you are up-to-date \n with everything happening in the issues \n you care about",
|
||||||
|
path: "/empty-state/search/notification",
|
||||||
|
},
|
||||||
|
[EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE]: {
|
||||||
|
key: EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE,
|
||||||
|
title: "No snoozed notifications yet",
|
||||||
|
description: "Any notification you snooze for later will \n be available here to act upon",
|
||||||
|
path: "/empty-state/search/snooze",
|
||||||
|
},
|
||||||
|
[EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE]: {
|
||||||
|
key: EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE,
|
||||||
|
title: "No archived notifications yet",
|
||||||
|
description: "Any notification you archive will be \n available here to help you focus",
|
||||||
|
path: "/empty-state/search/archive",
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const EMPTY_STATE_DETAILS: Record<EmptyStateType, EmptyStateDetails> = emptyStateDetails;
|
export const EMPTY_STATE_DETAILS: Record<EmptyStateType, EmptyStateDetails> = emptyStateDetails;
|
||||||
|
@ -3,6 +3,8 @@ import sortBy from "lodash/sortBy";
|
|||||||
import { satisfiesDateFilter } from "helpers/filter.helper";
|
import { satisfiesDateFilter } from "helpers/filter.helper";
|
||||||
// types
|
// types
|
||||||
import { IProject, TProjectDisplayFilters, TProjectFilters, TProjectOrderByOptions } from "@plane/types";
|
import { IProject, TProjectDisplayFilters, TProjectFilters, TProjectOrderByOptions } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the sort order of the project.
|
* Updates the sort order of the project.
|
||||||
@ -51,6 +53,14 @@ export const orderJoinedProjects = (
|
|||||||
export const projectIdentifierSanitizer = (identifier: string): string =>
|
export const projectIdentifierSanitizer = (identifier: string): string =>
|
||||||
identifier.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, "");
|
identifier.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, "");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Checks if the project should be rendered or not based on the user role
|
||||||
|
* @param {IProject} project
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const shouldRenderProject = (project: IProject): boolean =>
|
||||||
|
!!project.member_role && project.member_role >= EUserProjectRoles.MEMBER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description filters projects based on the filter
|
* @description filters projects based on the filter
|
||||||
* @param {IProject} project
|
* @param {IProject} project
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
import { FC, ReactNode } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
// layouts
|
// layouts
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
|
||||||
import { CommandPalette } from "components/command-palette";
|
import { CommandPalette } from "components/command-palette";
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
|
||||||
import { useIssues } from "hooks/store/use-issues";
|
|
||||||
import { UserAuthWrapper, WorkspaceAuthWrapper, ProjectAuthWrapper } from "layouts/auth-layout";
|
import { UserAuthWrapper, WorkspaceAuthWrapper, ProjectAuthWrapper } from "layouts/auth-layout";
|
||||||
// components
|
// components
|
||||||
import { AppSidebar } from "./sidebar";
|
import { AppSidebar } from "./sidebar";
|
||||||
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
|
|
||||||
export interface IAppLayout {
|
export interface IAppLayout {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
header: ReactNode;
|
header: ReactNode;
|
||||||
withProjectWrapper?: boolean;
|
withProjectWrapper?: boolean;
|
||||||
|
mobileHeader?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppLayout: FC<IAppLayout> = observer((props) => {
|
export const AppLayout: FC<IAppLayout> = observer((props) => {
|
||||||
const { children, header, withProjectWrapper = false } = props;
|
const { children, header, withProjectWrapper = false, mobileHeader } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -26,7 +25,15 @@ export const AppLayout: FC<IAppLayout> = observer((props) => {
|
|||||||
<div className="relative flex h-screen w-full overflow-hidden">
|
<div className="relative flex h-screen w-full overflow-hidden">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||||
{header}
|
<div className="z-[15]">
|
||||||
|
<div className="flex items-center w-full border-b border-custom-border-200 z-10">
|
||||||
|
<div className="pl-5 py-4 bg-custom-sidebar-background-100 block md:hidden">
|
||||||
|
<SidebarHamburgerToggle />
|
||||||
|
</div>
|
||||||
|
<div className="w-full">{header}</div>
|
||||||
|
</div>
|
||||||
|
{mobileHeader && mobileHeader}
|
||||||
|
</div>
|
||||||
<div className="h-full w-full overflow-hidden">
|
<div className="h-full w-full overflow-hidden">
|
||||||
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
|
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
|
||||||
{withProjectWrapper ? <ProjectAuthWrapper>{children}</ProjectAuthWrapper> : <>{children}</>}
|
{withProjectWrapper ? <ProjectAuthWrapper>{children}</ProjectAuthWrapper> : <>{children}</>}
|
||||||
|
@ -54,12 +54,12 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
|
|||||||
currentUser?.id === userId && !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
currentUser?.id === userId && !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full px-5 py-5 md:px-9 flex flex-col overflow-hidden">
|
<div className="h-full w-full py-5 flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2 px-5 md:px-9">
|
||||||
<h3 className="text-lg font-medium">Recent activity</h3>
|
<h3 className="text-lg font-medium">Recent activity</h3>
|
||||||
{canDownloadActivity && <DownloadActivityButton />}
|
{canDownloadActivity && <DownloadActivityButton />}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full flex flex-col overflow-y-auto">
|
<div className="h-full flex flex-col overflow-y-auto vertical-scrollbar scrollbar-md px-5 md:px-9">
|
||||||
{activityPages}
|
{activityPages}
|
||||||
{pageCount < totalPages && resultsCount !== 0 && (
|
{pageCount < totalPages && resultsCount !== 0 && (
|
||||||
<div className="flex items-center justify-center text-xs w-full">
|
<div className="flex items-center justify-center text-xs w-full">
|
||||||
|
@ -17,6 +17,7 @@ import { AppLayout } from "layouts/app-layout";
|
|||||||
// assets
|
// assets
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
import emptyCycle from "public/empty-state/cycle.svg";
|
import emptyCycle from "public/empty-state/cycle.svg";
|
||||||
|
import { CycleMobileHeader } from "components/cycles/cycle-mobile-header";
|
||||||
// types
|
// types
|
||||||
|
|
||||||
const CycleDetailPage: NextPageWithLayout = observer(() => {
|
const CycleDetailPage: NextPageWithLayout = observer(() => {
|
||||||
@ -85,7 +86,7 @@ const CycleDetailPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
CycleDetailPage.getLayout = function getLayout(page: ReactElement) {
|
CycleDetailPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<CycleIssuesHeader />} withProjectWrapper>
|
<AppLayout header={<CycleIssuesHeader />} mobileHeader={<CycleMobileHeader />} withProjectWrapper>
|
||||||
{page}
|
{page}
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
@ -28,6 +28,7 @@ import { TCycleFilters } from "@plane/types";
|
|||||||
import { CYCLE_TABS_LIST } from "constants/cycle";
|
import { CYCLE_TABS_LIST } from "constants/cycle";
|
||||||
import { EmptyStateType } from "constants/empty-state";
|
import { EmptyStateType } from "constants/empty-state";
|
||||||
import { E_CYCLES_EMPTY_STATE } from "constants/event-tracker";
|
import { E_CYCLES_EMPTY_STATE } from "constants/event-tracker";
|
||||||
|
import CyclesListMobileHeader from "components/cycles/cycles-list-mobile-header";
|
||||||
|
|
||||||
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||||
// states
|
// states
|
||||||
@ -48,7 +49,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined;
|
const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined;
|
||||||
// selected display filters
|
// selected display filters
|
||||||
const cycleTab = currentProjectDisplayFilters?.active_tab;
|
const cycleTab = currentProjectDisplayFilters?.active_tab;
|
||||||
const cycleLayout = currentProjectDisplayFilters?.layout;
|
const cycleLayout = currentProjectDisplayFilters?.layout ?? "list";
|
||||||
|
|
||||||
const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => {
|
const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
@ -121,14 +122,12 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
<ActiveCycleRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
<ActiveCycleRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||||
{cycleTab && cycleLayout && (
|
|
||||||
<CyclesView
|
<CyclesView
|
||||||
layout={cycleLayout}
|
layout={cycleLayout}
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
peekCycle={peekCycle?.toString()}
|
peekCycle={peekCycle?.toString()}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
@ -140,7 +139,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
ProjectCyclesPage.getLayout = function getLayout(page: ReactElement) {
|
ProjectCyclesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<CyclesHeader />} withProjectWrapper>
|
<AppLayout header={<CyclesHeader />} mobileHeader={<CyclesListMobileHeader />} withProjectWrapper>
|
||||||
{page}
|
{page}
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
@ -10,6 +10,7 @@ import { ProjectLayoutRoot } from "components/issues";
|
|||||||
import { useProject } from "hooks/store";
|
import { useProject } from "hooks/store";
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
import { IssuesMobileHeader } from "components/issues/issues-mobile-header";
|
||||||
// layouts
|
// layouts
|
||||||
// hooks
|
// hooks
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ const ProjectIssuesPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
ProjectIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ProjectIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<ProjectIssuesHeader />} withProjectWrapper>
|
<AppLayout header={<ProjectIssuesHeader />} mobileHeader={<IssuesMobileHeader/>} withProjectWrapper>
|
||||||
{page}
|
{page}
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
@ -16,6 +16,7 @@ import { AppLayout } from "layouts/app-layout";
|
|||||||
// assets
|
// assets
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
import emptyModule from "public/empty-state/module.svg";
|
import emptyModule from "public/empty-state/module.svg";
|
||||||
|
import { ModuleMobileHeader } from "components/modules/module-mobile-header";
|
||||||
// types
|
// types
|
||||||
|
|
||||||
const ModuleIssuesPage: NextPageWithLayout = observer(() => {
|
const ModuleIssuesPage: NextPageWithLayout = observer(() => {
|
||||||
@ -83,7 +84,7 @@ const ModuleIssuesPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
ModuleIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ModuleIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<ModuleIssuesHeader />} withProjectWrapper>
|
<AppLayout header={<ModuleIssuesHeader />} mobileHeader={<ModuleMobileHeader/>} withProjectWrapper>
|
||||||
{page}
|
{page}
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
@ -13,6 +13,7 @@ import { AppLayout } from "layouts/app-layout";
|
|||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
import { calculateTotalFilters } from "helpers/filter.helper";
|
import { calculateTotalFilters } from "helpers/filter.helper";
|
||||||
import { TModuleFilters } from "@plane/types";
|
import { TModuleFilters } from "@plane/types";
|
||||||
|
import ModulesListMobileHeader from "components/modules/moduels-list-mobile-header";
|
||||||
|
|
||||||
const ProjectModulesPage: NextPageWithLayout = observer(() => {
|
const ProjectModulesPage: NextPageWithLayout = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -59,7 +60,7 @@ const ProjectModulesPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
ProjectModulesPage.getLayout = function getLayout(page: ReactElement) {
|
ProjectModulesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<ModulesListHeader />} withProjectWrapper>
|
<AppLayout header={<ModulesListHeader />} mobileHeader={<ModulesListMobileHeader/>} withProjectWrapper>
|
||||||
{page}
|
{page}
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
BIN
web/public/empty-state/search/archive-dark.webp
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
web/public/empty-state/search/archive-light.webp
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
web/public/empty-state/search/comments-dark.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
web/public/empty-state/search/comments-light.webp
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
web/public/empty-state/search/issue-dark.webp
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
web/public/empty-state/search/issues-light.webp
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
web/public/empty-state/search/notification-dark.webp
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
web/public/empty-state/search/notification-light.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |