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