Merge branch 'develop' of https://github.com/makeplane/plane into chore/event-improvements

This commit is contained in:
LAKHAN BAHETI 2024-03-18 13:07:05 +05:30
commit 72d1b5de49
107 changed files with 1659 additions and 1003 deletions

View File

@ -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

View File

@ -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) => {

View File

@ -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();
}, },
}, },
{ {

View File

@ -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

View File

@ -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" />

View File

@ -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),

View File

@ -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;
}; };

View File

@ -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}

View File

@ -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) && (

View File

@ -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 />

View File

@ -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>

View File

@ -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">

View File

@ -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";

View File

@ -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;
};

View File

@ -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">

View File

@ -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"

View File

@ -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)",

View 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;

View File

@ -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>

View File

@ -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}
/> />
)} )}

View File

@ -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}>

View File

@ -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>
) )

View File

@ -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>
)}
</> </>
); );
}; };

View File

@ -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>
</> </>
); );

View File

@ -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>
); );
}); });

View File

@ -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

View File

@ -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>
</> </>
); );

View File

@ -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>
); );
}); });

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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={

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>
</> </>
); );

View File

@ -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">

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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);

View File

@ -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>
</> </>

View File

@ -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>
</> </>
); );

View File

@ -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>

View File

@ -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 (

View File

@ -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";

View File

@ -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} />;
};

View 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>
);
});

View File

@ -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>
)}
</> </>
); );
}); });

View File

@ -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,
})} })}
> >

View File

@ -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}

View File

@ -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>
); );

View File

@ -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>

View File

@ -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}
/> />
); );
}); });

View File

@ -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;
} }

View File

@ -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>

View File

@ -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)

View File

@ -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>

View File

@ -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>
</> </>
); );
}; });

View File

@ -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">

View File

@ -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

View File

@ -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";

View 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>
);
};

View File

@ -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>
); );
}); });

View File

@ -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" : ""}`}>

View File

@ -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>

View File

@ -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>
)} )}
</> </>

View File

@ -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(

View File

@ -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>

View 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;

View File

@ -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>
); );
}; });

View File

@ -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)",

View File

@ -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>
) )
) : ( ) : (

View File

@ -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>
); );
}); };

View File

@ -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" />
)} )}

View File

@ -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;

View File

@ -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

View File

@ -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}</>}

View File

@ -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">

View File

@ -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>
); );

View File

@ -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>
); );

View File

@ -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>
); );

View File

@ -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>
); );

View File

@ -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>
); );

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

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