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

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

View File

@ -1,82 +1,79 @@
# 1-Click Self-Hosting
# One-click deploy
In this guide, we will walk you through the process of setting up a 1-click self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization.
Deployment methods for Plane have improved significantly to make self-managing super-easy. One of those is a single-line-command installation of Plane.
Let's get started!
This short guide will guide you through the process, the background tasks that run with the command for the Community, One, and Enterprise editions, and the post-deployment configuration options available to you.
## Installing Plane
### Requirements
Installing Plane is a very easy and minimal step process.
- Operating systems: Debian, Ubuntu, CentOS
- Supported CPU architectures: AMD64, ARM64, x86_64, AArch64
### Prerequisite
### Download the latest stable release
- Operating System (latest): Debian / Ubuntu / Centos
- 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.

View File

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

View File

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

View File

@ -25,16 +25,20 @@ type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
const items: BubbleMenuItem[] = [
BoldItem(props.editor),
ItalicItem(props.editor),
UnderLineItem(props.editor),
StrikeThroughItem(props.editor),
...(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) => {
}}
/>
)}
<LinkSelector
editor={props.editor!!}
isOpen={isLinkSelectorOpen}
setIsOpen={() => {
setIsLinkSelectorOpen(!isLinkSelectorOpen);
setIsNodeSelectorOpen(false);
}}
/>
{!props.editor.isActive("code") && (
<LinkSelector
editor={props.editor}
isOpen={isLinkSelectorOpen}
setIsOpen={() => {
setIsLinkSelectorOpen(!isLinkSelectorOpen);
setIsNodeSelectorOpen(false);
}}
/>
)}
<div className="flex">
{items.map((item) => (
<button

View File

@ -84,8 +84,8 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90"
type="button"
onClick={(e) => {
e.stopPropagation();
onLinkSubmit();
e.stopPropagation();
}}
>
<Check className="h-4 w-4" />

View File

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

View File

@ -1,7 +1,7 @@
export type TIssueReaction = {
actor_id: string;
actor: string;
id: string;
issue_id: string;
issue: string;
reaction: string;
};

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, FC } from "react";
import React, { useCallback, useEffect, FC, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { 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 />

View File

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

View File

@ -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";
@ -40,7 +42,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
const [isSearching, setIsSearching] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
const { isMobile } = usePlatformOS();
const { isMobile } = usePlatformOS();
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
const handleClose = () => {
@ -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">

View File

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

View File

@ -0,0 +1,36 @@
import React from "react";
// components
import { EmptyState } from "components/empty-state";
// types
import { ISearchIssueResponse } from "@plane/types";
// constants
import { EmptyStateType } from "constants/empty-state";
interface EmptyStateProps {
issues: ISearchIssueResponse[];
searchTerm: string;
debouncedSearchTerm: string;
isSearching: boolean;
}
export const IssueSearchModalEmptyState: React.FC<EmptyStateProps> = ({
issues,
searchTerm,
debouncedSearchTerm,
isSearching,
}) => {
const renderEmptyState = (type: EmptyStateType) => (
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
<EmptyState type={type} layout="screen-simple" />
</div>
);
const emptyState =
issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && !isSearching
? renderEmptyState(EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE)
: issues.length === 0
? renderEmptyState(EmptyStateType.ISSUE_RELATION_EMPTY_STATE)
: null;
return emptyState;
};

View File

@ -22,12 +22,14 @@ export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
<div className="h-full w-full">
<div className="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">
<CyclesBoardMap
cycleIds={cycleIds}
peekCycle={peekCycle}
projectId={projectId}
workspaceSlug={workspaceSlug}
/>
{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">

View File

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

View File

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

View File

@ -0,0 +1,52 @@
import { observer } from "mobx-react";
// ui
import { CustomMenu } from "@plane/ui";
// icon
import { List } from "lucide-react";
// constants
import { CYCLE_VIEW_LAYOUTS } from "constants/cycle";
// hooks
import { useCycleFilter, useProject } from "hooks/store";
const CyclesListMobileHeader = observer(() => {
const { currentProjectDetails } = useProject();
// hooks
const { updateDisplayFilters } = useCycleFilter();
return (
<div className="flex justify-center sm:hidden">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm py-2 border-b border-custom-border-200 bg-custom-sidebar-background-100"
// placement="bottom-start"
customButton={
<span className="flex items-center gap-2">
<List className="h-4 w-4" />
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>
</span>
}
customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm"
closeOnSelect
>
{CYCLE_VIEW_LAYOUTS.map((layout) => {
if (layout.key == "gantt") return;
return (
<CustomMenu.MenuItem
key={layout.key}
onClick={() => {
updateDisplayFilters(currentProjectDetails!.id, {
layout: layout.key,
});
}}
className="flex items-center gap-2"
>
<layout.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
);
});
export default CyclesListMobileHeader;

View File

@ -42,6 +42,8 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
useOutsideClickDetector(inputRef, () => {
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>

View File

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

View File

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

View File

@ -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,24 +208,27 @@ 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) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
filteredOptions.map((option) => {
if (!option) return;
return (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
);
})
) : (
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
)

View File

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

View File

@ -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,9 +255,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
{issueCount && issueCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issueCount} ${
issueCount > 1 ? "issues" : "issue"
} in this cycle`}
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"
} in this cycle`}
position="bottom"
>
<span className="cursor-default flex items-center text-center justify-center px-2 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
@ -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>
</>
);

View File

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

View File

@ -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,9 +173,8 @@ 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
size={14}

View File

@ -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"
@ -257,10 +254,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
<p className="truncate">{moduleDetails?.name && moduleDetails.name}</p>
{issueCount && issueCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issueCount} ${
issueCount > 1 ? "issues" : "issue"
} in this module`}
isMobile={isMobile}
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"
} in this module`}
position="bottom"
>
<span className="cursor-default flex items-center text-center justify-center px-2 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
@ -365,7 +361,6 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
</button>
</div>
</div>
<ModuleMobileHeader />
</div>
</>
);

View File

@ -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,176 +89,138 @@ 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="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle />
<div>
<Breadcrumbs onBack={router.back}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
</span>
)
}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Modules" icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center">
{!isSearchOpen && (
<button
type="button"
className="-mr-1 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
placeholder="Search"
value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
// updateSearchQuery("");
setIsSearchOpen(false);
}}
>
<X className="h-3 w-3" />
</button>
)}
</div>
</div>
<div className="hidden md:flex items-center gap-1 rounded bg-custom-background-80 p-1">
{MODULE_VIEW_LAYOUTS.map((layout) => (
<Tooltip key={layout.key} tooltipContent={layout.title} isMobile={isMobile}>
<button
type="button"
className={cn(
"group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100",
{
"bg-custom-background-100 shadow-custom-shadow-2xs": displayFilters?.layout === layout.key,
}
)}
onClick={() => {
if (!projectId) return;
updateDisplayFilters(projectId.toString(), { layout: layout.key });
}}
>
<layout.icon
strokeWidth={2}
className={cn("h-3.5 w-3.5 text-custom-text-200", {
"text-custom-text-100": displayFilters?.layout === layout.key,
})}
/>
</button>
</Tooltip>
))}
</div>
<ModuleOrderByDropdown
value={displayFilters?.order_by}
onChange={(val) => {
if (!projectId || val === displayFilters?.order_by) return;
updateDisplayFilters(projectId.toString(), {
order_by: val,
});
}}
/>
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
<ModuleFiltersSelection
displayFilters={displayFilters ?? {}}
filters={filters ?? {}}
handleDisplayFiltersUpdate={(val) => {
if (!projectId) return;
updateDisplayFilters(projectId.toString(), val);
}}
handleFiltersUpdate={handleFilters}
memberIds={workspaceMemberIds ?? undefined}
<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>
<Breadcrumbs onBack={router.back}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
</span>
)
}
/>
}
/>
</FiltersDropdown>
{canUserCreateModule && (
<Button
variant="primary"
size="sm"
prependIcon={<Plus />}
onClick={() => {
setTrackElement(E_MODULES);
commandPaletteStore.toggleCreateModuleModal(true);
}}
>
<div className="hidden sm:block">Add</div> Module
</Button>
)}
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Modules" icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
</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}
<div className="flex items-center gap-2">
<div className="flex items-center">
{!isSearchOpen && (
<button
type="button"
className="-mr-1 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
onClick={() => {
if (!projectId) return;
updateDisplayFilters(projectId.toString(), { layout: layout.key });
setIsSearchOpen(true);
inputRef.current?.focus();
}}
className="flex items-center gap-2"
>
<layout.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
placeholder="Search"
value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
// updateSearchQuery("");
setIsSearchOpen(false);
}}
>
<X className="h-3 w-3" />
</button>
)}
</div>
</div>
<div className="hidden md:flex items-center gap-1 rounded bg-custom-background-80 p-1">
{MODULE_VIEW_LAYOUTS.map((layout) => (
<Tooltip key={layout.key} tooltipContent={layout.title} isMobile={isMobile}>
<button
type="button"
className={cn(
"group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100",
{
"bg-custom-background-100 shadow-custom-shadow-2xs": displayFilters?.layout === layout.key,
}
)}
onClick={() => {
if (!projectId) return;
updateDisplayFilters(projectId.toString(), { layout: layout.key });
}}
>
<layout.icon
strokeWidth={2}
className={cn("h-3.5 w-3.5 text-custom-text-200", {
"text-custom-text-100": displayFilters?.layout === layout.key,
})}
/>
</button>
</Tooltip>
))}
</CustomMenu>
</div>
<ModuleOrderByDropdown
value={displayFilters?.order_by}
onChange={(val) => {
if (!projectId || val === displayFilters?.order_by) return;
updateDisplayFilters(projectId.toString(), {
order_by: val,
});
}}
/>
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
<ModuleFiltersSelection
displayFilters={displayFilters ?? {}}
filters={filters ?? {}}
handleDisplayFiltersUpdate={(val) => {
if (!projectId) return;
updateDisplayFilters(projectId.toString(), val);
}}
handleFiltersUpdate={handleFilters}
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
{canUserCreateModule && (
<Button
variant="primary"
size="sm"
prependIcon={<Plus />}
onClick={() => {
setTrackElement(E_MODULES);
commandPaletteStore.toggleCreateModuleModal(true);
}}
>
<div className="hidden sm:block">Add</div> Module
</Button>
)}
</div>
</div>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,23 +21,21 @@ 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) => (
<IssueAttachmentsDetail
key={attachmentId}
attachmentId={attachmentId}
disabled={disabled}
handleAttachmentOperations={handleAttachmentOperations}
/>
))}
{issueAttachments?.map((attachmentId) => (
<IssueAttachmentsDetail
key={attachmentId}
attachmentId={attachmentId}
disabled={disabled}
handleAttachmentOperations={handleAttachmentOperations}
/>
))}
</>
);
});

View File

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

View File

@ -103,7 +103,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
return (
<div className="relative py-3 space-y-3">
<h3 className="text-lg">Attachments</h3>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<IssueAttachmentUpload
workspaceSlug={workspaceSlug}
disabled={disabled}

View File

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

View File

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

View File

@ -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}
addIssuesToView={addIssuesToView}
disableIssueCreation={disableIssueCreation}
enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback}
viewId={viewId}
readOnly={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}
viewId={viewId}
onOpen={() => setShowAllIssues(true)}
/>
</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>
</>
);

View File

@ -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,21 +95,24 @@ 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 ${
open ? "text-custom-text-100" : "text-custom-text-200"
}`}
>
<div className="font-medium">Options</div>
<button type="button" ref={setReferenceElement}>
<div
className={`flex h-3.5 w-3.5 items-center justify-center transition-all ${open ? "" : "rotate-180"}`}
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"
}`}
>
<ChevronUp width={12} strokeWidth={2} />
<div className="font-medium">Options</div>
<div
className={`flex h-3.5 w-3.5 items-center justify-center transition-all ${open ? "" : "rotate-180"}`}
>
<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>
@ -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>

View File

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

View File

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

View File

@ -0,0 +1,22 @@
import React from "react";
// components
import { CalendarIssueBlock } from "components/issues";
// types
import { TIssue, TIssueMap } from "@plane/types";
type Props = {
issues: TIssueMap | undefined;
issueId: string;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
isDragging?: boolean;
};
export const CalendarIssueBlockRoot: React.FC<Props> = (props) => {
const { issues, issueId, quickActions, isDragging } = props;
if (!issues?.[issueId]) return null;
const issue = issues?.[issueId];
return <CalendarIssueBlock isDragging={isDragging} issue={issue} quickActions={quickActions} />;
};

View File

@ -0,0 +1,113 @@
import { useState, useRef } from "react";
import { MoreHorizontal } from "lucide-react";
import { observer } from "mobx-react";
// components
import { Tooltip, ControlLink } from "@plane/ui";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
// helpers
import { cn } from "helpers/common.helper";
// types
import { TIssue } from "@plane/types";
import { usePlatformOS } from "hooks/use-platform-os";
type Props = {
issue: TIssue;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
isDragging?: boolean;
};
export const CalendarIssueBlock: React.FC<Props> = observer((props) => {
const { issue, quickActions, isDragging = false } = props;
// hooks
const {
router: { workspaceSlug, projectId },
} = useApplication();
const { getProjectIdentifierById } = useProject();
const { getProjectStates } = useProjectState();
const { peekIssue, setPeekIssue } = useIssueDetail();
const { isMobile } = usePlatformOS();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
const menuActionRef = useRef<HTMLDivElement | null>(null);
const stateColor = getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || "";
const handleIssuePeekOverview = (issue: TIssue) =>
workspaceSlug &&
issue &&
issue.project_id &&
issue.id &&
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
const customActionButton = (
<div
ref={menuActionRef}
className={`w-full cursor-pointer rounded p-1 text-custom-sidebar-text-400 hover:bg-custom-background-80 ${
isMenuActive ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
}`}
onClick={() => setIsMenuActive(!isMenuActive)}
>
<MoreHorizontal className="h-3.5 w-3.5" />
</div>
);
return (
<ControlLink
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
className="w-full cursor-pointer text-sm text-custom-text-100"
disabled={!!issue?.tempId}
>
<>
{issue?.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)}
<div
className={cn(
"group/calendar-block flex h-10 md:h-8 w-full items-center justify-between gap-1.5 rounded border-b md:border-[0.5px] border-custom-border-200 hover:border-custom-border-400 md:px-1 px-4 py-1.5 ",
{
"bg-custom-background-90 shadow-custom-shadow-rg border-custom-primary-100": isDragging,
},
{ "bg-custom-background-100 hover:bg-custom-background-90": !isDragging },
{
"border border-custom-primary-70 hover:border-custom-primary-70": peekIssue?.issueId === issue.id,
}
)}
>
<div className="flex h-full items-center gap-1.5 truncate">
<span
className="h-full w-0.5 flex-shrink-0 rounded"
style={{
backgroundColor: stateColor,
}}
/>
<div className="flex-shrink-0 text-sm md:text-xs text-custom-text-300">
{getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
</div>
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
<div className="truncate text-sm font-medium md:font-normal md:text-xs">{issue.name}</div>
</Tooltip>
</div>
<div
className={`flex-shrink-0 md:hidden h-5 w-5 group-hover/calendar-block:block ${
isMenuActive ? "!block" : ""
}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{quickActions(issue, customActionButton)}
</div>
</div>
</>
</ControlLink>
);
});

View File

@ -1,74 +1,62 @@
import { useState, useRef } from "react";
import { useState } from "react";
import { Draggable } from "@hello-pangea/dnd";
import { 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,
}}
/>
<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>
<CalendarIssueBlockRoot
issues={issues}
issueId={issueId}
quickActions={quickActions}
isDragging={snapshot.isDragging}
/>
</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>
)}
</>
);
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,26 +64,37 @@ 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) => (
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={_list.id}
icon={_list.icon}
title={_list.name}
count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
issuePayload={_list.payload}
storeType={storeType}
/>
</div>
))}
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}
group_by={group_by}
column_id={_list.id}
icon={_list.icon}
title={_list.name}
count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
issuePayload={_list.payload}
storeType={storeType}
/>
</div>
);
})}
</div>
);
@ -127,53 +155,74 @@ 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) => (
<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">
<HeaderSubGroupByCard
column_id={_list.id}
icon={_list.Icon}
title={_list.name || ""}
count={calculateIssueCount(_list.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
/>
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">
<HeaderSubGroupByCard
column_id={_list.id}
icon={_list.Icon}
title={_list.name || ""}
count={calculateIssueCount(_list.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
/>
</div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div>
{!kanbanFilters?.sub_group_by.includes(_list.id) && (
<div className="relative">
<KanBan
issuesMap={issuesMap}
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={_list.id}
updateIssue={updateIssue}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
showEmptyGroup={showEmptyGroup}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback}
viewId={viewId}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
storeType={storeType}
/>
</div>
)}
</div>
))}
{subGroupByVisibilityToggle.showIssues && (
<div className="relative">
<KanBan
issuesMap={issuesMap}
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={_list.id}
storeType={storeType}
updateIssue={updateIssue}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
showEmptyGroup={showEmptyGroup}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback}
viewId={viewId}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,111 @@
import { useMemo } from "react";
// hooks
import { useEventTracker, useIssueDetail } from "hooks/store";
// components
import { IssueAttachmentUpload, IssueAttachmentsList, TAttachmentOperations } from "components/issues";
// ui
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
type Props = {
disabled: boolean;
issueId: string;
projectId: string;
workspaceSlug: string;
};
export const PeekOverviewIssueAttachments: React.FC<Props> = (props) => {
const { disabled, issueId, projectId, workspaceSlug } = props;
// store hooks
const { captureIssueEvent } = useEventTracker();
const {
attachment: { createAttachment, removeAttachment },
} = useIssueDetail();
const handleAttachmentOperations: TAttachmentOperations = useMemo(
() => ({
create: async (data: FormData) => {
try {
const attachmentUploadPromise = createAttachment(workspaceSlug, projectId, issueId, data);
setPromiseToast(attachmentUploadPromise, {
loading: "Uploading attachment...",
success: {
title: "Attachment uploaded",
message: () => "The attachment has been successfully uploaded",
},
error: {
title: "Attachment not uploaded",
message: () => "The attachment could not be uploaded",
},
});
const res = await attachmentUploadPromise;
captureIssueEvent({
eventName: "Issue attachment added",
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "attachment",
change_details: res.id,
},
});
} catch (error) {
captureIssueEvent({
eventName: "Issue attachment added",
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
});
}
},
remove: async (attachmentId: string) => {
try {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
await removeAttachment(workspaceSlug, projectId, issueId, attachmentId);
setToast({
message: "The attachment has been successfully removed",
type: TOAST_TYPE.SUCCESS,
title: "Attachment removed",
});
captureIssueEvent({
eventName: "Issue attachment deleted",
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "attachment",
change_details: "",
},
});
} catch (error) {
captureIssueEvent({
eventName: "Issue attachment deleted",
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: "attachment",
change_details: "",
},
});
setToast({
message: "The Attachment could not be removed",
type: TOAST_TYPE.ERROR,
title: "Attachment not removed",
});
}
},
}),
[workspaceSlug, projectId, issueId, captureIssueEvent, createAttachment, removeAttachment]
);
return (
<div>
<h6 className="text-sm font-medium">Attachments</h6>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2 mt-3">
<IssueAttachmentUpload
workspaceSlug={workspaceSlug}
disabled={disabled}
handleAttachmentOperations={handleAttachmentOperations}
/>
<IssueAttachmentsList
issueId={issueId}
disabled={disabled}
handleAttachmentOperations={handleAttachmentOperations}
/>
</div>
</div>
);
};

View File

@ -55,7 +55,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
: undefined;
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>
);
});

View File

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

View File

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

View File

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

View File

@ -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"
onClick={handleFetchSubIssues}
>
<div className="flex h-[16px] w-[16px] 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} />
) : (
<ChevronRight width={14} strokeWidth={2} />
)}
<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 flex-shrink-0 items-center justify-center">
{subIssueHelpers.preview_loader.includes(parentIssueId) ? (
<Loader strokeWidth={2} className="h-3 w-3 animate-spin" />
) : (
<ChevronRight
className={cn("h-3 w-3 transition-all", {
"rotate-90": subIssueHelpers.issue_visibility.includes(parentIssueId),
})}
strokeWidth={2}
/>
)}
</div>
<div>Sub-issues</div>
</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>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)
}
/>
</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>
<div
className="cursor-pointer rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80"
<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>
</div>
<div className="flex items-center gap-2">
<LayersIcon className="h-3 w-3" />
<span>Add existing</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
@ -392,62 +422,74 @@ 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>
<CustomMenu
label={
<>
<Plus className="h-3 w-3" />
Add sub-issue
</>
}
buttonClassName="whitespace-nowrap"
placement="bottom-end"
noBorder
noChevron
<div className="text-xs italic text-custom-text-300">No sub-issues yet</div>
<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);
}}
>
<CustomMenu.MenuItem
onClick={() => {
setTrackElement(E_ISSUE_DETAILS);
handleIssueCrudState("create", parentIssueId, null);
}}
>
Create new
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
setTrackElement(E_ISSUE_DETAILS);
handleIssueCrudState("existing", parentIssueId, null);
}}
>
Add an existing issue
</CustomMenu.MenuItem>
</CustomMenu>
</div>
<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);
}}
>
<div className="flex items-center gap-2">
<LayersIcon className="h-3 w-3" />
<span>Add existing</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</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(

View File

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

View File

@ -0,0 +1,40 @@
import { CustomMenu } from "@plane/ui";
import { MODULE_VIEW_LAYOUTS } from "constants/module";
import { useModuleFilter, useProject } from "hooks/store";
import { observer } from "mobx-react";
const ModulesListMobileHeader = observer(() => {
const { currentProjectDetails } = useProject();
const { updateDisplayFilters } = useModuleFilter();
return (
<div className="flex justify-center md:hidden">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm py-2 border-b border-custom-border-200 bg-custom-sidebar-background-100"
// placement="bottom-start"
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm"
closeOnSelect
>
{MODULE_VIEW_LAYOUTS.map((layout) => {
if (layout.key == "gantt") return;
return (
<CustomMenu.MenuItem
key={layout.key}
onClick={() => {
updateDisplayFilters(currentProjectDetails!.id.toString(), { layout: layout.key });
}}
className="flex items-center gap-2"
>
<layout.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
);
});
export default ModulesListMobileHeader;

View File

@ -1,14 +1,21 @@
import { useCallback, useState } from "react";
import { 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>
);
};
});

View File

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

View File

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

View File

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

View File

@ -55,10 +55,11 @@ 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" : ""}`}
} ${themeStore?.sidebarCollapsed ? "justify-center" : ""}`}
>
{
<link.Icon
@ -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" />
)}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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