mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
conflicts: resolved merge conflicts
This commit is contained in:
commit
2b27e9c339
@ -354,6 +354,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"external_id",
|
"external_id",
|
||||||
"progress_snapshot",
|
"progress_snapshot",
|
||||||
# meta fields
|
# meta fields
|
||||||
|
"total_issues",
|
||||||
"is_favorite",
|
"is_favorite",
|
||||||
"cancelled_issues",
|
"cancelled_issues",
|
||||||
"completed_issues",
|
"completed_issues",
|
||||||
|
@ -79,6 +79,15 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"issue_module",
|
||||||
|
filter=Q(
|
||||||
|
issue_module__issue__archived_at__isnull=True,
|
||||||
|
issue_module__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"issue_module__issue__state__group",
|
||||||
@ -214,6 +223,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"external_source",
|
"external_source",
|
||||||
"external_id",
|
"external_id",
|
||||||
# computed fields
|
# computed fields
|
||||||
|
"total_issues",
|
||||||
"is_favorite",
|
"is_favorite",
|
||||||
"cancelled_issues",
|
"cancelled_issues",
|
||||||
"completed_issues",
|
"completed_issues",
|
||||||
|
@ -1,82 +1,79 @@
|
|||||||
# 1-Click Self-Hosting
|
# One-click deploy
|
||||||
|
|
||||||
In this guide, we will walk you through the process of setting up a 1-click self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization.
|
Deployment methods for Plane have improved significantly to make self-managing super-easy. One of those is a single-line-command installation of Plane.
|
||||||
|
|
||||||
Let's get started!
|
This short guide will guide you through the process, the background tasks that run with the command for the Community, One, and Enterprise editions, and the post-deployment configuration options available to you.
|
||||||
|
|
||||||
## Installing Plane
|
### Requirements
|
||||||
|
|
||||||
Installing Plane is a very easy and minimal step process.
|
- Operating systems: Debian, Ubuntu, CentOS
|
||||||
|
- Supported CPU architectures: AMD64, ARM64, x86_64, AArch64
|
||||||
|
|
||||||
### Prerequisite
|
### Download the latest stable release
|
||||||
|
|
||||||
- Operating System (latest): Debian / Ubuntu / Centos
|
Run ↓ on any CLI.
|
||||||
- Supported CPU Architechture: AMD64 / ARM64 / x86_64 / aarch64
|
|
||||||
|
|
||||||
### Downloading Latest Stable Release
|
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh -
|
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh -
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
### Download the Preview release
|
||||||
<summary>Downloading Preview Release</summary>
|
|
||||||
|
`Preview` builds do not support ARM64, AArch64 CPU architectures
|
||||||
|
|
||||||
|
Run ↓ on any CLI.
|
||||||
|
|
||||||
```
|
```
|
||||||
export BRANCH=preview
|
export BRANCH=preview
|
||||||
|
|
||||||
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh -
|
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh -
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
NOTE: `Preview` builds do not support ARM64/AARCH64 CPU architecture
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
--
|
|
||||||
|
|
||||||
Expect this after a successful install
|
|
||||||
|
|
||||||
![Install Output](images/install.png)
|
|
||||||
|
|
||||||
Access the application on a browser via http://server-ip-address
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Get Control of your Plane Server Setup
|
### Successful installation
|
||||||
|
|
||||||
Plane App is available via the command `plane-app`. Running the command `plane-app --help` helps you to manage Plane
|
You should see ↓ if there are no hitches. That output will also list the IP address you can use to access your Plane instance.
|
||||||
|
|
||||||
|
![Install Output](images/install.png)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Manage your Plane instance
|
||||||
|
|
||||||
|
Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of all operators with `plane-app ---help`.
|
||||||
|
|
||||||
![Plane Help](images/help.png)
|
![Plane Help](images/help.png)
|
||||||
|
|
||||||
<ins>Basic Operations</ins>:
|
1. Basic operators
|
||||||
|
|
||||||
1. Start Server using `plane-app start`
|
1. `plane-app start` starts the Plane server.
|
||||||
1. Stop Server using `plane-app stop`
|
2. `plane-app restart` restarts the Plane server.
|
||||||
1. Restart Server using `plane-app restart`
|
3. `plane-app stop` stops the Plane server.
|
||||||
|
|
||||||
<ins>Advanced Operations</ins>:
|
2. Advanced operators
|
||||||
|
|
||||||
1. Configure Plane using `plane-app --configure`. This will give you options to modify
|
`plane-app --configure` will show advanced configurators.
|
||||||
|
|
||||||
- NGINX Port (default 80)
|
- Change your proxy or listening port
|
||||||
- Domain Name (default is the local server public IP address)
|
<br>Default: 80
|
||||||
- File Upload Size (default 5MB)
|
- Change your domain name
|
||||||
- External Postgres DB Url (optional - default empty)
|
<br>Default: Deployed server's public IP address
|
||||||
- External Redis URL (optional - default empty)
|
- File upload size
|
||||||
- AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket)
|
<br>Default: 5MB
|
||||||
|
- Specify external database address when using an external database
|
||||||
|
<br>Default: `Empty`
|
||||||
|
<br>`Default folder: /opt/plane/data/postgres`
|
||||||
|
- Specify external Redis URL when using external Redis
|
||||||
|
<br>Default: `Empty`
|
||||||
|
<br>`Default folder: /opt/plane/data/redis`
|
||||||
|
- Configure AWS S3 bucket
|
||||||
|
<br>Use only when you or your users want to use S3
|
||||||
|
<br>`Default folder: /opt/plane/data/minio`
|
||||||
|
|
||||||
1. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images)
|
3. Version operators
|
||||||
|
|
||||||
1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility.
|
1. `plane-app --upgrade` gets the latest stable version of `docker-compose.yaml`, `.env`, and Docker images
|
||||||
|
2. `plane-app --update-installer` updates the installer and the `plane-app` utility.
|
||||||
1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio.
|
3. `plane-app --uninstall` uninstalls the Plane application and all Docker containers from the server but leaves the data stored in
|
||||||
|
Postgres, Redis, and Minio alone.
|
||||||
1. Plane App can be reinstalled using `plane-app --install`.
|
4. `plane-app --install` installs the Plane app again.
|
||||||
|
|
||||||
<ins>Application Data is stored in the mentioned folders</ins>:
|
|
||||||
|
|
||||||
1. DB Data: /opt/plane/data/postgres
|
|
||||||
1. Redis Data: /opt/plane/data/redis
|
|
||||||
1. Minio Data: /opt/plane/data/minio
|
|
||||||
|
2
packages/types/src/importer/index.d.ts
vendored
2
packages/types/src/importer/index.d.ts
vendored
@ -1,7 +1,7 @@
|
|||||||
export * from "./github-importer";
|
export * from "./github-importer";
|
||||||
export * from "./jira-importer";
|
export * from "./jira-importer";
|
||||||
|
|
||||||
import { IProjectLite } from "../projects";
|
import { IProjectLite } from "../project";
|
||||||
// types
|
// types
|
||||||
import { IUserLite } from "../users";
|
import { IUserLite } from "../users";
|
||||||
|
|
||||||
|
2
packages/types/src/inbox/inbox-types.d.ts
vendored
2
packages/types/src/inbox/inbox-types.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
import { TIssue } from "../issues/base";
|
import { TIssue } from "../issues/base";
|
||||||
import type { IProjectLite } from "../projects";
|
import type { IProjectLite } from "../project";
|
||||||
|
|
||||||
export type TInboxIssueExtended = {
|
export type TInboxIssueExtended = {
|
||||||
completed_at: string | null;
|
completed_at: string | null;
|
||||||
|
4
packages/types/src/index.d.ts
vendored
4
packages/types/src/index.d.ts
vendored
@ -2,10 +2,10 @@ export * from "./users";
|
|||||||
export * from "./workspace";
|
export * from "./workspace";
|
||||||
export * from "./cycle";
|
export * from "./cycle";
|
||||||
export * from "./dashboard";
|
export * from "./dashboard";
|
||||||
export * from "./projects";
|
export * from "./project";
|
||||||
export * from "./state";
|
export * from "./state";
|
||||||
export * from "./issues";
|
export * from "./issues";
|
||||||
export * from "./modules";
|
export * from "./module";
|
||||||
export * from "./views";
|
export * from "./views";
|
||||||
export * from "./integration";
|
export * from "./integration";
|
||||||
export * from "./pages";
|
export * from "./pages";
|
||||||
|
2
packages/types/src/module/index.ts
Normal file
2
packages/types/src/module/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./module_filters";
|
||||||
|
export * from "./modules";
|
32
packages/types/src/module/module_filters.d.ts
vendored
Normal file
32
packages/types/src/module/module_filters.d.ts
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export type TModuleOrderByOptions =
|
||||||
|
| "name"
|
||||||
|
| "-name"
|
||||||
|
| "progress"
|
||||||
|
| "-progress"
|
||||||
|
| "issues_length"
|
||||||
|
| "-issues_length"
|
||||||
|
| "target_date"
|
||||||
|
| "-target_date"
|
||||||
|
| "created_at"
|
||||||
|
| "-created_at";
|
||||||
|
|
||||||
|
export type TModuleLayoutOptions = "list" | "board" | "gantt";
|
||||||
|
|
||||||
|
export type TModuleDisplayFilters = {
|
||||||
|
favorites?: boolean;
|
||||||
|
layout?: TModuleLayoutOptions;
|
||||||
|
order_by?: TModuleOrderByOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TModuleFilters = {
|
||||||
|
lead?: string[] | null;
|
||||||
|
members?: string[] | null;
|
||||||
|
start_date?: string[] | null;
|
||||||
|
status?: string[] | null;
|
||||||
|
target_date?: string[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TModuleStoredFilters = {
|
||||||
|
display_filters?: TModuleDisplayFilters;
|
||||||
|
filters?: TModuleFilters;
|
||||||
|
};
|
2
packages/types/src/project/index.ts
Normal file
2
packages/types/src/project/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./project_filters";
|
||||||
|
export * from "./projects";
|
25
packages/types/src/project/project_filters.d.ts
vendored
Normal file
25
packages/types/src/project/project_filters.d.ts
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export type TProjectOrderByOptions =
|
||||||
|
| "sort_order"
|
||||||
|
| "name"
|
||||||
|
| "-name"
|
||||||
|
| "created_at"
|
||||||
|
| "-created_at"
|
||||||
|
| "members_length"
|
||||||
|
| "-members_length";
|
||||||
|
|
||||||
|
export type TProjectDisplayFilters = {
|
||||||
|
my_projects?: boolean;
|
||||||
|
order_by?: TProjectOrderByOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TProjectFilters = {
|
||||||
|
access?: string[] | null;
|
||||||
|
lead?: string[] | null;
|
||||||
|
members?: string[] | null;
|
||||||
|
created_at?: string[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TProjectStoredFilters = {
|
||||||
|
display_filters?: TProjectDisplayFilters;
|
||||||
|
filters?: TProjectFilters;
|
||||||
|
};
|
@ -7,7 +7,7 @@ import type {
|
|||||||
IWorkspace,
|
IWorkspace,
|
||||||
IWorkspaceLite,
|
IWorkspaceLite,
|
||||||
TStateGroups,
|
TStateGroups,
|
||||||
} from ".";
|
} from "..";
|
||||||
|
|
||||||
export type TProjectLogoProps = {
|
export type TProjectLogoProps = {
|
||||||
in_use: "emoji" | "icon";
|
in_use: "emoji" | "icon";
|
@ -103,7 +103,7 @@ export const CustomEmojiIconPicker: React.FC<TCustomEmojiPicker> = (props) => {
|
|||||||
style={styles.popper}
|
style={styles.popper}
|
||||||
{...attributes.popper}
|
{...attributes.popper}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-80 w-80 bg-custom-background-100 rounded-md border-[0.5px] border-custom-border-300 overflow-hidden",
|
"w-80 bg-custom-background-100 rounded-md border-[0.5px] border-custom-border-300 overflow-hidden",
|
||||||
dropdownClassName
|
dropdownClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -146,7 +146,7 @@ export const CustomEmojiIconPicker: React.FC<TCustomEmojiPicker> = (props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel className="h-80 w-full">
|
||||||
<IconsList
|
<IconsList
|
||||||
defaultColor={defaultIconColor}
|
defaultColor={defaultIconColor}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
|
@ -29,6 +29,7 @@ interface ITooltipProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
openDelay?: number;
|
openDelay?: number;
|
||||||
closeDelay?: number;
|
closeDelay?: number;
|
||||||
|
isMobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tooltip: React.FC<ITooltipProps> = ({
|
export const Tooltip: React.FC<ITooltipProps> = ({
|
||||||
@ -40,6 +41,7 @@ export const Tooltip: React.FC<ITooltipProps> = ({
|
|||||||
className = "",
|
className = "",
|
||||||
openDelay = 200,
|
openDelay = 200,
|
||||||
closeDelay,
|
closeDelay,
|
||||||
|
isMobile = false,
|
||||||
}) => (
|
}) => (
|
||||||
<Tooltip2
|
<Tooltip2
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -47,7 +49,7 @@ export const Tooltip: React.FC<ITooltipProps> = ({
|
|||||||
hoverCloseDelay={closeDelay}
|
hoverCloseDelay={closeDelay}
|
||||||
content={
|
content={
|
||||||
<div
|
<div
|
||||||
className={`relative z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md ${className}`}
|
className={`relative ${isMobile ? "hidden" : "block"} z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md ${className}`}
|
||||||
>
|
>
|
||||||
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>}
|
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>}
|
||||||
{tooltipContent}
|
{tooltipContent}
|
||||||
|
@ -6,6 +6,8 @@ import { renderFormattedDate } from "helpers/date-time.helper";
|
|||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IApiToken } from "@plane/types";
|
import { IApiToken } from "@plane/types";
|
||||||
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
@ -14,7 +16,7 @@ type Props = {
|
|||||||
|
|
||||||
export const GeneratedTokenDetails: React.FC<Props> = (props) => {
|
export const GeneratedTokenDetails: React.FC<Props> = (props) => {
|
||||||
const { handleClose, tokenDetails } = props;
|
const { handleClose, tokenDetails } = props;
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const copyApiToken = (token: string) => {
|
const copyApiToken = (token: string) => {
|
||||||
copyTextToClipboard(token).then(() =>
|
copyTextToClipboard(token).then(() =>
|
||||||
setToast({
|
setToast({
|
||||||
@ -40,7 +42,7 @@ export const GeneratedTokenDetails: React.FC<Props> = (props) => {
|
|||||||
className="mt-4 flex w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-2 text-sm font-medium outline-none"
|
className="mt-4 flex w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-2 text-sm font-medium outline-none"
|
||||||
>
|
>
|
||||||
{tokenDetails.token}
|
{tokenDetails.token}
|
||||||
<Tooltip tooltipContent="Copy secret key">
|
<Tooltip tooltipContent="Copy secret key" isMobile={isMobile}>
|
||||||
<Copy className="h-4 w-4 text-custom-text-400" />
|
<Copy className="h-4 w-4 text-custom-text-400" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</button>
|
</button>
|
||||||
|
@ -3,6 +3,7 @@ import { XCircle } from "lucide-react";
|
|||||||
// components
|
// components
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
import { DeleteApiTokenModal } from "components/api-token";
|
import { DeleteApiTokenModal } from "components/api-token";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// ui
|
// ui
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper";
|
import { renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper";
|
||||||
@ -17,12 +18,14 @@ export const ApiTokenListItem: React.FC<Props> = (props) => {
|
|||||||
const { token } = props;
|
const { token } = props;
|
||||||
// states
|
// states
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||||
|
// hooks
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteApiTokenModal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)} tokenId={token.id} />
|
<DeleteApiTokenModal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)} tokenId={token.id} />
|
||||||
<div className="group relative flex flex-col justify-center border-b border-custom-border-200 px-4 py-3">
|
<div className="group relative flex flex-col justify-center border-b border-custom-border-200 px-4 py-3">
|
||||||
<Tooltip tooltipContent="Delete token">
|
<Tooltip tooltipContent="Delete token" isMobile={isMobile}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setDeleteModalOpen(true)}
|
onClick={() => setDeleteModalOpen(true)}
|
||||||
className="absolute right-4 hidden place-items-center group-hover:grid"
|
className="absolute right-4 hidden place-items-center group-hover:grid"
|
||||||
@ -33,9 +36,8 @@ export const ApiTokenListItem: React.FC<Props> = (props) => {
|
|||||||
<div className="flex w-4/5 items-center">
|
<div className="flex w-4/5 items-center">
|
||||||
<h5 className="truncate text-sm font-medium">{token.label}</h5>
|
<h5 className="truncate text-sm font-medium">{token.label}</h5>
|
||||||
<span
|
<span
|
||||||
className={`${
|
className={`${token.is_active ? "bg-green-500/10 text-green-500" : "bg-custom-background-80 text-custom-text-400"
|
||||||
token.is_active ? "bg-green-500/10 text-green-500" : "bg-custom-background-80 text-custom-text-400"
|
} ml-2 flex h-4 max-h-fit items-center rounded-sm px-2 text-xs font-medium`}
|
||||||
} ml-2 flex h-4 max-h-fit items-center rounded-sm px-2 text-xs font-medium`}
|
|
||||||
>
|
>
|
||||||
{token.is_active ? "Active" : "Expired"}
|
{token.is_active ? "Active" : "Expired"}
|
||||||
</span>
|
</span>
|
||||||
|
@ -20,12 +20,11 @@ import {
|
|||||||
} from "components/command-palette";
|
} from "components/command-palette";
|
||||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||||
import { useApplication, useEventTracker, useProject } from "hooks/store";
|
import { useApplication, useEventTracker, useProject } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// services
|
// services
|
||||||
import useDebounce from "hooks/use-debounce";
|
import useDebounce from "hooks/use-debounce";
|
||||||
import { IssueService } from "services/issue";
|
import { IssueService } from "services/issue";
|
||||||
import { WorkspaceService } from "services/workspace.service";
|
import { WorkspaceService } from "services/workspace.service";
|
||||||
// hooks
|
|
||||||
// components
|
|
||||||
// types
|
// types
|
||||||
import { IWorkspaceSearchResults } from "@plane/types";
|
import { IWorkspaceSearchResults } from "@plane/types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -37,6 +36,7 @@ const issueService = new IssueService();
|
|||||||
export const CommandModal: React.FC = observer(() => {
|
export const CommandModal: React.FC = observer(() => {
|
||||||
// hooks
|
// hooks
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
// states
|
// states
|
||||||
const [placeholder, setPlaceholder] = useState("Type a command or search...");
|
const [placeholder, setPlaceholder] = useState("Type a command or search...");
|
||||||
const [resultsCount, setResultsCount] = useState(0);
|
const [resultsCount, setResultsCount] = useState(0);
|
||||||
@ -197,7 +197,7 @@ export const CommandModal: React.FC = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{projectId && (
|
{projectId && (
|
||||||
<Tooltip tooltipContent="Toggle workspace level search">
|
<Tooltip tooltipContent="Toggle workspace level search" isMobile={isMobile}>
|
||||||
<div className="flex flex-shrink-0 cursor-pointer items-center gap-1 self-end text-xs sm:self-center">
|
<div className="flex flex-shrink-0 cursor-pointer items-center gap-1 self-end text-xs sm:self-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label?: string;
|
label?: string;
|
||||||
@ -9,8 +10,9 @@ type Props = {
|
|||||||
|
|
||||||
export const BreadcrumbLink: React.FC<Props> = (props) => {
|
export const BreadcrumbLink: React.FC<Props> = (props) => {
|
||||||
const { href, label, icon } = props;
|
const { href, label, icon } = props;
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipContent={label} position="bottom">
|
<Tooltip tooltipContent={label} position="bottom" isMobile={isMobile}>
|
||||||
<li className="flex items-center space-x-2" tabIndex={-1}>
|
<li className="flex items-center space-x-2" tabIndex={-1}>
|
||||||
<div className="flex flex-wrap items-center gap-2.5">
|
<div className="flex flex-wrap items-center gap-2.5">
|
||||||
{href ? (
|
{href ? (
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// store hooks
|
// store hooks
|
||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
@ -29,9 +30,13 @@ import { IIssueActivity } from "@plane/types";
|
|||||||
export const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
export const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipContent={activity?.issue_detail ? activity.issue_detail.name : "This issue has been deleted"}>
|
<Tooltip
|
||||||
|
tooltipContent={activity?.issue_detail ? activity.issue_detail.name : "This issue has been deleted"}
|
||||||
|
isMobile={isMobile}
|
||||||
|
>
|
||||||
{activity?.issue_detail ? (
|
{activity?.issue_detail ? (
|
||||||
<a
|
<a
|
||||||
aria-disabled={activity.issue === null}
|
aria-disabled={activity.issue === null}
|
||||||
|
@ -5,7 +5,7 @@ import { Rocket, Search, X } from "lucide-react";
|
|||||||
import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
|
|
||||||
import useDebounce from "hooks/use-debounce";
|
import useDebounce from "hooks/use-debounce";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
import { ProjectService } from "services/project";
|
import { ProjectService } from "services/project";
|
||||||
// ui
|
// ui
|
||||||
// types
|
// types
|
||||||
@ -40,7 +40,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
|||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
|
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@ -154,7 +154,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{workspaceLevelToggle && (
|
{workspaceLevelToggle && (
|
||||||
<Tooltip tooltipContent="Toggle workspace level search">
|
<Tooltip tooltipContent="Toggle workspace level search" isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
className={`flex flex-shrink-0 cursor-pointer items-center gap-1 text-xs ${
|
className={`flex flex-shrink-0 cursor-pointer items-center gap-1 text-xs ${
|
||||||
isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200"
|
isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
|
@ -7,6 +7,7 @@ import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
|||||||
import { calculateTimeAgo } from "helpers/date-time.helper";
|
import { calculateTimeAgo } from "helpers/date-time.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMember } from "hooks/store";
|
import { useMember } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// types
|
// types
|
||||||
import { ILinkDetails, UserAuth } from "@plane/types";
|
import { ILinkDetails, UserAuth } from "@plane/types";
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ type Props = {
|
|||||||
|
|
||||||
export const LinksList: React.FC<Props> = observer(({ links, handleDeleteLink, handleEditLink, userAuth }) => {
|
export const LinksList: React.FC<Props> = observer(({ links, handleDeleteLink, handleEditLink, userAuth }) => {
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
const copyToClipboard = (text: string) => {
|
const copyToClipboard = (text: string) => {
|
||||||
@ -42,7 +43,7 @@ export const LinksList: React.FC<Props> = observer(({ links, handleDeleteLink, h
|
|||||||
<span className="py-1">
|
<span className="py-1">
|
||||||
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
||||||
</span>
|
</span>
|
||||||
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url}>
|
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url} isMobile={isMobile}>
|
||||||
<span
|
<span
|
||||||
className="cursor-pointer truncate text-xs"
|
className="cursor-pointer truncate text-xs"
|
||||||
onClick={() => copyToClipboard(link.title && link.title !== "" ? link.title : link.url)}
|
onClick={() => copyToClipboard(link.title && link.title !== "" ? link.title : link.url)}
|
||||||
|
@ -4,6 +4,7 @@ import Link from "next/link";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// hooks
|
// hooks
|
||||||
import { useCycle, useCycleFilter, useIssues, useMember, useProject } from "hooks/store";
|
import { useCycle, useCycleFilter, useIssues, useMember, useProject } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// ui
|
// ui
|
||||||
import { SingleProgressStats } from "components/core";
|
import { SingleProgressStats } from "components/core";
|
||||||
import {
|
import {
|
||||||
@ -46,6 +47,8 @@ interface IActiveCycleDetails {
|
|||||||
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
|
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
|
||||||
// props
|
// props
|
||||||
const { workspaceSlug, projectId } = props;
|
const { workspaceSlug, projectId } = props;
|
||||||
|
// hooks
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
issues: { fetchActiveCycleIssues },
|
issues: { fetchActiveCycleIssues },
|
||||||
@ -197,7 +200,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
|||||||
<span className="h-5 w-5">
|
<span className="h-5 w-5">
|
||||||
<CycleGroupIcon cycleGroup={cycleStatus} className="h-4 w-4" />
|
<CycleGroupIcon cycleGroup={cycleStatus} className="h-4 w-4" />
|
||||||
</span>
|
</span>
|
||||||
<Tooltip tooltipContent={activeCycle.name} position="top-left">
|
<Tooltip tooltipContent={activeCycle.name} position="top-left" isMobile={isMobile}>
|
||||||
<h3 className="break-words text-lg font-semibold">{truncateText(activeCycle.name, 70)}</h3>
|
<h3 className="break-words text-lg font-semibold">{truncateText(activeCycle.name, 70)}</h3>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
@ -325,6 +328,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
|||||||
<PriorityIcon priority={issue.priority} withContainer size={12} />
|
<PriorityIcon priority={issue.priority} withContainer size={12} />
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipHeading="Issue ID"
|
tooltipHeading="Issue ID"
|
||||||
tooltipContent={`${currentProjectDetails?.identifier}-${issue.sequence_id}`}
|
tooltipContent={`${currentProjectDetails?.identifier}-${issue.sequence_id}`}
|
||||||
>
|
>
|
||||||
@ -332,7 +336,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
|||||||
{currentProjectDetails?.identifier}-{issue.sequence_id}
|
{currentProjectDetails?.identifier}-{issue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip position="top-left" tooltipContent={issue.name}>
|
<Tooltip position="top-left" tooltipContent={issue.name} isMobile={isMobile}>
|
||||||
<span className="text-[0.825rem] text-custom-text-100">{truncateText(issue.name, 30)}</span>
|
<span className="text-[0.825rem] text-custom-text-100">{truncateText(issue.name, 30)}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@ -345,7 +349,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
|||||||
buttonVariant="background-with-text"
|
buttonVariant="background-with-text"
|
||||||
/>
|
/>
|
||||||
{issue.target_date && (
|
{issue.target_date && (
|
||||||
<Tooltip tooltipHeading="Target Date" tooltipContent={renderFormattedDate(issue.target_date)}>
|
<Tooltip tooltipHeading="Target Date" tooltipContent={renderFormattedDate(issue.target_date)} isMobile={isMobile}>
|
||||||
<div className="flex h-full cursor-not-allowed items-center gap-1.5 rounded bg-custom-background-80 px-2 py-0.5 text-xs">
|
<div className="flex h-full cursor-not-allowed items-center gap-1.5 rounded bg-custom-background-80 px-2 py-0.5 text-xs">
|
||||||
<CalendarCheck className="h-3 w-3 flex-shrink-0" />
|
<CalendarCheck className="h-3 w-3 flex-shrink-0" />
|
||||||
<span className="text-xs">{renderFormattedDateWithoutYear(issue.target_date)}</span>
|
<span className="text-xs">{renderFormattedDateWithoutYear(issue.target_date)}</span>
|
||||||
|
@ -4,7 +4,7 @@ import { X } from "lucide-react";
|
|||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||||
// constants
|
// constants
|
||||||
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
editable: boolean | undefined;
|
editable: boolean | undefined;
|
||||||
@ -18,7 +18,7 @@ export const AppliedDateFilters: React.FC<Props> = observer((props) => {
|
|||||||
const getDateLabel = (value: string): string => {
|
const getDateLabel = (value: string): string => {
|
||||||
let dateLabel = "";
|
let dateLabel = "";
|
||||||
|
|
||||||
const dateDetails = DATE_FILTER_OPTIONS.find((d) => d.value === value);
|
const dateDetails = DATE_AFTER_FILTER_OPTIONS.find((d) => d.value === value);
|
||||||
|
|
||||||
if (dateDetails) dateLabel = dateDetails.name;
|
if (dateDetails) dateLabel = dateDetails.name;
|
||||||
else {
|
else {
|
||||||
|
@ -3,6 +3,7 @@ import { observer } from "mobx-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
import { Info, Star } from "lucide-react";
|
import { Info, Star } from "lucide-react";
|
||||||
import { Avatar, AvatarGroup, Tooltip, LayersIcon, CycleGroupIcon, setPromiseToast } from "@plane/ui";
|
import { Avatar, AvatarGroup, Tooltip, LayersIcon, CycleGroupIcon, setPromiseToast } from "@plane/ui";
|
||||||
@ -38,6 +39,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
|
|||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
// computed
|
// computed
|
||||||
const cycleDetails = getCycleById(cycleId);
|
const cycleDetails = getCycleById(cycleId);
|
||||||
|
// hooks
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
if (!cycleDetails) return null;
|
if (!cycleDetails) return null;
|
||||||
|
|
||||||
@ -145,7 +148,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
|
|||||||
<span className="flex-shrink-0">
|
<span className="flex-shrink-0">
|
||||||
<CycleGroupIcon cycleGroup={cycleStatus as TCycleGroups} className="h-3.5 w-3.5" />
|
<CycleGroupIcon cycleGroup={cycleStatus as TCycleGroups} className="h-3.5 w-3.5" />
|
||||||
</span>
|
</span>
|
||||||
<Tooltip tooltipContent={cycleDetails.name} position="top">
|
<Tooltip tooltipContent={cycleDetails.name} position="top" isMobile={isMobile}>
|
||||||
<span className="truncate text-base font-medium">{cycleDetails.name}</span>
|
<span className="truncate text-base font-medium">{cycleDetails.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@ -176,7 +179,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
|
|||||||
<span className="text-xs text-custom-text-300">{issueCount}</span>
|
<span className="text-xs text-custom-text-300">{issueCount}</span>
|
||||||
</div>
|
</div>
|
||||||
{cycleDetails.assignee_ids.length > 0 && (
|
{cycleDetails.assignee_ids.length > 0 && (
|
||||||
<Tooltip tooltipContent={`${cycleDetails.assignee_ids.length} Members`}>
|
<Tooltip tooltipContent={`${cycleDetails.assignee_ids.length} Members`} isMobile={isMobile}>
|
||||||
<div className="flex cursor-default items-center gap-1">
|
<div className="flex cursor-default items-center gap-1">
|
||||||
<AvatarGroup showTooltip={false}>
|
<AvatarGroup showTooltip={false}>
|
||||||
{cycleDetails.assignee_ids.map((assigne_id) => {
|
{cycleDetails.assignee_ids.map((assigne_id) => {
|
||||||
@ -190,6 +193,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={isNaN(completionPercentage) ? "0" : `${completionPercentage.toFixed(0)}%`}
|
tooltipContent={isNaN(completionPercentage) ? "0" : `${completionPercentage.toFixed(0)}%`}
|
||||||
position="top-left"
|
position="top-left"
|
||||||
>
|
>
|
||||||
|
@ -5,6 +5,7 @@ import { ListFilter, Search, X } from "lucide-react";
|
|||||||
// hooks
|
// hooks
|
||||||
import { useCycleFilter } from "hooks/store";
|
import { useCycleFilter } from "hooks/store";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
import { CycleFiltersSelection } from "components/cycles";
|
import { CycleFiltersSelection } from "components/cycles";
|
||||||
import { FiltersDropdown } from "components/issues";
|
import { FiltersDropdown } from "components/issues";
|
||||||
@ -36,6 +37,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||||||
updateFilters,
|
updateFilters,
|
||||||
updateSearchQuery,
|
updateSearchQuery,
|
||||||
} = useCycleFilter();
|
} = useCycleFilter();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
// outside click detector hook
|
// outside click detector hook
|
||||||
useOutsideClickDetector(inputRef, () => {
|
useOutsideClickDetector(inputRef, () => {
|
||||||
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||||
@ -62,7 +64,10 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
|
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
|
||||||
else setIsSearchOpen(false);
|
else {
|
||||||
|
setIsSearchOpen(false);
|
||||||
|
inputRef.current?.blur();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -107,7 +112,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||||||
<Search className="h-3.5 w-3.5" />
|
<Search className="h-3.5 w-3.5" />
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 focus:outline-none"
|
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"
|
placeholder="Search"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => updateSearchQuery(e.target.value)}
|
onChange={(e) => updateSearchQuery(e.target.value)}
|
||||||
@ -131,7 +136,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
|
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
|
||||||
{CYCLE_VIEW_LAYOUTS.map((layout) => (
|
{CYCLE_VIEW_LAYOUTS.map((layout) => (
|
||||||
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
<Tooltip key={layout.key} tooltipContent={layout.title} isMobile={isMobile}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
||||||
|
@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { DateFilterModal } from "components/core";
|
import { DateFilterModal } from "components/core";
|
||||||
import { FilterHeader, FilterOption } from "components/issues";
|
import { FilterHeader, FilterOption } from "components/issues";
|
||||||
// constants
|
// constants
|
||||||
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appliedFilters: string[] | null;
|
appliedFilters: string[] | null;
|
||||||
@ -21,7 +21,9 @@ export const FilterEndDate: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) =>
|
||||||
|
d.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { DateFilterModal } from "components/core";
|
import { DateFilterModal } from "components/core";
|
||||||
import { FilterHeader, FilterOption } from "components/issues";
|
import { FilterHeader, FilterOption } from "components/issues";
|
||||||
// constants
|
// constants
|
||||||
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appliedFilters: string[] | null;
|
appliedFilters: string[] | null;
|
||||||
@ -21,7 +21,9 @@ export const FilterStartDate: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) =>
|
||||||
|
d.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, ContrastIcon } from "@plane/ui";
|
import { Tooltip, ContrastIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -22,7 +24,7 @@ export const CycleGanttBlock: React.FC<Props> = observer((props) => {
|
|||||||
const { getCycleById } = useCycle();
|
const { getCycleById } = useCycle();
|
||||||
// derived values
|
// derived values
|
||||||
const cycleDetails = getCycleById(cycleId);
|
const cycleDetails = getCycleById(cycleId);
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
|
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -33,17 +35,18 @@ export const CycleGanttBlock: React.FC<Props> = observer((props) => {
|
|||||||
cycleStatus === "current"
|
cycleStatus === "current"
|
||||||
? "#09a953"
|
? "#09a953"
|
||||||
: cycleStatus === "upcoming"
|
: cycleStatus === "upcoming"
|
||||||
? "#f7ae59"
|
? "#f7ae59"
|
||||||
: cycleStatus === "completed"
|
: cycleStatus === "completed"
|
||||||
? "#3f76ff"
|
? "#3f76ff"
|
||||||
: cycleStatus === "draft"
|
: cycleStatus === "draft"
|
||||||
? "rgb(var(--color-text-200))"
|
? "rgb(var(--color-text-200))"
|
||||||
: "",
|
: "",
|
||||||
}}
|
}}
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project_id}/cycles/${cycleDetails?.id}`)}
|
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project_id}/cycles/${cycleDetails?.id}`)}
|
||||||
>
|
>
|
||||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h5>{cycleDetails?.name}</h5>
|
<h5>{cycleDetails?.name}</h5>
|
||||||
@ -63,8 +66,6 @@ export const CycleGanttBlock: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
||||||
const { cycleId } = props;
|
const { cycleId } = props;
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
router: { workspaceSlug },
|
router: { workspaceSlug },
|
||||||
@ -76,9 +77,9 @@ export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
|
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Link
|
||||||
className="relative flex h-full w-full items-center gap-2"
|
className="relative flex h-full w-full items-center gap-2"
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project_id}/cycles/${cycleDetails?.id}`)}
|
href={`/${workspaceSlug}/projects/${cycleDetails?.project_id}/cycles/${cycleDetails?.id}`}
|
||||||
>
|
>
|
||||||
<ContrastIcon
|
<ContrastIcon
|
||||||
className="h-5 w-5 flex-shrink-0"
|
className="h-5 w-5 flex-shrink-0"
|
||||||
@ -86,15 +87,15 @@ export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
cycleStatus === "current"
|
cycleStatus === "current"
|
||||||
? "#09a953"
|
? "#09a953"
|
||||||
: cycleStatus === "upcoming"
|
: cycleStatus === "upcoming"
|
||||||
? "#f7ae59"
|
? "#f7ae59"
|
||||||
: cycleStatus === "completed"
|
: cycleStatus === "completed"
|
||||||
? "#3f76ff"
|
? "#3f76ff"
|
||||||
: cycleStatus === "draft"
|
: cycleStatus === "draft"
|
||||||
? "rgb(var(--color-text-200))"
|
? "rgb(var(--color-text-200))"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<h6 className="flex-grow truncate text-sm font-medium">{cycleDetails?.name}</h6>
|
<h6 className="flex-grow truncate text-sm font-medium">{cycleDetails?.name}</h6>
|
||||||
</div>
|
</Link>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -3,6 +3,7 @@ import { observer } from "mobx-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
import { Check, Info, Star, User2 } from "lucide-react";
|
import { Check, Info, Star, User2 } from "lucide-react";
|
||||||
import { Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarGroup, Avatar, setPromiseToast } from "@plane/ui";
|
import { Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarGroup, Avatar, setPromiseToast } from "@plane/ui";
|
||||||
import { CycleQuickActions } from "components/cycles";
|
import { CycleQuickActions } from "components/cycles";
|
||||||
@ -33,6 +34,8 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
|||||||
const { cycleId, workspaceSlug, projectId } = props;
|
const { cycleId, workspaceSlug, projectId } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
// hooks
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
@ -164,7 +167,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
|||||||
|
|
||||||
<div className="relative flex items-center gap-2.5 overflow-hidden">
|
<div className="relative flex items-center gap-2.5 overflow-hidden">
|
||||||
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
|
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
<Tooltip tooltipContent={cycleDetails.name} position="top">
|
<Tooltip tooltipContent={cycleDetails.name} position="top" isMobile={isMobile}>
|
||||||
<span className="line-clamp-1 inline-block overflow-hidden truncate text-base font-medium">
|
<span className="line-clamp-1 inline-block overflow-hidden truncate text-base font-medium">
|
||||||
{cycleDetails.name}
|
{cycleDetails.name}
|
||||||
</span>
|
</span>
|
||||||
@ -196,7 +199,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex flex-shrink-0 items-center gap-3">
|
<div className="relative flex flex-shrink-0 items-center gap-3">
|
||||||
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`}>
|
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
|
||||||
<div className="flex w-10 cursor-default items-center justify-center">
|
<div className="flex w-10 cursor-default items-center justify-center">
|
||||||
{cycleDetails.assignee_ids?.length > 0 ? (
|
{cycleDetails.assignee_ids?.length > 0 ? (
|
||||||
<AvatarGroup showTooltip={false}>
|
<AvatarGroup showTooltip={false}>
|
||||||
|
@ -4,7 +4,7 @@ import { cn } from "helpers/common.helper";
|
|||||||
// types
|
// types
|
||||||
import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS } from "./constants";
|
import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS } from "./constants";
|
||||||
import { TButtonVariants } from "./types";
|
import { TButtonVariants } from "./types";
|
||||||
// constants
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
|
|
||||||
export type DropdownButtonProps = {
|
export type DropdownButtonProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -27,7 +27,6 @@ type ButtonProps = {
|
|||||||
|
|
||||||
export const DropdownButton: React.FC<DropdownButtonProps> = (props) => {
|
export const DropdownButton: React.FC<DropdownButtonProps> = (props) => {
|
||||||
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip, variant } = props;
|
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip, variant } = props;
|
||||||
|
|
||||||
const ButtonToRender: React.FC<ButtonProps> = BORDER_BUTTON_VARIANTS.includes(variant)
|
const ButtonToRender: React.FC<ButtonProps> = BORDER_BUTTON_VARIANTS.includes(variant)
|
||||||
? BorderButton
|
? BorderButton
|
||||||
: BACKGROUND_BUTTON_VARIANTS.includes(variant)
|
: BACKGROUND_BUTTON_VARIANTS.includes(variant)
|
||||||
@ -49,9 +48,10 @@ export const DropdownButton: React.FC<DropdownButtonProps> = (props) => {
|
|||||||
|
|
||||||
const BorderButton: React.FC<ButtonProps> = (props) => {
|
const BorderButton: React.FC<ButtonProps> = (props) => {
|
||||||
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
|
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
|
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip} isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||||
@ -67,9 +67,9 @@ const BorderButton: React.FC<ButtonProps> = (props) => {
|
|||||||
|
|
||||||
const BackgroundButton: React.FC<ButtonProps> = (props) => {
|
const BackgroundButton: React.FC<ButtonProps> = (props) => {
|
||||||
const { children, className, tooltipContent, tooltipHeading, showTooltip } = props;
|
const { children, className, tooltipContent, tooltipHeading, showTooltip } = props;
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
|
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip} isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||||
@ -84,9 +84,9 @@ const BackgroundButton: React.FC<ButtonProps> = (props) => {
|
|||||||
|
|
||||||
const TransparentButton: React.FC<ButtonProps> = (props) => {
|
const TransparentButton: React.FC<ButtonProps> = (props) => {
|
||||||
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
|
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
|
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip} isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||||
|
@ -8,6 +8,7 @@ import { cn } from "helpers/common.helper";
|
|||||||
import { useModule } from "hooks/store";
|
import { useModule } from "hooks/store";
|
||||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
import { DropdownButton } from "../buttons";
|
import { DropdownButton } from "../buttons";
|
||||||
// icons
|
// icons
|
||||||
@ -47,6 +48,7 @@ type ButtonContentProps = {
|
|||||||
onChange: (moduleIds: string[]) => void;
|
onChange: (moduleIds: string[]) => void;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
showCount: boolean;
|
showCount: boolean;
|
||||||
|
showTooltip?: boolean;
|
||||||
value: string | string[] | null;
|
value: string | string[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -60,10 +62,12 @@ const ButtonContent: React.FC<ButtonContentProps> = (props) => {
|
|||||||
onChange,
|
onChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
showCount,
|
showCount,
|
||||||
|
showTooltip = false,
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getModuleById } = useModule();
|
const { getModuleById } = useModule();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
if (Array.isArray(value))
|
if (Array.isArray(value))
|
||||||
return (
|
return (
|
||||||
@ -90,12 +94,12 @@ const ButtonContent: React.FC<ButtonContentProps> = (props) => {
|
|||||||
>
|
>
|
||||||
{!hideIcon && <DiceIcon className="h-2.5 w-2.5 flex-shrink-0" />}
|
{!hideIcon && <DiceIcon className="h-2.5 w-2.5 flex-shrink-0" />}
|
||||||
{!hideText && (
|
{!hideText && (
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={moduleDetails?.name}>
|
<Tooltip tooltipHeading="Title" tooltipContent={moduleDetails?.name} disabled={!showTooltip} isMobile={isMobile}>
|
||||||
<span className="max-w-40 flex-grow truncate text-xs font-medium">{moduleDetails?.name}</span>
|
<span className="max-w-40 flex-grow truncate text-xs font-medium">{moduleDetails?.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<Tooltip tooltipContent="Remove">
|
<Tooltip tooltipContent="Remove" disabled={!showTooltip} isMobile={isMobile}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
@ -265,6 +269,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
|
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
showCount={showCount}
|
showCount={showCount}
|
||||||
|
showTooltip={showTooltip}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange as any}
|
onChange={onChange as any}
|
||||||
/>
|
/>
|
||||||
|
@ -9,6 +9,7 @@ import { ISSUE_PRIORITIES } from "constants/issue";
|
|||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// icons
|
// icons
|
||||||
// helpers
|
// helpers
|
||||||
// types
|
// types
|
||||||
@ -61,8 +62,10 @@ const BorderButton = (props: ButtonProps) => {
|
|||||||
none: "hover:bg-custom-background-80 border-custom-border-300",
|
none: "hover:bg-custom-background-80 border-custom-border-300",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
|
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip} isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5",
|
"h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5",
|
||||||
@ -130,8 +133,10 @@ const BackgroundButton = (props: ButtonProps) => {
|
|||||||
none: "bg-custom-background-80",
|
none: "bg-custom-background-80",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
|
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip} isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5",
|
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5",
|
||||||
@ -200,8 +205,10 @@ const TransparentButton = (props: ButtonProps) => {
|
|||||||
none: "hover:text-custom-text-300",
|
none: "hover:text-custom-text-300",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
|
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip} isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||||
|
@ -19,7 +19,7 @@ export const MonthChartView: FC<any> = observer(() => {
|
|||||||
{monthBlocks?.map((block, rootIndex) => (
|
{monthBlocks?.map((block, rootIndex) => (
|
||||||
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
|
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
|
||||||
<div
|
<div
|
||||||
className="w-full sticky top-0 z-[5] bg-custom-background-100"
|
className="w-full sticky top-0 z-[5] bg-custom-background-100 flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
height: `${HEADER_HEIGHT}px`,
|
height: `${HEADER_HEIGHT}px`,
|
||||||
}}
|
}}
|
||||||
@ -55,7 +55,7 @@ export const MonthChartView: FC<any> = observer(() => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-full flex divide-x divide-custom-border-100/50">
|
<div className="h-full w-full flex-grow flex divide-x divide-custom-border-100/50">
|
||||||
{block?.children?.map((monthDay, index) => (
|
{block?.children?.map((monthDay, index) => (
|
||||||
<div
|
<div
|
||||||
key={`column-${rootIndex}-${index}`}
|
key={`column-${rootIndex}-${index}`}
|
||||||
|
@ -9,6 +9,8 @@ import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-ti
|
|||||||
// types
|
// types
|
||||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||||
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: IGanttBlock;
|
block: IGanttBlock;
|
||||||
@ -23,6 +25,8 @@ export const ChartAddBlock: React.FC<Props> = observer((props) => {
|
|||||||
const [buttonStartDate, setButtonStartDate] = useState<Date | null>(null);
|
const [buttonStartDate, setButtonStartDate] = useState<Date | null>(null);
|
||||||
// refs
|
// refs
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
// hooks
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
// chart hook
|
// chart hook
|
||||||
const { currentViewData } = useGanttChart();
|
const { currentViewData } = useGanttChart();
|
||||||
|
|
||||||
@ -73,7 +77,7 @@ export const ChartAddBlock: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<div ref={containerRef} className="h-full w-full" />
|
<div ref={containerRef} className="h-full w-full" />
|
||||||
{isButtonVisible && (
|
{isButtonVisible && (
|
||||||
<Tooltip tooltipContent={buttonStartDate && renderFormattedDate(buttonStartDate)}>
|
<Tooltip tooltipContent={buttonStartDate && renderFormattedDate(buttonStartDate)} isMobile={isMobile}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="absolute top-1/2 -translate-x-1/2 -translate-y-1/2 h-8 w-8 bg-custom-background-80 p-1.5 rounded border border-custom-border-300 grid place-items-center text-custom-text-200 hover:text-custom-text-100"
|
className="absolute top-1/2 -translate-x-1/2 -translate-y-1/2 h-8 w-8 bg-custom-background-80 p-1.5 rounded border border-custom-border-300 grid place-items-center text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
@ -3,6 +3,8 @@ import { observer } from "mobx-react-lite";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
|
// components
|
||||||
import { ArrowRight, Plus, PanelRight } from "lucide-react";
|
import { ArrowRight, Plus, PanelRight } from "lucide-react";
|
||||||
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui";
|
||||||
import { ProjectAnalyticsModal } from "components/analytics";
|
import { ProjectAnalyticsModal } from "components/analytics";
|
||||||
@ -26,7 +28,6 @@ import {
|
|||||||
useIssues,
|
useIssues,
|
||||||
} from "hooks/store";
|
} from "hooks/store";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
|
||||||
// ui
|
// ui
|
||||||
// icons
|
// icons
|
||||||
// helpers
|
// helpers
|
||||||
@ -43,6 +44,7 @@ const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
|
|||||||
const { getCycleById } = useCycle();
|
const { getCycleById } = useCycle();
|
||||||
// derived values
|
// derived values
|
||||||
const cycle = getCycleById(cycleId);
|
const cycle = getCycleById(cycleId);
|
||||||
|
//
|
||||||
|
|
||||||
if (!cycle) return null;
|
if (!cycle) return null;
|
||||||
|
|
||||||
@ -84,6 +86,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
const {
|
const {
|
||||||
project: { projectMemberIds },
|
project: { projectMemberIds },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
|
const { isMobile } = usePlatformOS()
|
||||||
|
|
||||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
@ -207,6 +210,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
<p className="truncate">{cycleDetails?.name && cycleDetails.name}</p>
|
<p className="truncate">{cycleDetails?.name && cycleDetails.name}</p>
|
||||||
{issueCount && issueCount > 0 ? (
|
{issueCount && issueCount > 0 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={`There are ${issueCount} ${
|
tooltipContent={`There are ${issueCount} ${
|
||||||
issueCount > 1 ? "issues" : "issue"
|
issueCount > 1 ? "issues" : "issue"
|
||||||
} in this cycle`}
|
} in this cycle`}
|
||||||
|
@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
import { List, PlusIcon, Sheet } from "lucide-react";
|
import { List, PlusIcon, Sheet } from "lucide-react";
|
||||||
import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
@ -46,6 +47,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
workspace: { workspaceMemberIds },
|
workspace: { workspaceMemberIds },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined;
|
const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined;
|
||||||
|
|
||||||
@ -133,7 +135,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
|
|||||||
{GLOBAL_VIEW_LAYOUTS.map((layout) => (
|
{GLOBAL_VIEW_LAYOUTS.map((layout) => (
|
||||||
<Link key={layout.key} href={`/${workspaceSlug}/${layout.link}`}>
|
<Link key={layout.key} href={`/${workspaceSlug}/${layout.link}`}>
|
||||||
<span>
|
<span>
|
||||||
<Tooltip tooltipContent={layout.title}>
|
<Tooltip tooltipContent={layout.title} isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
||||||
activeLayout === layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
activeLayout === layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
||||||
|
@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
import { ArrowRight, PanelRight, Plus } from "lucide-react";
|
import { ArrowRight, PanelRight, Plus } from "lucide-react";
|
||||||
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui";
|
||||||
import { ProjectAnalyticsModal } from "components/analytics";
|
import { ProjectAnalyticsModal } from "components/analytics";
|
||||||
@ -66,6 +67,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||||
|
// hooks
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
issuesFilter: { issueFilters },
|
issuesFilter: { issueFilters },
|
||||||
@ -208,6 +211,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
<p className="truncate">{moduleDetails?.name && moduleDetails.name}</p>
|
<p className="truncate">{moduleDetails?.name && moduleDetails.name}</p>
|
||||||
{issueCount && issueCount > 0 ? (
|
{issueCount && issueCount > 0 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={`There are ${issueCount} ${
|
tooltipContent={`There are ${issueCount} ${
|
||||||
issueCount > 1 ? "issues" : "issue"
|
issueCount > 1 ? "issues" : "issue"
|
||||||
} in this module`}
|
} in this module`}
|
||||||
|
@ -1,24 +1,36 @@
|
|||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// icons
|
import { GanttChartSquare, LayoutGrid, List, ListFilter, Plus, Search, X } from "lucide-react";
|
||||||
import { GanttChartSquare, LayoutGrid, List, Plus } from "lucide-react";
|
// hooks
|
||||||
// ui
|
import { useApplication, useEventTracker, useMember, useModuleFilter, useProject, useUser } from "hooks/store";
|
||||||
import { Breadcrumbs, Button, Tooltip, DiceIcon, CustomMenu } from "@plane/ui";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
|
import { ProjectLogo } from "components/project";
|
||||||
// constants
|
// constants
|
||||||
import { MODULE_VIEW_LAYOUTS } from "constants/module";
|
import { MODULE_VIEW_LAYOUTS } from "constants/module";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import { ModuleFiltersSelection, ModuleOrderByDropdown } from "components/modules";
|
||||||
import { ProjectLogo } from "components/project";
|
import { FiltersDropdown } from "components/issues";
|
||||||
|
// ui
|
||||||
|
import { Breadcrumbs, Button, Tooltip, DiceIcon, CustomMenu } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
// types
|
||||||
|
import { TModuleFilters } from "@plane/types";
|
||||||
|
|
||||||
export const ModulesListHeader: React.FC = observer(() => {
|
export const ModulesListHeader: React.FC = observer(() => {
|
||||||
|
// states
|
||||||
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
|
// refs
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { commandPalette: commandPaletteStore } = useApplication();
|
const { commandPalette: commandPaletteStore } = useApplication();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
@ -26,11 +38,56 @@ export const ModulesListHeader: React.FC = observer(() => {
|
|||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
const {
|
||||||
|
workspace: { workspaceMemberIds },
|
||||||
|
} = useMember();
|
||||||
|
const {
|
||||||
|
currentProjectDisplayFilters: displayFilters,
|
||||||
|
currentProjectFilters: filters,
|
||||||
|
searchQuery,
|
||||||
|
updateDisplayFilters,
|
||||||
|
updateFilters,
|
||||||
|
updateSearchQuery,
|
||||||
|
} = useModuleFilter();
|
||||||
|
// outside click detector hook
|
||||||
|
useOutsideClickDetector(inputRef, () => {
|
||||||
|
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
const { storedValue: modulesView, setValue: setModulesView } = useLocalStorage("modules_view", "grid");
|
const handleFilters = useCallback(
|
||||||
|
(key: keyof TModuleFilters, value: string | string[]) => {
|
||||||
|
if (!projectId) return;
|
||||||
|
const newValues = filters?.[key] ?? [];
|
||||||
|
|
||||||
|
if (Array.isArray(value))
|
||||||
|
value.forEach((val) => {
|
||||||
|
if (!newValues.includes(val)) newValues.push(val);
|
||||||
|
});
|
||||||
|
else {
|
||||||
|
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
|
else newValues.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilters(projectId.toString(), { [key]: newValues });
|
||||||
|
},
|
||||||
|
[filters, projectId, updateFilters]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
|
||||||
|
else {
|
||||||
|
setIsSearchOpen(false);
|
||||||
|
inputRef.current?.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// auth
|
||||||
const canUserCreateModule =
|
const canUserCreateModule =
|
||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||||
@ -62,26 +119,97 @@ export const ModulesListHeader: React.FC = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="items-center gap-1 rounded bg-custom-background-80 p-1 hidden md:flex">
|
<div className="flex items-center">
|
||||||
{MODULE_VIEW_LAYOUTS.map((layout) => (
|
{!isSearchOpen && (
|
||||||
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
className="grid place-items-center"
|
||||||
modulesView == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
onClick={() => {
|
||||||
}`}
|
// updateSearchQuery("");
|
||||||
onClick={() => setModulesView(layout.key)}
|
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
|
<layout.icon
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
className={`h-3.5 w-3.5 ${
|
className={cn("h-3.5 w-3.5 text-custom-text-200", {
|
||||||
modulesView == layout.key ? "text-custom-text-100" : "text-custom-text-200"
|
"text-custom-text-100": displayFilters?.layout === layout.key,
|
||||||
}`}
|
})}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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 && (
|
{canUserCreateModule && (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@ -104,9 +232,9 @@ export const ModulesListHeader: React.FC = observer(() => {
|
|||||||
// placement="bottom-start"
|
// placement="bottom-start"
|
||||||
customButton={
|
customButton={
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
{modulesView === "gantt_chart" ? (
|
{displayFilters?.layout === "gantt" ? (
|
||||||
<GanttChartSquare className="w-3 h-3" />
|
<GanttChartSquare className="w-3 h-3" />
|
||||||
) : modulesView === "grid" ? (
|
) : displayFilters?.layout === "board" ? (
|
||||||
<LayoutGrid className="w-3 h-3" />
|
<LayoutGrid className="w-3 h-3" />
|
||||||
) : (
|
) : (
|
||||||
<List className="w-3 h-3" />
|
<List className="w-3 h-3" />
|
||||||
@ -120,7 +248,10 @@ export const ModulesListHeader: React.FC = observer(() => {
|
|||||||
{MODULE_VIEW_LAYOUTS.map((layout) => (
|
{MODULE_VIEW_LAYOUTS.map((layout) => (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={layout.key}
|
key={layout.key}
|
||||||
onClick={() => setModulesView(layout.key)}
|
onClick={() => {
|
||||||
|
if (!projectId) return;
|
||||||
|
updateDisplayFilters(projectId.toString(), { layout: layout.key });
|
||||||
|
}}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<layout.icon className="w-3 h-3" />
|
<layout.icon className="w-3 h-3" />
|
||||||
|
@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// constants
|
// constants
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
||||||
@ -31,10 +32,10 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
|
|||||||
const {
|
const {
|
||||||
project: { projectMemberIds },
|
project: { projectMemberIds },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
|
|
||||||
// for archived issues list layout is the only option
|
// for archived issues list layout is the only option
|
||||||
const activeLayout = "list";
|
const activeLayout = "list";
|
||||||
|
// hooks
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const handleFiltersUpdate = (key: keyof IIssueFilterOptions, value: string | string[]) => {
|
const handleFiltersUpdate = (key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
@ -119,6 +120,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
{issueCount && issueCount > 0 ? (
|
{issueCount && issueCount > 0 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in project's archived`}
|
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in project's archived`}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
|
@ -2,6 +2,7 @@ import { FC, useCallback } from "react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
@ -28,7 +29,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
|||||||
const {
|
const {
|
||||||
project: { projectMemberIds },
|
project: { projectMemberIds },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
const handleFiltersUpdate = useCallback(
|
const handleFiltersUpdate = useCallback(
|
||||||
@ -112,6 +113,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
{issueCount && issueCount > 0 ? (
|
{issueCount && issueCount > 0 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in project's draft`}
|
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in project's draft`}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
|
@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react";
|
import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
|
||||||
import { ProjectAnalyticsModal } from "components/analytics";
|
import { ProjectAnalyticsModal } from "components/analytics";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
@ -52,7 +53,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
const { projectStates } = useProjectState();
|
const { projectStates } = useProjectState();
|
||||||
const { projectLabels } = useLabel();
|
const { projectLabels } = useLabel();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
const handleFiltersUpdate = useCallback(
|
const handleFiltersUpdate = useCallback(
|
||||||
@ -153,6 +154,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
{issueCount && issueCount > 0 ? (
|
{issueCount && issueCount > 0 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in this project`}
|
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in this project`}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
|
@ -1,26 +1,81 @@
|
|||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Search, Plus, Briefcase } from "lucide-react";
|
import { Search, Plus, Briefcase, X, ListFilter } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
// ui
|
import { useApplication, useEventTracker, useMember, useProject, useProjectFilter, useUser } from "hooks/store";
|
||||||
import { Breadcrumbs, Button } from "@plane/ui";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// constants
|
// components
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
|
// ui
|
||||||
|
import { Breadcrumbs, Button } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
// components
|
import { FiltersDropdown } from "components/issues";
|
||||||
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "components/project";
|
||||||
|
import { TProjectFilters } from "@plane/types";
|
||||||
|
|
||||||
export const ProjectsHeader = observer(() => {
|
export const ProjectsHeader = observer(() => {
|
||||||
|
// states
|
||||||
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
|
// refs
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { commandPalette: commandPaletteStore } = useApplication();
|
const {
|
||||||
|
commandPalette: commandPaletteStore,
|
||||||
|
router: { workspaceSlug },
|
||||||
|
} = useApplication();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { workspaceProjectIds, searchQuery, setSearchQuery } = useProject();
|
const { workspaceProjectIds } = useProject();
|
||||||
|
const {
|
||||||
|
currentWorkspaceDisplayFilters: displayFilters,
|
||||||
|
currentWorkspaceFilters: filters,
|
||||||
|
updateFilters,
|
||||||
|
updateDisplayFilters,
|
||||||
|
searchQuery,
|
||||||
|
updateSearchQuery,
|
||||||
|
} = useProjectFilter();
|
||||||
|
const {
|
||||||
|
workspace: { workspaceMemberIds },
|
||||||
|
} = useMember();
|
||||||
|
// outside click detector hook
|
||||||
|
useOutsideClickDetector(inputRef, () => {
|
||||||
|
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||||
|
});
|
||||||
|
// auth
|
||||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
|
const handleFilters = useCallback(
|
||||||
|
(key: keyof TProjectFilters, value: string | string[]) => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
const newValues = filters?.[key] ?? [];
|
||||||
|
|
||||||
|
if (Array.isArray(value))
|
||||||
|
value.forEach((val) => {
|
||||||
|
if (!newValues.includes(val)) newValues.push(val);
|
||||||
|
});
|
||||||
|
else {
|
||||||
|
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
|
else newValues.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilters(workspaceSlug, { [key]: newValues });
|
||||||
|
},
|
||||||
|
[filters, updateFilters, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
|
||||||
|
else setIsSearchOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
@ -34,18 +89,74 @@ export const ProjectsHeader = observer(() => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end items-center gap-3">
|
<div className="w-full flex items-center justify-end gap-3">
|
||||||
{workspaceProjectIds && workspaceProjectIds?.length > 0 && (
|
{workspaceProjectIds && workspaceProjectIds?.length > 0 && (
|
||||||
<div className=" flex items-center justify-start gap-1 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5 text-custom-text-400">
|
<div className="flex items-center">
|
||||||
<Search className="h-3.5" />
|
{!isSearchOpen && (
|
||||||
<input
|
<button
|
||||||
className="border-none w-full bg-transparent text-sm focus:outline-none"
|
type="button"
|
||||||
value={searchQuery}
|
className="-mr-1 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onClick={() => {
|
||||||
placeholder="Search"
|
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 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>
|
||||||
)}
|
)}
|
||||||
|
<ProjectOrderByDropdown
|
||||||
|
value={displayFilters?.order_by}
|
||||||
|
onChange={(val) => {
|
||||||
|
if (!workspaceSlug || val === displayFilters?.order_by) return;
|
||||||
|
updateDisplayFilters(workspaceSlug, {
|
||||||
|
order_by: val,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
||||||
|
<ProjectFiltersSelection
|
||||||
|
displayFilters={displayFilters ?? {}}
|
||||||
|
filters={filters ?? {}}
|
||||||
|
handleFiltersUpdate={handleFilters}
|
||||||
|
handleDisplayFiltersUpdate={(val) => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
updateDisplayFilters(workspaceSlug, val);
|
||||||
|
}}
|
||||||
|
memberIds={workspaceMemberIds ?? undefined}
|
||||||
|
/>
|
||||||
|
</FiltersDropdown>
|
||||||
{isAuthorizedUser && (
|
{isAuthorizedUser && (
|
||||||
<Button
|
<Button
|
||||||
prependIcon={<Plus />}
|
prependIcon={<Plus />}
|
||||||
@ -54,9 +165,9 @@ export const ProjectsHeader = observer(() => {
|
|||||||
setTrackElement("Projects page");
|
setTrackElement("Projects page");
|
||||||
commandPaletteStore.toggleCreateProjectModal(true);
|
commandPaletteStore.toggleCreateProjectModal(true);
|
||||||
}}
|
}}
|
||||||
className="items-center"
|
className="items-center gap-1"
|
||||||
>
|
>
|
||||||
<div className="hidden sm:block">Add</div> Project
|
<span className="hidden sm:inline-block">Add</span> Project
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,7 @@ import { useRouter } from "next/router";
|
|||||||
// icons
|
// icons
|
||||||
import { CalendarDays } from "lucide-react";
|
import { CalendarDays } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, PriorityIcon } from "@plane/ui";
|
import { Tooltip, PriorityIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -33,7 +34,7 @@ export const InboxIssueListItem: FC<TInboxIssueListItem> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const inboxIssueDetail = getInboxIssueByIssueId(inboxId, issueId);
|
const inboxIssueDetail = getInboxIssueByIssueId(inboxId, issueId);
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
|
|
||||||
@ -83,10 +84,10 @@ export const InboxIssueListItem: FC<TInboxIssueListItem> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Tooltip tooltipHeading="Priority" tooltipContent={`${issue.priority ?? "None"}`}>
|
<Tooltip tooltipHeading="Priority" tooltipContent={`${issue.priority ?? "None"}`} isMobile={isMobile}>
|
||||||
<PriorityIcon priority={issue.priority ?? null} className="h-3.5 w-3.5" />
|
<PriorityIcon priority={issue.priority ?? null} className="h-3.5 w-3.5" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip tooltipHeading="Created on" tooltipContent={`${renderFormattedDate(issue.created_at ?? "")}`}>
|
<Tooltip tooltipHeading="Created on" tooltipContent={`${renderFormattedDate(issue.created_at ?? "")}`} isMobile={isMobile}>
|
||||||
<div className="flex items-center gap-1 rounded border border-custom-border-200 px-2 py-[0.19rem] text-xs text-custom-text-200 shadow-sm">
|
<div className="flex items-center gap-1 rounded border border-custom-border-200 px-2 py-[0.19rem] text-xs text-custom-text-200 shadow-sm">
|
||||||
<CalendarDays size={12} strokeWidth={1.5} />
|
<CalendarDays size={12} strokeWidth={1.5} />
|
||||||
<span>{renderFormattedDate(issue.created_at ?? "")}</span>
|
<span>{renderFormattedDate(issue.created_at ?? "")}</span>
|
||||||
|
@ -9,6 +9,7 @@ import { Menu, Transition } from "@headlessui/react";
|
|||||||
// icons
|
// icons
|
||||||
import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react";
|
import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
import { Avatar, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Avatar, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
import { useApplication, useUser } from "hooks/store";
|
import { useApplication, useUser } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
@ -34,7 +35,7 @@ export const InstanceSidebarDropdown = observer(() => {
|
|||||||
const { signOut, currentUser, currentUserSettings } = useUser();
|
const { signOut, currentUser, currentUserSettings } = useUser();
|
||||||
// hooks
|
// hooks
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
// redirect url for normal mode
|
// redirect url for normal mode
|
||||||
const redirectWorkspaceSlug =
|
const redirectWorkspaceSlug =
|
||||||
workspaceSlug ||
|
workspaceSlug ||
|
||||||
@ -73,7 +74,7 @@ export const InstanceSidebarDropdown = observer(() => {
|
|||||||
{!sidebarCollapsed && (
|
{!sidebarCollapsed && (
|
||||||
<div className="flex w-full gap-2">
|
<div className="flex w-full gap-2">
|
||||||
<h4 className="grow truncate text-base font-medium text-custom-text-200">Instance admin</h4>
|
<h4 className="grow truncate text-base font-medium text-custom-text-200">Instance admin</h4>
|
||||||
<Tooltip position="bottom-left" tooltipContent="Exit God Mode">
|
<Tooltip position="bottom-left" tooltipContent="Exit God Mode" isMobile={isMobile}>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Link href={`/${redirectWorkspaceSlug}`}>
|
<Link href={`/${redirectWorkspaceSlug}`}>
|
||||||
<span>
|
<span>
|
||||||
|
@ -2,6 +2,7 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
|
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
import { useApplication } from "hooks/store";
|
import { useApplication } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
@ -46,6 +47,7 @@ export const InstanceAdminSidebarMenu = () => {
|
|||||||
} = useApplication();
|
} = useApplication();
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col gap-2.5 overflow-y-auto px-4 py-6">
|
<div className="flex h-full w-full flex-col gap-2.5 overflow-y-auto px-4 py-6">
|
||||||
@ -55,7 +57,7 @@ export const InstanceAdminSidebarMenu = () => {
|
|||||||
return (
|
return (
|
||||||
<Link key={index} href={item.href}>
|
<Link key={index} href={item.href}>
|
||||||
<div>
|
<div>
|
||||||
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!sidebarCollapsed}>
|
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!sidebarCollapsed} isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
className={`group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none ${
|
className={`group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none ${
|
||||||
isActive
|
isActive
|
||||||
|
@ -11,6 +11,7 @@ import { WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
|
|||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useUser } from "hooks/store";
|
import { useApplication, useUser } from "hooks/store";
|
||||||
import useIntegrationPopup from "hooks/use-integration-popup";
|
import useIntegrationPopup from "hooks/use-integration-popup";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// services
|
// services
|
||||||
import { IntegrationService } from "services/integrations";
|
import { IntegrationService } from "services/integrations";
|
||||||
// icons
|
// icons
|
||||||
@ -55,7 +56,7 @@ export const SingleIntegrationCard: React.FC<Props> = observer(({ integration })
|
|||||||
} = useUser();
|
} = useUser();
|
||||||
|
|
||||||
const isUserAdmin = currentWorkspaceRole === 20;
|
const isUserAdmin = currentWorkspaceRole === 20;
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const { startAuth, isConnecting: isInstalling } = useIntegrationPopup({
|
const { startAuth, isConnecting: isInstalling } = useIntegrationPopup({
|
||||||
provider: integration.provider,
|
provider: integration.provider,
|
||||||
github_app_name: envConfig?.github_app_name || "",
|
github_app_name: envConfig?.github_app_name || "",
|
||||||
@ -129,6 +130,7 @@ export const SingleIntegrationCard: React.FC<Props> = observer(({ integration })
|
|||||||
{workspaceIntegrations ? (
|
{workspaceIntegrations ? (
|
||||||
isInstalled ? (
|
isInstalled ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
disabled={isUserAdmin}
|
disabled={isUserAdmin}
|
||||||
tooltipContent={!isUserAdmin ? "You don't have permission to perform this" : null}
|
tooltipContent={!isUserAdmin ? "You don't have permission to perform this" : null}
|
||||||
>
|
>
|
||||||
@ -147,6 +149,7 @@ export const SingleIntegrationCard: React.FC<Props> = observer(({ integration })
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
disabled={isUserAdmin}
|
disabled={isUserAdmin}
|
||||||
tooltipContent={!isUserAdmin ? "You don't have permission to perform this" : null}
|
tooltipContent={!isUserAdmin ? "You don't have permission to perform this" : null}
|
||||||
>
|
>
|
||||||
|
@ -2,6 +2,7 @@ import { FC, useState } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AlertCircle, X } from "lucide-react";
|
import { AlertCircle, X } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
@ -34,7 +35,7 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = (props) => {
|
|||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
// states
|
// states
|
||||||
const [attachmentDeleteModal, setAttachmentDeleteModal] = useState<boolean>(false);
|
const [attachmentDeleteModal, setAttachmentDeleteModal] = useState<boolean>(false);
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const attachment = attachmentId && getAttachmentById(attachmentId);
|
const attachment = attachmentId && getAttachmentById(attachmentId);
|
||||||
|
|
||||||
if (!attachment) return <></>;
|
if (!attachment) return <></>;
|
||||||
@ -56,10 +57,11 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = (props) => {
|
|||||||
<div className="h-7 w-7">{getFileIcon(getFileExtension(attachment.asset))}</div>
|
<div className="h-7 w-7">{getFileIcon(getFileExtension(attachment.asset))}</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Tooltip tooltipContent={getFileName(attachment.attributes.name)}>
|
<Tooltip tooltipContent={getFileName(attachment.attributes.name)} isMobile={isMobile}>
|
||||||
<span className="text-sm">{truncateText(`${getFileName(attachment.attributes.name)}`, 10)}</span>
|
<span className="text-sm">{truncateText(`${getFileName(attachment.attributes.name)}`, 10)}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={`${
|
tooltipContent={`${
|
||||||
getUserDetails(attachment.updated_by)?.display_name ?? ""
|
getUserDetails(attachment.updated_by)?.display_name ?? ""
|
||||||
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
|
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
|
||||||
|
@ -4,6 +4,7 @@ import { Network } from "lucide-react";
|
|||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper";
|
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper";
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// ui
|
// ui
|
||||||
// components
|
// components
|
||||||
import { IssueUser } from "../";
|
import { IssueUser } from "../";
|
||||||
@ -25,7 +26,7 @@ export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (pr
|
|||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
|
||||||
const activity = getActivityById(activityId);
|
const activity = getActivityById(activityId);
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
if (!activity) return <></>;
|
if (!activity) return <></>;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -42,6 +43,7 @@ export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (pr
|
|||||||
<span> {children} </span>
|
<span> {children} </span>
|
||||||
<span>
|
<span>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={`${renderFormattedDate(activity.created_at)}, ${renderFormattedTime(activity.created_at)}`}
|
tooltipContent={`${renderFormattedDate(activity.created_at)}, ${renderFormattedTime(activity.created_at)}`}
|
||||||
>
|
>
|
||||||
<span className="whitespace-nowrap"> {calculateTimeAgo(activity.created_at)}</span>
|
<span className="whitespace-nowrap"> {calculateTimeAgo(activity.created_at)}</span>
|
||||||
|
@ -2,6 +2,7 @@ import { FC } from "react";
|
|||||||
// hooks
|
// hooks
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// ui
|
// ui
|
||||||
|
|
||||||
type TIssueLink = {
|
type TIssueLink = {
|
||||||
@ -14,12 +15,12 @@ export const IssueLink: FC<TIssueLink> = (props) => {
|
|||||||
const {
|
const {
|
||||||
activity: { getActivityById },
|
activity: { getActivityById },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const activity = getActivityById(activityId);
|
const activity = getActivityById(activityId);
|
||||||
|
|
||||||
if (!activity) return <></>;
|
if (!activity) return <></>;
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipContent={activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"}>
|
<Tooltip tooltipContent={activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"} isMobile={isMobile}>
|
||||||
<a
|
<a
|
||||||
aria-disabled={activity.issue === null}
|
aria-disabled={activity.issue === null}
|
||||||
href={`${
|
href={`${
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
// components
|
// components
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
import { LabelListItem } from "./label-list-item";
|
import { LabelListItem } from "./label-list-item";
|
||||||
@ -14,7 +15,7 @@ type TLabelList = {
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LabelList: FC<TLabelList> = (props) => {
|
export const LabelList: FC<TLabelList> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, issueId, labelOperations, disabled } = props;
|
const { workspaceSlug, projectId, issueId, labelOperations, disabled } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
@ -40,4 +41,4 @@ export const LabelList: FC<TLabelList> = (props) => {
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -9,6 +9,7 @@ import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
|||||||
import { calculateTimeAgo } from "helpers/date-time.helper";
|
import { calculateTimeAgo } from "helpers/date-time.helper";
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
import { useIssueDetail, useMember } from "hooks/store";
|
import { useIssueDetail, useMember } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal";
|
import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal";
|
||||||
|
|
||||||
export type TIssueLinkDetail = {
|
export type TIssueLinkDetail = {
|
||||||
@ -33,7 +34,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
|
|||||||
toggleIssueLinkModalStore(modalToggle);
|
toggleIssueLinkModalStore(modalToggle);
|
||||||
setIsIssueLinkModalOpen(modalToggle);
|
setIsIssueLinkModalOpen(modalToggle);
|
||||||
};
|
};
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const linkDetail = getLinkById(linkId);
|
const linkDetail = getLinkById(linkId);
|
||||||
if (!linkDetail) return <></>;
|
if (!linkDetail) return <></>;
|
||||||
|
|
||||||
@ -64,7 +65,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
|
|||||||
<span className="py-1">
|
<span className="py-1">
|
||||||
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
||||||
</span>
|
</span>
|
||||||
<Tooltip tooltipContent={linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url}>
|
<Tooltip tooltipContent={linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url} isMobile={isMobile}>
|
||||||
<span className="truncate text-xs">
|
<span className="truncate text-xs">
|
||||||
{linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url}
|
{linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url}
|
||||||
</span>
|
</span>
|
||||||
|
@ -71,7 +71,6 @@ export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props)
|
|||||||
hideIcon
|
hideIcon
|
||||||
dropdownArrow
|
dropdownArrow
|
||||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||||
showTooltip
|
|
||||||
multiple
|
multiple
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Pencil, X } from "lucide-react";
|
import { Pencil, X } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
import { ParentIssuesListModal } from "components/issues";
|
import { ParentIssuesListModal } from "components/issues";
|
||||||
@ -35,7 +36,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
|||||||
const parentIssue = issue?.parent_id ? getIssueById(issue.parent_id) : undefined;
|
const parentIssue = issue?.parent_id ? getIssueById(issue.parent_id) : undefined;
|
||||||
const parentIssueProjectDetails =
|
const parentIssueProjectDetails =
|
||||||
parentIssue && parentIssue.project_id ? getProjectById(parentIssue.project_id) : undefined;
|
parentIssue && parentIssue.project_id ? getProjectById(parentIssue.project_id) : undefined;
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const handleParentIssue = async (_issueId: string | null = null) => {
|
const handleParentIssue = async (_issueId: string | null = null) => {
|
||||||
try {
|
try {
|
||||||
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId });
|
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId });
|
||||||
@ -73,7 +74,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
|||||||
>
|
>
|
||||||
{issue.parent_id && parentIssue ? (
|
{issue.parent_id && parentIssue ? (
|
||||||
<div className="flex items-center gap-1 bg-green-500/20 text-green-700 rounded px-1.5 py-1">
|
<div className="flex items-center gap-1 bg-green-500/20 text-green-700 rounded px-1.5 py-1">
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={parentIssue.name}>
|
<Tooltip tooltipHeading="Title" tooltipContent={parentIssue.name} isMobile={isMobile}>
|
||||||
<Link
|
<Link
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`}
|
href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -86,7 +87,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<Tooltip tooltipContent="Remove" position="bottom">
|
<Tooltip tooltipContent="Remove" position="bottom" isMobile={isMobile}>
|
||||||
<span
|
<span
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { FC, useMemo } from "react";
|
import { FC, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail, useMember } from "hooks/store";
|
||||||
// ui
|
// helper
|
||||||
|
import { formatTextList } from "helpers/issue.helper";
|
||||||
// types
|
// types
|
||||||
import { IUser } from "@plane/types";
|
import { IUser } from "@plane/types";
|
||||||
import { ReactionSelector } from "./reaction-selector";
|
import { ReactionSelector } from "./reaction-selector";
|
||||||
@ -21,10 +22,11 @@ export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props)
|
|||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
commentReaction: { getCommentReactionsByCommentId, commentReactionsByUser },
|
commentReaction: { getCommentReactionsByCommentId, commentReactionsByUser, getCommentReactionById },
|
||||||
createCommentReaction,
|
createCommentReaction,
|
||||||
removeCommentReaction,
|
removeCommentReaction,
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
|
|
||||||
const reactionIds = getCommentReactionsByCommentId(commentId);
|
const reactionIds = getCommentReactionsByCommentId(commentId);
|
||||||
const userReactions = commentReactionsByUser(commentId, currentUser.id).map((r) => r.reaction);
|
const userReactions = commentReactionsByUser(commentId, currentUser.id).map((r) => r.reaction);
|
||||||
@ -73,6 +75,17 @@ export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props)
|
|||||||
[workspaceSlug, projectId, commentId, currentUser, createCommentReaction, removeCommentReaction, userReactions]
|
[workspaceSlug, projectId, commentId, currentUser, createCommentReaction, removeCommentReaction, userReactions]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getReactionUsers = (reaction: string): string => {
|
||||||
|
const reactionUsers = (reactionIds?.[reaction] || [])
|
||||||
|
.map((reactionId) => {
|
||||||
|
const reactionDetails = getCommentReactionById(reactionId);
|
||||||
|
return reactionDetails ? getUserDetails(reactionDetails.actor)?.display_name : null;
|
||||||
|
})
|
||||||
|
.filter((displayName): displayName is string => !!displayName);
|
||||||
|
const formattedUsers = formatTextList(reactionUsers);
|
||||||
|
return formattedUsers;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 relative flex items-center gap-1.5">
|
<div className="mt-4 relative flex items-center gap-1.5">
|
||||||
<ReactionSelector
|
<ReactionSelector
|
||||||
@ -87,19 +100,21 @@ export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props)
|
|||||||
(reaction) =>
|
(reaction) =>
|
||||||
reactionIds[reaction]?.length > 0 && (
|
reactionIds[reaction]?.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<Tooltip tooltipContent={getReactionUsers(reaction)}>
|
||||||
type="button"
|
<button
|
||||||
onClick={() => issueCommentReactionOperations.react(reaction)}
|
type="button"
|
||||||
key={reaction}
|
onClick={() => issueCommentReactionOperations.react(reaction)}
|
||||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
key={reaction}
|
||||||
userReactions.includes(reaction) ? "bg-custom-primary-100/10" : "bg-custom-background-80"
|
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||||
}`}
|
userReactions.includes(reaction) ? "bg-custom-primary-100/10" : "bg-custom-background-80"
|
||||||
>
|
}`}
|
||||||
<span>{renderEmoji(reaction)}</span>
|
>
|
||||||
<span className={userReactions.includes(reaction) ? "text-custom-primary-100" : ""}>
|
<span>{renderEmoji(reaction)}</span>
|
||||||
{(reactionIds || {})[reaction].length}{" "}
|
<span className={userReactions.includes(reaction) ? "text-custom-primary-100" : ""}>
|
||||||
</span>
|
{(reactionIds || {})[reaction].length}{" "}
|
||||||
</button>
|
</span>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { FC, useMemo } from "react";
|
import { FC, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// hooks
|
||||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
import { useIssueDetail, useMember } from "hooks/store";
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
|
||||||
import { useIssueDetail } from "hooks/store";
|
|
||||||
// ui
|
// ui
|
||||||
|
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
|
import { formatTextList } from "helpers/issue.helper";
|
||||||
// types
|
// types
|
||||||
import { IUser } from "@plane/types";
|
import { IUser } from "@plane/types";
|
||||||
import { ReactionSelector } from "./reaction-selector";
|
import { ReactionSelector } from "./reaction-selector";
|
||||||
@ -20,10 +22,11 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
|
|||||||
const { workspaceSlug, projectId, issueId, currentUser } = props;
|
const { workspaceSlug, projectId, issueId, currentUser } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
reaction: { getReactionsByIssueId, reactionsByUser },
|
reaction: { getReactionsByIssueId, reactionsByUser, getReactionById },
|
||||||
createReaction,
|
createReaction,
|
||||||
removeReaction,
|
removeReaction,
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
|
|
||||||
const reactionIds = getReactionsByIssueId(issueId);
|
const reactionIds = getReactionsByIssueId(issueId);
|
||||||
const userReactions = reactionsByUser(issueId, currentUser.id).map((r) => r.reaction);
|
const userReactions = reactionsByUser(issueId, currentUser.id).map((r) => r.reaction);
|
||||||
@ -72,6 +75,18 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
|
|||||||
[workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, userReactions]
|
[workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, userReactions]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getReactionUsers = (reaction: string): string => {
|
||||||
|
const reactionUsers = (reactionIds?.[reaction] || [])
|
||||||
|
.map((reactionId) => {
|
||||||
|
const reactionDetails = getReactionById(reactionId);
|
||||||
|
return reactionDetails ? getUserDetails(reactionDetails.actor_id)?.display_name : null;
|
||||||
|
})
|
||||||
|
.filter((displayName): displayName is string => !!displayName);
|
||||||
|
|
||||||
|
const formattedUsers = formatTextList(reactionUsers);
|
||||||
|
return formattedUsers;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 relative flex items-center gap-1.5">
|
<div className="mt-4 relative flex items-center gap-1.5">
|
||||||
<ReactionSelector size="md" position="top" value={userReactions} onSelect={issueReactionOperations.react} />
|
<ReactionSelector size="md" position="top" value={userReactions} onSelect={issueReactionOperations.react} />
|
||||||
@ -81,19 +96,21 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
|
|||||||
(reaction) =>
|
(reaction) =>
|
||||||
reactionIds[reaction]?.length > 0 && (
|
reactionIds[reaction]?.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<Tooltip tooltipContent={getReactionUsers(reaction)}>
|
||||||
type="button"
|
<button
|
||||||
onClick={() => issueReactionOperations.react(reaction)}
|
type="button"
|
||||||
key={reaction}
|
onClick={() => issueReactionOperations.react(reaction)}
|
||||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
key={reaction}
|
||||||
userReactions.includes(reaction) ? "bg-custom-primary-100/10" : "bg-custom-background-80"
|
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||||
}`}
|
userReactions.includes(reaction) ? "bg-custom-primary-100/10" : "bg-custom-background-80"
|
||||||
>
|
}`}
|
||||||
<span>{renderEmoji(reaction)}</span>
|
>
|
||||||
<span className={userReactions.includes(reaction) ? "text-custom-primary-100" : ""}>
|
<span>{renderEmoji(reaction)}</span>
|
||||||
{(reactionIds || {})[reaction].length}{" "}
|
<span className={userReactions.includes(reaction) ? "text-custom-primary-100" : ""}>
|
||||||
</span>
|
{(reactionIds || {})[reaction].length}{" "}
|
||||||
</button>
|
</span>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
@ -7,6 +7,7 @@ import { RelatedIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
|||||||
import { ExistingIssuesListModal } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
import { useIssueDetail, useIssues, useProject } from "hooks/store";
|
import { useIssueDetail, useIssues, useProject } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
// ui
|
// ui
|
||||||
// helpers
|
// helpers
|
||||||
@ -59,7 +60,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
|||||||
toggleRelationModal,
|
toggleRelationModal,
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const { issueMap } = useIssues();
|
const { issueMap } = useIssues();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const relationIssueIds = getRelationByIssueIdRelationType(issueId, relationKey);
|
const relationIssueIds = getRelationByIssueIdRelationType(issueId, relationKey);
|
||||||
|
|
||||||
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
||||||
@ -124,7 +125,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
|||||||
key={relationIssueId}
|
key={relationIssueId}
|
||||||
className={`group flex items-center gap-1 rounded px-1.5 pb-1 pt-1 leading-3 hover:bg-custom-background-90 ${issueRelationObject[relationKey].className}`}
|
className={`group flex items-center gap-1 rounded px-1.5 pb-1 pt-1 leading-3 hover:bg-custom-background-90 ${issueRelationObject[relationKey].className}`}
|
||||||
>
|
>
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={currentIssue.name}>
|
<Tooltip tooltipHeading="Title" tooltipContent={currentIssue.name} isMobile={isMobile}>
|
||||||
<Link
|
<Link
|
||||||
href={`/${workspaceSlug}/projects/${projectDetails?.id}/issues/${currentIssue.id}`}
|
href={`/${workspaceSlug}/projects/${projectDetails?.id}/issues/${currentIssue.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -136,7 +137,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
|||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<Tooltip tooltipContent="Remove" position="bottom">
|
<Tooltip tooltipContent="Remove" position="bottom" isMobile={isMobile}>
|
||||||
<span
|
<span
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -376,7 +376,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="fixed right-0 z-[5] h-full w-full min-w-[300px] space-y-5 overflow-hidden border-l border-custom-border-200 bg-custom-sidebar-background-100 py-5 sm:w-1/2 md:relative md:w-1/3 lg:min-w-80 xl:min-w-96"
|
className="fixed right-0 z-[5] h-full w-full min-w-[300px] overflow-hidden border-l border-custom-border-200 bg-custom-sidebar-background-100 py-5 sm:w-1/2 md:relative md:w-1/3 lg:min-w-80 xl:min-w-96"
|
||||||
style={themeStore.issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
|
style={themeStore.issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
|
||||||
>
|
>
|
||||||
<IssueDetailsSidebar
|
<IssueDetailsSidebar
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
CalendarCheck2,
|
CalendarCheck2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
@ -78,7 +79,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const { getStateById } = useProjectState();
|
const { getStateById } = useProjectState();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
if (!issue) return <></>;
|
if (!issue) return <></>;
|
||||||
|
|
||||||
@ -138,7 +139,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-wrap items-center gap-2.5 text-custom-text-300">
|
<div className="flex flex-wrap items-center gap-2.5 text-custom-text-300">
|
||||||
<Tooltip tooltipContent="Copy link">
|
<Tooltip tooltipContent="Copy link" isMobile={isMobile}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-5 w-5 place-items-center rounded hover:text-custom-text-200 focus:outline-none focus:ring-2 focus:ring-custom-primary"
|
className="grid h-5 w-5 place-items-center rounded hover:text-custom-text-200 focus:outline-none focus:ring-2 focus:ring-custom-primary"
|
||||||
@ -149,6 +150,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{isArchivingAllowed && (
|
{isArchivingAllowed && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"}
|
tooltipContent={isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -170,7 +172,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{is_editable && (
|
{is_editable && (
|
||||||
<Tooltip tooltipContent="Delete">
|
<Tooltip tooltipContent="Delete" isMobile={isMobile}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-5 w-5 place-items-center rounded hover:text-custom-text-200 focus:outline-none focus:ring-2 focus:ring-custom-primary"
|
className="grid h-5 w-5 place-items-center rounded hover:text-custom-text-200 focus:outline-none focus:ring-2 focus:ring-custom-primary"
|
||||||
|
@ -8,6 +8,7 @@ import { Tooltip, ControlLink } from "@plane/ui";
|
|||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
|
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// helpers
|
// helpers
|
||||||
// types
|
// types
|
||||||
import { TIssue, TIssueMap } from "@plane/types";
|
import { TIssue, TIssueMap } from "@plane/types";
|
||||||
@ -29,6 +30,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
|||||||
const { getProjectIdentifierById } = useProject();
|
const { getProjectIdentifierById } = useProject();
|
||||||
const { getProjectStates } = useProjectState();
|
const { getProjectStates } = useProjectState();
|
||||||
const { peekIssue, setPeekIssue } = useIssueDetail();
|
const { peekIssue, setPeekIssue } = useIssueDetail();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
// states
|
// states
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
|
|
||||||
@ -110,7 +112,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
|||||||
<div className="flex-shrink-0 text-xs text-custom-text-300">
|
<div className="flex-shrink-0 text-xs text-custom-text-300">
|
||||||
{getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
|
{getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
<Tooltip tooltipContent={issue.name}>
|
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
||||||
<div className="truncate text-xs">{issue.name}</div>
|
<div className="truncate text-xs">{issue.name}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
// icons
|
// icons
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
|
||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||||
// constants
|
// constants
|
||||||
@ -18,7 +18,7 @@ export const AppliedDateFilters: React.FC<Props> = observer((props) => {
|
|||||||
const getDateLabel = (value: string): string => {
|
const getDateLabel = (value: string): string => {
|
||||||
let dateLabel = "";
|
let dateLabel = "";
|
||||||
|
|
||||||
const dateDetails = DATE_FILTER_OPTIONS.find((d) => d.value === value);
|
const dateDetails = DATE_AFTER_FILTER_OPTIONS.find((d) => d.value === value);
|
||||||
|
|
||||||
if (dateDetails) dateLabel = dateDetails.name;
|
if (dateDetails) dateLabel = dateDetails.name;
|
||||||
else {
|
else {
|
||||||
|
@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { DateFilterModal } from "components/core";
|
import { DateFilterModal } from "components/core";
|
||||||
import { FilterHeader, FilterOption } from "components/issues";
|
import { FilterHeader, FilterOption } from "components/issues";
|
||||||
// constants
|
// constants
|
||||||
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appliedFilters: string[] | null;
|
appliedFilters: string[] | null;
|
||||||
@ -21,7 +21,9 @@ export const FilterStartDate: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) =>
|
||||||
|
d.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { DateFilterModal } from "components/core";
|
import { DateFilterModal } from "components/core";
|
||||||
import { FilterHeader, FilterOption } from "components/issues";
|
import { FilterHeader, FilterOption } from "components/issues";
|
||||||
// constants
|
// constants
|
||||||
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appliedFilters: string[] | null;
|
appliedFilters: string[] | null;
|
||||||
@ -21,7 +21,9 @@ export const FilterTargetDate: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) =>
|
||||||
|
d.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -5,7 +5,8 @@ import { Tooltip } from "@plane/ui";
|
|||||||
// types
|
// types
|
||||||
import { ISSUE_LAYOUTS } from "constants/issue";
|
import { ISSUE_LAYOUTS } from "constants/issue";
|
||||||
import { TIssueLayouts } from "@plane/types";
|
import { TIssueLayouts } from "@plane/types";
|
||||||
// constants
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layouts: TIssueLayouts[];
|
layouts: TIssueLayouts[];
|
||||||
@ -15,11 +16,12 @@ type Props = {
|
|||||||
|
|
||||||
export const LayoutSelection: React.FC<Props> = (props) => {
|
export const LayoutSelection: React.FC<Props> = (props) => {
|
||||||
const { layouts, onChange, selectedLayout } = props;
|
const { layouts, onChange, selectedLayout } = props;
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
|
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
|
||||||
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => (
|
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => (
|
||||||
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
<Tooltip key={layout.key} tooltipContent={layout.title} isMobile={isMobile}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
|
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -31,6 +32,7 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
|
|||||||
issueDetails &&
|
issueDetails &&
|
||||||
!issueDetails.tempId &&
|
!issueDetails.tempId &&
|
||||||
setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id });
|
setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id });
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -42,6 +44,7 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h5>{issueDetails?.name}</h5>
|
<h5>{issueDetails?.name}</h5>
|
||||||
@ -83,6 +86,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
workspaceSlug &&
|
workspaceSlug &&
|
||||||
issueDetails &&
|
issueDetails &&
|
||||||
setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id });
|
setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id });
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlLink
|
<ControlLink
|
||||||
@ -97,7 +101,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
<div className="flex-shrink-0 text-xs text-custom-text-300">
|
<div className="flex-shrink-0 text-xs text-custom-text-300">
|
||||||
{projectIdentifier} {issueDetails?.sequence_id}
|
{projectIdentifier} {issueDetails?.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
<Tooltip tooltipContent={issueDetails?.name}>
|
<Tooltip tooltipContent={issueDetails?.name} isMobile={isMobile}>
|
||||||
<span className="flex-grow truncate text-sm font-medium">{issueDetails?.name}</span>
|
<span className="flex-grow truncate text-sm font-medium">{issueDetails?.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,7 @@ import { Tooltip, ControlLink } from "@plane/ui";
|
|||||||
import RenderIfVisible from "components/core/render-if-visible-HOC";
|
import RenderIfVisible from "components/core/render-if-visible-HOC";
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
import { useApplication, useIssueDetail, useProject } from "hooks/store";
|
import { useApplication, useIssueDetail, useProject } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
||||||
import { IssueProperties } from "../properties/all-properties";
|
import { IssueProperties } from "../properties/all-properties";
|
||||||
@ -41,6 +42,7 @@ interface IssueDetailsBlockProps {
|
|||||||
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props: IssueDetailsBlockProps) => {
|
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props: IssueDetailsBlockProps) => {
|
||||||
const { issue, updateIssue, quickActions, isReadOnly, displayProperties } = props;
|
const { issue, updateIssue, quickActions, isReadOnly, displayProperties } = props;
|
||||||
// hooks
|
// hooks
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const { getProjectIdentifierById } = useProject();
|
const { getProjectIdentifierById } = useProject();
|
||||||
const {
|
const {
|
||||||
router: { workspaceSlug },
|
router: { workspaceSlug },
|
||||||
@ -66,7 +68,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
|
|||||||
</WithDisplayPropertiesHOC>
|
</WithDisplayPropertiesHOC>
|
||||||
|
|
||||||
{issue?.is_draft ? (
|
{issue?.is_draft ? (
|
||||||
<Tooltip tooltipContent={issue.name}>
|
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
||||||
<span>{issue.name}</span>
|
<span>{issue.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
@ -79,7 +81,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
|
|||||||
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
||||||
disabled={!!issue?.tempId}
|
disabled={!!issue?.tempId}
|
||||||
>
|
>
|
||||||
<Tooltip tooltipContent={issue.name}>
|
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
||||||
<span>{issue.name}</span>
|
<span>{issue.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ControlLink>
|
</ControlLink>
|
||||||
|
@ -58,6 +58,7 @@ export interface IGroupByKanBan {
|
|||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
isDragStarted?: boolean;
|
isDragStarted?: boolean;
|
||||||
showEmptyGroup?: boolean;
|
showEmptyGroup?: boolean;
|
||||||
|
subGroupIssueHeaderCount?: (listId: string) => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||||
@ -83,6 +84,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
isDragStarted,
|
isDragStarted,
|
||||||
showEmptyGroup = true,
|
showEmptyGroup = true,
|
||||||
|
subGroupIssueHeaderCount,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const member = useMember();
|
const member = useMember();
|
||||||
@ -105,44 +107,57 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
|
|
||||||
if (!list) return null;
|
if (!list) return null;
|
||||||
|
|
||||||
const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0);
|
const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
|
||||||
|
|
||||||
const groupList = showEmptyGroup ? list : groupWithIssues;
|
|
||||||
|
|
||||||
const visibilityGroupBy = (_list: IGroupByColumn) => {
|
|
||||||
if (sub_group_by) {
|
if (sub_group_by) {
|
||||||
if (kanbanFilters?.sub_group_by.includes(_list.id)) return true;
|
const groupVisibility = {
|
||||||
return false;
|
showGroup: true,
|
||||||
|
showIssues: true,
|
||||||
|
};
|
||||||
|
if (!showEmptyGroup) {
|
||||||
|
groupVisibility.showGroup = subGroupIssueHeaderCount ? subGroupIssueHeaderCount(_list.id) > 0 : true;
|
||||||
|
}
|
||||||
|
return groupVisibility;
|
||||||
} else {
|
} else {
|
||||||
if (kanbanFilters?.group_by.includes(_list.id)) return true;
|
const groupVisibility = {
|
||||||
return false;
|
showGroup: true,
|
||||||
|
showIssues: true,
|
||||||
|
};
|
||||||
|
if (!showEmptyGroup) {
|
||||||
|
if ((issueIds as TGroupedIssues)?.[_list.id]?.length > 0) groupVisibility.showGroup = true;
|
||||||
|
else groupVisibility.showGroup = false;
|
||||||
|
}
|
||||||
|
if (kanbanFilters?.group_by.includes(_list.id)) groupVisibility.showIssues = false;
|
||||||
|
return groupVisibility;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isGroupByCreatedBy = group_by === "created_by";
|
const isGroupByCreatedBy = group_by === "created_by";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative flex w-full gap-2 ${sub_group_by ? "h-full" : "h-full"}`}>
|
<div className={`relative w-full flex gap-2 ${sub_group_by ? "h-full" : "h-full"}`}>
|
||||||
{groupList &&
|
{list &&
|
||||||
groupList.length > 0 &&
|
list.length > 0 &&
|
||||||
groupList.map((_list: IGroupByColumn) => {
|
list.map((subList: IGroupByColumn) => {
|
||||||
const groupByVisibilityToggle = visibilityGroupBy(_list);
|
const groupByVisibilityToggle = visibilityGroupBy(subList);
|
||||||
|
|
||||||
|
if (groupByVisibilityToggle.showGroup === false) return <></>;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={_list.id}
|
key={subList.id}
|
||||||
className={`group relative flex flex-shrink-0 flex-col ${groupByVisibilityToggle ? `` : `w-[350px]`}`}
|
className={`relative flex flex-shrink-0 flex-col group ${
|
||||||
|
groupByVisibilityToggle.showIssues ? `w-[350px]` : ``
|
||||||
|
} `}
|
||||||
>
|
>
|
||||||
{sub_group_by === null && (
|
{sub_group_by === null && (
|
||||||
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
|
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
|
||||||
<HeaderGroupByCard
|
<HeaderGroupByCard
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
column_id={_list.id}
|
column_id={subList.id}
|
||||||
icon={_list.icon}
|
icon={subList.icon}
|
||||||
title={_list.name}
|
title={subList.name}
|
||||||
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
|
count={(issueIds as TGroupedIssues)?.[subList.id]?.length || 0}
|
||||||
issuePayload={_list.payload}
|
issuePayload={subList.payload}
|
||||||
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
|
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
|
||||||
storeType={storeType}
|
storeType={storeType}
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
@ -152,9 +167,9 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!groupByVisibilityToggle && (
|
{groupByVisibilityToggle.showIssues && (
|
||||||
<KanbanGroup
|
<KanbanGroup
|
||||||
groupId={_list.id}
|
groupId={subList.id}
|
||||||
issuesMap={issuesMap}
|
issuesMap={issuesMap}
|
||||||
issueIds={issueIds}
|
issueIds={issueIds}
|
||||||
peekIssueId={peekIssue?.issueId ?? ""}
|
peekIssueId={peekIssue?.issueId ?? ""}
|
||||||
@ -170,7 +185,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
disableIssueCreation={disableIssueCreation}
|
disableIssueCreation={disableIssueCreation}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
groupByVisibilityToggle={groupByVisibilityToggle}
|
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
isDragStarted={isDragStarted}
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
@ -208,6 +222,7 @@ export interface IKanBan {
|
|||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
isDragStarted?: boolean;
|
isDragStarted?: boolean;
|
||||||
|
subGroupIssueHeaderCount?: (listId: string) => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanBan: React.FC<IKanBan> = observer((props) => {
|
export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||||
@ -232,6 +247,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
isDragStarted,
|
isDragStarted,
|
||||||
showEmptyGroup,
|
showEmptyGroup,
|
||||||
|
subGroupIssueHeaderCount,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const issueKanBanView = useKanbanView();
|
const issueKanBanView = useKanbanView();
|
||||||
@ -259,6 +275,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
isDragStarted={isDragStarted}
|
isDragStarted={isDragStarted}
|
||||||
showEmptyGroup={showEmptyGroup}
|
showEmptyGroup={showEmptyGroup}
|
||||||
|
subGroupIssueHeaderCount={subGroupIssueHeaderCount}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -36,7 +36,7 @@ interface IKanbanGroup {
|
|||||||
viewId?: string;
|
viewId?: string;
|
||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
groupByVisibilityToggle: boolean;
|
groupByVisibilityToggle?: boolean;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
isDragStarted?: boolean;
|
isDragStarted?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ interface ISubGroupSwimlaneHeader {
|
|||||||
kanbanFilters: TIssueKanbanFilters;
|
kanbanFilters: TIssueKanbanFilters;
|
||||||
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
||||||
storeType: KanbanStoreType;
|
storeType: KanbanStoreType;
|
||||||
|
showEmptyGroup: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
|
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
|
||||||
@ -39,6 +40,22 @@ const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: st
|
|||||||
return headerCount;
|
return headerCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const visibilitySubGroupByGroupCount = (
|
||||||
|
issueIds: TSubGroupedIssues,
|
||||||
|
_list: IGroupByColumn,
|
||||||
|
showEmptyGroup: boolean
|
||||||
|
): boolean => {
|
||||||
|
let subGroupHeaderVisibility = true;
|
||||||
|
|
||||||
|
if (showEmptyGroup) subGroupHeaderVisibility = true;
|
||||||
|
else {
|
||||||
|
if (getSubGroupHeaderIssuesCount(issueIds, _list.id) > 0) subGroupHeaderVisibility = true;
|
||||||
|
else subGroupHeaderVisibility = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return subGroupHeaderVisibility;
|
||||||
|
};
|
||||||
|
|
||||||
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||||
issueIds,
|
issueIds,
|
||||||
sub_group_by,
|
sub_group_by,
|
||||||
@ -47,26 +64,37 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
|||||||
list,
|
list,
|
||||||
kanbanFilters,
|
kanbanFilters,
|
||||||
handleKanbanFilters,
|
handleKanbanFilters,
|
||||||
|
showEmptyGroup,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="relative flex h-max min-h-full w-full items-center gap-2">
|
<div className="relative flex h-max min-h-full w-full items-center gap-2">
|
||||||
{list &&
|
{list &&
|
||||||
list.length > 0 &&
|
list.length > 0 &&
|
||||||
list.map((_list: IGroupByColumn) => (
|
list.map((_list: IGroupByColumn) => {
|
||||||
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
|
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(
|
||||||
<HeaderGroupByCard
|
issueIds as TSubGroupedIssues,
|
||||||
sub_group_by={sub_group_by}
|
_list,
|
||||||
group_by={group_by}
|
showEmptyGroup
|
||||||
column_id={_list.id}
|
);
|
||||||
icon={_list.icon}
|
|
||||||
title={_list.name}
|
if (subGroupByVisibilityToggle === false) return <></>;
|
||||||
count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)}
|
|
||||||
kanbanFilters={kanbanFilters}
|
return (
|
||||||
handleKanbanFilters={handleKanbanFilters}
|
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
|
||||||
issuePayload={_list.payload}
|
<HeaderGroupByCard
|
||||||
storeType={storeType}
|
sub_group_by={sub_group_by}
|
||||||
/>
|
group_by={group_by}
|
||||||
</div>
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -127,53 +155,74 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
return issueCount;
|
return issueCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const visibilitySubGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
|
||||||
|
const subGroupVisibility = {
|
||||||
|
showGroup: true,
|
||||||
|
showIssues: true,
|
||||||
|
};
|
||||||
|
if (showEmptyGroup) subGroupVisibility.showGroup = true;
|
||||||
|
else {
|
||||||
|
if (calculateIssueCount(_list.id) > 0) subGroupVisibility.showGroup = true;
|
||||||
|
else subGroupVisibility.showGroup = false;
|
||||||
|
}
|
||||||
|
if (kanbanFilters?.sub_group_by.includes(_list.id)) subGroupVisibility.showIssues = false;
|
||||||
|
return subGroupVisibility;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-max min-h-full w-full">
|
<div className="relative h-max min-h-full w-full">
|
||||||
{list &&
|
{list &&
|
||||||
list.length > 0 &&
|
list.length > 0 &&
|
||||||
list.map((_list: any) => (
|
list.map((_list: any) => {
|
||||||
<div key={_list.id} className="flex flex-shrink-0 flex-col">
|
const subGroupByVisibilityToggle = visibilitySubGroupBy(_list);
|
||||||
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
|
if (subGroupByVisibilityToggle.showGroup === false) return <></>;
|
||||||
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
|
return (
|
||||||
<HeaderSubGroupByCard
|
<div key={_list.id} className="flex flex-shrink-0 flex-col">
|
||||||
column_id={_list.id}
|
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
|
||||||
icon={_list.Icon}
|
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
|
||||||
title={_list.name || ""}
|
<HeaderSubGroupByCard
|
||||||
count={calculateIssueCount(_list.id)}
|
column_id={_list.id}
|
||||||
kanbanFilters={kanbanFilters}
|
icon={_list.Icon}
|
||||||
handleKanbanFilters={handleKanbanFilters}
|
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>
|
||||||
<div className="w-full border-b border-dashed border-custom-border-400" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!kanbanFilters?.sub_group_by.includes(_list.id) && (
|
{subGroupByVisibilityToggle.showIssues && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<KanBan
|
<KanBan
|
||||||
issuesMap={issuesMap}
|
issuesMap={issuesMap}
|
||||||
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
|
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
|
||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
sub_group_id={_list.id}
|
sub_group_id={_list.id}
|
||||||
updateIssue={updateIssue}
|
storeType={storeType}
|
||||||
quickActions={quickActions}
|
updateIssue={updateIssue}
|
||||||
kanbanFilters={kanbanFilters}
|
quickActions={quickActions}
|
||||||
handleKanbanFilters={handleKanbanFilters}
|
kanbanFilters={kanbanFilters}
|
||||||
showEmptyGroup={showEmptyGroup}
|
handleKanbanFilters={handleKanbanFilters}
|
||||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
showEmptyGroup={showEmptyGroup}
|
||||||
canEditProperties={canEditProperties}
|
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||||
addIssuesToView={addIssuesToView}
|
canEditProperties={canEditProperties}
|
||||||
quickAddCallback={quickAddCallback}
|
addIssuesToView={addIssuesToView}
|
||||||
viewId={viewId}
|
quickAddCallback={quickAddCallback}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
viewId={viewId}
|
||||||
isDragStarted={isDragStarted}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
storeType={storeType}
|
isDragStarted={isDragStarted}
|
||||||
/>
|
subGroupIssueHeaderCount={(groupByListId: string) =>
|
||||||
</div>
|
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
|
||||||
)}
|
}
|
||||||
</div>
|
/>
|
||||||
))}
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -267,6 +316,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
handleKanbanFilters={handleKanbanFilters}
|
handleKanbanFilters={handleKanbanFilters}
|
||||||
list={groupByList}
|
list={groupByList}
|
||||||
storeType={storeType}
|
storeType={storeType}
|
||||||
|
showEmptyGroup={showEmptyGroup}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
|
import { useApplication, useIssueDetail, useProject } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner, Tooltip, ControlLink } from "@plane/ui";
|
import { Spinner, Tooltip, ControlLink } from "@plane/ui";
|
||||||
// helper
|
// helper
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
import { useApplication, useIssueDetail, useProject } from "hooks/store";
|
|
||||||
// types
|
// types
|
||||||
import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types";
|
import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types";
|
||||||
import { IssueProperties } from "../properties/all-properties";
|
import { IssueProperties } from "../properties/all-properties";
|
||||||
@ -36,7 +37,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
|||||||
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
|
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
|
||||||
|
|
||||||
const issue = issuesMap[issueId];
|
const issue = issuesMap[issueId];
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
if (!issue) return null;
|
if (!issue) return null;
|
||||||
|
|
||||||
const canEditIssueProperties = canEditProperties(issue.project_id);
|
const canEditIssueProperties = canEditProperties(issue.project_id);
|
||||||
@ -65,7 +66,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{issue?.is_draft ? (
|
{issue?.is_draft ? (
|
||||||
<Tooltip tooltipContent={issue.name}>
|
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
||||||
<span>{issue.name}</span>
|
<span>{issue.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
@ -78,7 +79,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
|||||||
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
||||||
disabled={!!issue?.tempId}
|
disabled={!!issue?.tempId}
|
||||||
>
|
>
|
||||||
<Tooltip tooltipContent={issue.name}>
|
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
||||||
<span>{issue.name}</span>
|
<span>{issue.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ControlLink>
|
</ControlLink>
|
||||||
|
@ -20,6 +20,7 @@ import { cn } from "helpers/common.helper";
|
|||||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||||
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
|
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
|
||||||
import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store";
|
import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types";
|
import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types";
|
||||||
import { IssuePropertyLabels } from "../properties/labels";
|
import { IssuePropertyLabels } from "../properties/labels";
|
||||||
@ -50,6 +51,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
} = useIssues(EIssuesStoreType.CYCLE);
|
} = useIssues(EIssuesStoreType.CYCLE);
|
||||||
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
||||||
const { getStateById } = useProjectState();
|
const { getStateById } = useProjectState();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
@ -391,7 +393,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
displayPropertyKey="sub_issue_count"
|
displayPropertyKey="sub_issue_count"
|
||||||
shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!issue.sub_issues_count}
|
shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!issue.sub_issues_count}
|
||||||
>
|
>
|
||||||
<Tooltip tooltipHeading="Sub-issues" tooltipContent={`${issue.sub_issues_count}`}>
|
<Tooltip tooltipHeading="Sub-issues" tooltipContent={`${issue.sub_issues_count}`} isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
onClick={issue.sub_issues_count ? redirectToIssueDetail : () => {}}
|
onClick={issue.sub_issues_count ? redirectToIssueDetail : () => {}}
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -413,7 +415,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
displayPropertyKey="attachment_count"
|
displayPropertyKey="attachment_count"
|
||||||
shouldRenderProperty={(properties) => !!properties.attachment_count && !!issue.attachment_count}
|
shouldRenderProperty={(properties) => !!properties.attachment_count && !!issue.attachment_count}
|
||||||
>
|
>
|
||||||
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
|
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`} isMobile={isMobile}>
|
||||||
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1">
|
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1">
|
||||||
<Paperclip className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
<Paperclip className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
||||||
<div className="text-xs">{issue.attachment_count}</div>
|
<div className="text-xs">{issue.attachment_count}</div>
|
||||||
@ -427,7 +429,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
displayPropertyKey="link"
|
displayPropertyKey="link"
|
||||||
shouldRenderProperty={(properties) => !!properties.link && !!issue.link_count}
|
shouldRenderProperty={(properties) => !!properties.link && !!issue.link_count}
|
||||||
>
|
>
|
||||||
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
|
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`} isMobile={isMobile}>
|
||||||
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1">
|
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1">
|
||||||
<Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
<Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
||||||
<div className="text-xs">{issue.link_count}</div>
|
<div className="text-xs">{issue.link_count}</div>
|
||||||
|
@ -9,6 +9,7 @@ import { Tooltip } from "@plane/ui";
|
|||||||
import { useApplication, useLabel } from "hooks/store";
|
import { useApplication, useLabel } from "hooks/store";
|
||||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
// types
|
// types
|
||||||
import { IIssueLabel } from "@plane/types";
|
import { IIssueLabel } from "@plane/types";
|
||||||
@ -62,7 +63,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
router: { workspaceSlug },
|
router: { workspaceSlug },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { fetchProjectLabels, getProjectLabels } = useLabel();
|
const { fetchProjectLabels, getProjectLabels } = useLabel();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const storeLabels = getProjectLabels(projectId);
|
const storeLabels = getProjectLabels(projectId);
|
||||||
|
|
||||||
const onOpen = () => {
|
const onOpen = () => {
|
||||||
@ -149,7 +150,13 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
{projectLabels
|
{projectLabels
|
||||||
?.filter((l) => value.includes(l?.id))
|
?.filter((l) => value.includes(l?.id))
|
||||||
.map((label) => (
|
.map((label) => (
|
||||||
<Tooltip key={label.id} position="top" tooltipHeading="Labels" tooltipContent={label?.name ?? ""}>
|
<Tooltip
|
||||||
|
key={label.id}
|
||||||
|
position="top"
|
||||||
|
tooltipHeading="Labels"
|
||||||
|
tooltipContent={label?.name ?? ""}
|
||||||
|
isMobile={isMobile}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
key={label?.id}
|
key={label?.id}
|
||||||
className={`flex overflow-hidden hover:bg-custom-background-80 ${
|
className={`flex overflow-hidden hover:bg-custom-background-80 ${
|
||||||
@ -176,6 +183,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
position="top"
|
position="top"
|
||||||
tooltipHeading="Labels"
|
tooltipHeading="Labels"
|
||||||
tooltipContent={projectLabels
|
tooltipContent={projectLabels
|
||||||
@ -191,7 +199,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Tooltip position="top" tooltipHeading="Labels" tooltipContent="None">
|
<Tooltip position="top" tooltipHeading="Labels" tooltipContent="None" isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
className={`flex h-full items-center justify-center gap-2 rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${
|
className={`flex h-full items-center justify-center gap-2 rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${
|
||||||
noLabelBorder ? "" : "border-[0.5px] border-custom-border-300"
|
noLabelBorder ? "" : "border-[0.5px] border-custom-border-300"
|
||||||
@ -224,8 +232,8 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
disabled
|
disabled
|
||||||
? "cursor-not-allowed text-custom-text-200"
|
? "cursor-not-allowed text-custom-text-200"
|
||||||
: value.length <= maxRender
|
: value.length <= maxRender
|
||||||
? "cursor-pointer"
|
? "cursor-pointer"
|
||||||
: "cursor-pointer hover:bg-custom-background-80"
|
: "cursor-pointer hover:bg-custom-background-80"
|
||||||
} ${buttonClassName}`}
|
} ${buttonClassName}`}
|
||||||
onClick={handleOnClick}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
|
@ -14,6 +14,7 @@ import { cn } from "helpers/common.helper";
|
|||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail, useProject } from "hooks/store";
|
import { useIssueDetail, useProject } from "hooks/store";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayProperties, TIssue } from "@plane/types";
|
import { IIssueDisplayProperties, TIssue } from "@plane/types";
|
||||||
// local components
|
// local components
|
||||||
@ -144,6 +145,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
|||||||
//hooks
|
//hooks
|
||||||
const { getProjectIdentifierById } = useProject();
|
const { getProjectIdentifierById } = useProject();
|
||||||
const { peekIssue, setPeekIssue } = useIssueDetail();
|
const { peekIssue, setPeekIssue } = useIssueDetail();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
// states
|
// states
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||||
@ -241,7 +243,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
|||||||
disabled={!!issueDetail?.tempId}
|
disabled={!!issueDetail?.tempId}
|
||||||
>
|
>
|
||||||
<div className="w-full overflow-hidden">
|
<div className="w-full overflow-hidden">
|
||||||
<Tooltip tooltipContent={issueDetail.name}>
|
<Tooltip tooltipContent={issueDetail.name} isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
className="h-full w-full cursor-pointer truncate px-4 text-left text-[0.825rem] text-custom-text-100 focus:outline-none"
|
className="h-full w-full cursor-pointer truncate px-4 text-left text-[0.825rem] text-custom-text-100 focus:outline-none"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// components
|
// components
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
type Props = {
|
type Props = {
|
||||||
labelDetails: any[];
|
labelDetails: any[];
|
||||||
maxRender?: number;
|
maxRender?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewIssueLabel: React.FC<Props> = ({ labelDetails, maxRender = 1 }) => (
|
export const ViewIssueLabel: React.FC<Props> = ({ labelDetails, maxRender = 1 }) => {
|
||||||
<>
|
const { isMobile } = usePlatformOS();
|
||||||
|
return (<>
|
||||||
{labelDetails?.length > 0 ? (
|
{labelDetails?.length > 0 ? (
|
||||||
labelDetails.length <= maxRender ? (
|
labelDetails.length <= maxRender ? (
|
||||||
<>
|
<>
|
||||||
@ -17,7 +18,7 @@ export const ViewIssueLabel: React.FC<Props> = ({ labelDetails, maxRender = 1 })
|
|||||||
key={label.id}
|
key={label.id}
|
||||||
className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
||||||
>
|
>
|
||||||
<Tooltip position="top" tooltipHeading="Label" tooltipContent={label.name}>
|
<Tooltip position="top" tooltipHeading="Label" tooltipContent={label.name} isMobile={isMobile}>
|
||||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||||
<span
|
<span
|
||||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||||
@ -33,7 +34,7 @@ export const ViewIssueLabel: React.FC<Props> = ({ labelDetails, maxRender = 1 })
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm">
|
<div className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm">
|
||||||
<Tooltip position="top" tooltipHeading="Labels" tooltipContent={labelDetails.map((l) => l.name).join(", ")}>
|
<Tooltip position="top" tooltipHeading="Labels" tooltipContent={labelDetails.map((l) => l.name).join(", ")} isMobile={isMobile}>
|
||||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||||
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
|
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
|
||||||
{`${labelDetails.length} Labels`}
|
{`${labelDetails.length} Labels`}
|
||||||
@ -44,5 +45,5 @@ export const ViewIssueLabel: React.FC<Props> = ({ labelDetails, maxRender = 1 })
|
|||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
</>
|
</>)
|
||||||
);
|
};
|
||||||
|
@ -8,6 +8,7 @@ import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
|||||||
import useDebounce from "hooks/use-debounce";
|
import useDebounce from "hooks/use-debounce";
|
||||||
import { ProjectService } from "services/project";
|
import { ProjectService } from "services/project";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// ui
|
// ui
|
||||||
// icons
|
// icons
|
||||||
// types
|
// types
|
||||||
@ -37,7 +38,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
|
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
|
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -113,7 +114,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex p-2 sm:justify-end">
|
<div className="flex p-2 sm:justify-end">
|
||||||
<Tooltip tooltipContent="Toggle workspace level search">
|
<Tooltip tooltipContent="Toggle workspace level search" isMobile={isMobile}>
|
||||||
<div
|
<div
|
||||||
className={`flex flex-shrink-0 cursor-pointer items-center gap-1 text-xs ${
|
className={`flex flex-shrink-0 cursor-pointer items-center gap-1 text-xs ${
|
||||||
isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200"
|
isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
|
@ -21,9 +21,8 @@ import { cn } from "helpers/common.helper";
|
|||||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||||
// store hooks
|
// store hooks
|
||||||
import { useIssueDetail, useProjectState, useUser } from "hooks/store";
|
import { useIssueDetail, useProjectState, useUser } from "hooks/store";
|
||||||
// helpers
|
// hooks
|
||||||
// components
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// helpers
|
|
||||||
|
|
||||||
export type TPeekModes = "side-peek" | "modal" | "full-screen";
|
export type TPeekModes = "side-peek" | "modal" | "full-screen";
|
||||||
|
|
||||||
@ -83,6 +82,7 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
|||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const { getStateById } = useProjectState();
|
const { getStateById } = useProjectState();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
// derived values
|
// derived values
|
||||||
const issueDetails = getIssueById(issueId);
|
const issueDetails = getIssueById(issueId);
|
||||||
const stateDetails = issueDetails ? getStateById(issueDetails?.state_id) : undefined;
|
const stateDetails = issueDetails ? getStateById(issueDetails?.state_id) : undefined;
|
||||||
@ -160,13 +160,14 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
|||||||
{currentUser && !isArchived && (
|
{currentUser && !isArchived && (
|
||||||
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||||
)}
|
)}
|
||||||
<Tooltip tooltipContent="Copy link">
|
<Tooltip tooltipContent="Copy link" isMobile={isMobile}>
|
||||||
<button type="button" onClick={handleCopyText}>
|
<button type="button" onClick={handleCopyText}>
|
||||||
<Link2 className="h-4 w-4 -rotate-45 text-custom-text-300 hover:text-custom-text-200" />
|
<Link2 className="h-4 w-4 -rotate-45 text-custom-text-300 hover:text-custom-text-200" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{isArchivingAllowed && (
|
{isArchivingAllowed && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"}
|
tooltipContent={isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -185,14 +186,14 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{isRestoringAllowed && (
|
{isRestoringAllowed && (
|
||||||
<Tooltip tooltipContent="Restore">
|
<Tooltip tooltipContent="Restore" isMobile={isMobile}>
|
||||||
<button type="button" onClick={handleRestoreIssue}>
|
<button type="button" onClick={handleRestoreIssue}>
|
||||||
<RotateCcw className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
<RotateCcw className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<Tooltip tooltipContent="Delete">
|
<Tooltip tooltipContent="Delete" isMobile={isMobile}>
|
||||||
<button type="button" onClick={() => toggleDeleteIssueModal(true)}>
|
<button type="button" onClick={() => toggleDeleteIssueModal(true)}>
|
||||||
<Trash2 className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
<Trash2 className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -93,7 +93,6 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
isOpen={isDeleteIssueModalOpen}
|
isOpen={isDeleteIssueModalOpen}
|
||||||
handleClose={() => {
|
handleClose={() => {
|
||||||
toggleDeleteIssueModal(false);
|
toggleDeleteIssueModal(false);
|
||||||
removeRoutePeekId();
|
|
||||||
}}
|
}}
|
||||||
data={issue}
|
data={issue}
|
||||||
onSubmit={() => issueOperations.remove(workspaceSlug, projectId, issueId)}
|
onSubmit={() => issueOperations.remove(workspaceSlug, projectId, issueId)}
|
||||||
|
@ -4,6 +4,7 @@ import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader }
|
|||||||
// components
|
// components
|
||||||
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
|
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
|
||||||
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
|
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
import { IssueList } from "./issues-list";
|
import { IssueList } from "./issues-list";
|
||||||
import { IssueProperty } from "./properties";
|
import { IssueProperty } from "./properties";
|
||||||
@ -46,7 +47,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
|||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const project = useProject();
|
const project = useProject();
|
||||||
const { getProjectStates } = useProjectState();
|
const { getProjectStates } = useProjectState();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
const projectDetail = (issue && issue.project_id && project.getProjectById(issue.project_id)) || undefined;
|
const projectDetail = (issue && issue.project_id && project.getProjectById(issue.project_id)) || undefined;
|
||||||
const currentIssueStateDetail =
|
const currentIssueStateDetail =
|
||||||
@ -117,7 +118,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
|||||||
onClick={() => handleIssuePeekOverview(issue)}
|
onClick={() => handleIssuePeekOverview(issue)}
|
||||||
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
||||||
>
|
>
|
||||||
<Tooltip tooltipContent={issue.name}>
|
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
||||||
<span>{issue.name}</span>
|
<span>{issue.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ControlLink>
|
</ControlLink>
|
||||||
|
56
web/components/modules/applied-filters/date.tsx
Normal file
56
web/components/modules/applied-filters/date.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// icons
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
// helpers
|
||||||
|
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
|
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||||
|
// constants
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
editable: boolean | undefined;
|
||||||
|
handleRemove: (val: string) => void;
|
||||||
|
values: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppliedDateFilters: React.FC<Props> = observer((props) => {
|
||||||
|
const { editable, handleRemove, values } = props;
|
||||||
|
|
||||||
|
const getDateLabel = (value: string): string => {
|
||||||
|
let dateLabel = "";
|
||||||
|
|
||||||
|
const dateDetails = DATE_AFTER_FILTER_OPTIONS.find((d) => d.value === value);
|
||||||
|
|
||||||
|
if (dateDetails) dateLabel = dateDetails.name;
|
||||||
|
else {
|
||||||
|
const dateParts = value.split(";");
|
||||||
|
|
||||||
|
if (dateParts.length === 2) {
|
||||||
|
const [date, time] = dateParts;
|
||||||
|
|
||||||
|
dateLabel = `${capitalizeFirstLetter(time)} ${renderFormattedDate(date)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dateLabel;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{values.map((date) => (
|
||||||
|
<div key={date} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||||
|
<span className="normal-case">{getDateLabel(date)}</span>
|
||||||
|
{editable && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
|
onClick={() => handleRemove(date)}
|
||||||
|
>
|
||||||
|
<X size={10} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
4
web/components/modules/applied-filters/index.ts
Normal file
4
web/components/modules/applied-filters/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./date";
|
||||||
|
export * from "./members";
|
||||||
|
export * from "./root";
|
||||||
|
export * from "./status";
|
46
web/components/modules/applied-filters/members.tsx
Normal file
46
web/components/modules/applied-filters/members.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
// ui
|
||||||
|
import { Avatar } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { useMember } from "hooks/store";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleRemove: (val: string) => void;
|
||||||
|
values: string[];
|
||||||
|
editable: boolean | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
|
||||||
|
const { handleRemove, values, editable } = props;
|
||||||
|
// store hooks
|
||||||
|
const {
|
||||||
|
workspace: { getWorkspaceMemberDetails },
|
||||||
|
} = useMember();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{values.map((memberId) => {
|
||||||
|
const memberDetails = getWorkspaceMemberDetails(memberId)?.member;
|
||||||
|
|
||||||
|
if (!memberDetails) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={memberId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||||
|
<Avatar name={memberDetails.display_name} src={memberDetails.avatar} showTooltip={false} />
|
||||||
|
<span className="normal-case">{memberDetails.display_name}</span>
|
||||||
|
{editable && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
|
onClick={() => handleRemove(memberId)}
|
||||||
|
>
|
||||||
|
<X size={10} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
88
web/components/modules/applied-filters/root.tsx
Normal file
88
web/components/modules/applied-filters/root.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { X } from "lucide-react";
|
||||||
|
// components
|
||||||
|
import { AppliedDateFilters, AppliedMembersFilters, AppliedStatusFilters } from "components/modules";
|
||||||
|
// helpers
|
||||||
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
|
// types
|
||||||
|
import { TModuleFilters } from "@plane/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appliedFilters: TModuleFilters;
|
||||||
|
handleClearAllFilters: () => void;
|
||||||
|
handleRemoveFilter: (key: keyof TModuleFilters, value: string | null) => void;
|
||||||
|
alwaysAllowEditing?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MEMBERS_FILTERS = ["lead", "members"];
|
||||||
|
const DATE_FILTERS = ["start_date", "target_date"];
|
||||||
|
|
||||||
|
export const ModuleAppliedFiltersList: React.FC<Props> = (props) => {
|
||||||
|
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, alwaysAllowEditing } = props;
|
||||||
|
|
||||||
|
if (!appliedFilters) return null;
|
||||||
|
if (Object.keys(appliedFilters).length === 0) return null;
|
||||||
|
|
||||||
|
const isEditingAllowed = alwaysAllowEditing;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
|
||||||
|
{Object.entries(appliedFilters).map(([key, value]) => {
|
||||||
|
const filterKey = key as keyof TModuleFilters;
|
||||||
|
|
||||||
|
if (!value) return;
|
||||||
|
if (Array.isArray(value) && value.length === 0) return;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={filterKey}
|
||||||
|
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize"
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
<span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span>
|
||||||
|
{filterKey === "status" && (
|
||||||
|
<AppliedStatusFilters
|
||||||
|
editable={isEditingAllowed}
|
||||||
|
handleRemove={(val) => handleRemoveFilter("status", val)}
|
||||||
|
values={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{DATE_FILTERS.includes(filterKey) && (
|
||||||
|
<AppliedDateFilters
|
||||||
|
editable={isEditingAllowed}
|
||||||
|
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||||
|
values={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{MEMBERS_FILTERS.includes(filterKey) && (
|
||||||
|
<AppliedMembersFilters
|
||||||
|
editable={isEditingAllowed}
|
||||||
|
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||||
|
values={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isEditingAllowed && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
|
onClick={() => handleRemoveFilter(filterKey, null)}
|
||||||
|
>
|
||||||
|
<X size={12} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{isEditingAllowed && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClearAllFilters}
|
||||||
|
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
|
||||||
|
>
|
||||||
|
Clear all
|
||||||
|
<X size={12} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
41
web/components/modules/applied-filters/status.tsx
Normal file
41
web/components/modules/applied-filters/status.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
// ui
|
||||||
|
import { ModuleStatusIcon } from "@plane/ui";
|
||||||
|
// constants
|
||||||
|
import { MODULE_STATUS } from "constants/module";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleRemove: (val: string) => void;
|
||||||
|
values: string[];
|
||||||
|
editable: boolean | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppliedStatusFilters: React.FC<Props> = observer((props) => {
|
||||||
|
const { handleRemove, values, editable } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{values.map((status) => {
|
||||||
|
const statusDetails = MODULE_STATUS?.find((s) => s.value === status);
|
||||||
|
if (!statusDetails) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={status} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||||
|
<ModuleStatusIcon status={statusDetails.value} height="12px" width="12px" />
|
||||||
|
{statusDetails.label}
|
||||||
|
{editable && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
|
onClick={() => handleRemove(status)}
|
||||||
|
>
|
||||||
|
<X size={10} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
6
web/components/modules/dropdowns/filters/index.ts
Normal file
6
web/components/modules/dropdowns/filters/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from "./lead";
|
||||||
|
export * from "./members";
|
||||||
|
export * from "./root";
|
||||||
|
export * from "./start-date";
|
||||||
|
export * from "./status";
|
||||||
|
export * from "./target-date";
|
96
web/components/modules/dropdowns/filters/lead.tsx
Normal file
96
web/components/modules/dropdowns/filters/lead.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import sortBy from "lodash/sortBy";
|
||||||
|
// hooks
|
||||||
|
import { useMember } from "hooks/store";
|
||||||
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "components/issues";
|
||||||
|
// ui
|
||||||
|
import { Avatar, Loader } from "@plane/ui";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appliedFilters: string[] | null;
|
||||||
|
handleUpdate: (val: string) => void;
|
||||||
|
memberIds: string[] | undefined;
|
||||||
|
searchQuery: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterLead: React.FC<Props> = observer((props: Props) => {
|
||||||
|
const { appliedFilters, handleUpdate, memberIds, searchQuery } = props;
|
||||||
|
// states
|
||||||
|
const [itemsToRender, setItemsToRender] = useState(5);
|
||||||
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
// store hooks
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
|
|
||||||
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
const sortedOptions = useMemo(() => {
|
||||||
|
const filteredOptions = (memberIds || []).filter((memberId) =>
|
||||||
|
getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return sortBy(filteredOptions, [
|
||||||
|
(memberId) => !(appliedFilters ?? []).includes(memberId),
|
||||||
|
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
|
||||||
|
]);
|
||||||
|
}, [appliedFilters, getUserDetails, memberIds, , searchQuery]);
|
||||||
|
|
||||||
|
const handleViewToggle = () => {
|
||||||
|
if (!sortedOptions) return;
|
||||||
|
|
||||||
|
if (itemsToRender === sortedOptions.length) setItemsToRender(5);
|
||||||
|
else setItemsToRender(sortedOptions.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FilterHeader
|
||||||
|
title={`Lead${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||||
|
isPreviewEnabled={previewEnabled}
|
||||||
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
|
/>
|
||||||
|
{previewEnabled && (
|
||||||
|
<div>
|
||||||
|
{sortedOptions ? (
|
||||||
|
sortedOptions.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{sortedOptions.slice(0, itemsToRender).map((memberId) => {
|
||||||
|
const member = getUserDetails(memberId);
|
||||||
|
|
||||||
|
if (!member) return null;
|
||||||
|
return (
|
||||||
|
<FilterOption
|
||||||
|
key={`lead-${member.id}`}
|
||||||
|
isChecked={appliedFilters?.includes(member.id) ? true : false}
|
||||||
|
onClick={() => handleUpdate(member.id)}
|
||||||
|
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
|
||||||
|
title={member.display_name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{sortedOptions.length > 5 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-8 text-xs font-medium text-custom-primary-100"
|
||||||
|
onClick={handleViewToggle}
|
||||||
|
>
|
||||||
|
{itemsToRender === sortedOptions.length ? "View less" : "View all"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Loader className="space-y-2">
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
97
web/components/modules/dropdowns/filters/members.tsx
Normal file
97
web/components/modules/dropdowns/filters/members.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import sortBy from "lodash/sortBy";
|
||||||
|
// hooks
|
||||||
|
import { useMember } from "hooks/store";
|
||||||
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "components/issues";
|
||||||
|
// ui
|
||||||
|
import { Avatar, Loader } from "@plane/ui";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appliedFilters: string[] | null;
|
||||||
|
handleUpdate: (val: string) => void;
|
||||||
|
memberIds: string[] | undefined;
|
||||||
|
searchQuery: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterMembers: React.FC<Props> = observer((props: Props) => {
|
||||||
|
const { appliedFilters, handleUpdate, memberIds, searchQuery } = props;
|
||||||
|
// states
|
||||||
|
const [itemsToRender, setItemsToRender] = useState(5);
|
||||||
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
// store hooks
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
|
|
||||||
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
const sortedOptions = useMemo(() => {
|
||||||
|
const filteredOptions = (memberIds || []).filter((memberId) =>
|
||||||
|
getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return sortBy(filteredOptions, [
|
||||||
|
(memberId) => !(appliedFilters ?? []).includes(memberId),
|
||||||
|
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
|
||||||
|
]);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
const handleViewToggle = () => {
|
||||||
|
if (!sortedOptions) return;
|
||||||
|
|
||||||
|
if (itemsToRender === sortedOptions.length) setItemsToRender(5);
|
||||||
|
else setItemsToRender(sortedOptions.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FilterHeader
|
||||||
|
title={`Members${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||||
|
isPreviewEnabled={previewEnabled}
|
||||||
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
|
/>
|
||||||
|
{previewEnabled && (
|
||||||
|
<div>
|
||||||
|
{sortedOptions ? (
|
||||||
|
sortedOptions.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{sortedOptions.slice(0, itemsToRender).map((memberId) => {
|
||||||
|
const member = getUserDetails(memberId);
|
||||||
|
|
||||||
|
if (!member) return null;
|
||||||
|
return (
|
||||||
|
<FilterOption
|
||||||
|
key={`member-${member.id}`}
|
||||||
|
isChecked={appliedFilters?.includes(member.id) ? true : false}
|
||||||
|
onClick={() => handleUpdate(member.id)}
|
||||||
|
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
|
||||||
|
title={member.display_name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{sortedOptions.length > 5 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-8 text-xs font-medium text-custom-primary-100"
|
||||||
|
onClick={handleViewToggle}
|
||||||
|
>
|
||||||
|
{itemsToRender === sortedOptions.length ? "View less" : "View all"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Loader className="space-y-2">
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
106
web/components/modules/dropdowns/filters/root.tsx
Normal file
106
web/components/modules/dropdowns/filters/root.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Search, X } from "lucide-react";
|
||||||
|
// components
|
||||||
|
import { FilterLead, FilterMembers, FilterStartDate, FilterStatus, FilterTargetDate } from "components/modules";
|
||||||
|
import { FilterOption } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { TModuleDisplayFilters, TModuleFilters } from "@plane/types";
|
||||||
|
import { TModuleStatus } from "@plane/ui";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
displayFilters: TModuleDisplayFilters;
|
||||||
|
filters: TModuleFilters;
|
||||||
|
handleDisplayFiltersUpdate: (updatedDisplayProperties: Partial<TModuleDisplayFilters>) => void;
|
||||||
|
handleFiltersUpdate: (key: keyof TModuleFilters, value: string | string[]) => void;
|
||||||
|
memberIds?: string[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModuleFiltersSelection: React.FC<Props> = observer((props) => {
|
||||||
|
const { displayFilters, filters, handleDisplayFiltersUpdate, handleFiltersUpdate, memberIds } = props;
|
||||||
|
// states
|
||||||
|
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||||
|
<div className="bg-custom-background-100 p-2.5 pb-0">
|
||||||
|
<div className="flex items-center gap-1.5 rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-1.5 py-1 text-xs">
|
||||||
|
<Search className="text-custom-text-400" size={12} strokeWidth={2} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
|
||||||
|
placeholder="Search"
|
||||||
|
value={filtersSearchQuery}
|
||||||
|
onChange={(e) => setFiltersSearchQuery(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{filtersSearchQuery !== "" && (
|
||||||
|
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>
|
||||||
|
<X className="text-custom-text-300" size={12} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="h-full w-full divide-y divide-custom-border-200 overflow-y-auto px-2.5 vertical-scrollbar scrollbar-sm">
|
||||||
|
<div className="py-2">
|
||||||
|
<FilterOption
|
||||||
|
isChecked={!!displayFilters.favorites}
|
||||||
|
onClick={() =>
|
||||||
|
handleDisplayFiltersUpdate({
|
||||||
|
favorites: !displayFilters.favorites,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
title="Favorites"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* status */}
|
||||||
|
<div className="py-2">
|
||||||
|
<FilterStatus
|
||||||
|
appliedFilters={(filters.status as TModuleStatus[]) ?? null}
|
||||||
|
handleUpdate={(val) => handleFiltersUpdate("status", val)}
|
||||||
|
searchQuery={filtersSearchQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* lead */}
|
||||||
|
<div className="py-2">
|
||||||
|
<FilterLead
|
||||||
|
appliedFilters={filters.lead ?? null}
|
||||||
|
handleUpdate={(val) => handleFiltersUpdate("lead", val)}
|
||||||
|
searchQuery={filtersSearchQuery}
|
||||||
|
memberIds={memberIds}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* members */}
|
||||||
|
<div className="py-2">
|
||||||
|
<FilterMembers
|
||||||
|
appliedFilters={filters.members ?? null}
|
||||||
|
handleUpdate={(val) => handleFiltersUpdate("members", val)}
|
||||||
|
searchQuery={filtersSearchQuery}
|
||||||
|
memberIds={memberIds}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* start date */}
|
||||||
|
<div className="py-2">
|
||||||
|
<FilterStartDate
|
||||||
|
appliedFilters={filters.start_date ?? null}
|
||||||
|
handleUpdate={(val) => handleFiltersUpdate("start_date", val)}
|
||||||
|
searchQuery={filtersSearchQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* target date */}
|
||||||
|
<div className="py-2">
|
||||||
|
<FilterTargetDate
|
||||||
|
appliedFilters={filters.target_date ?? null}
|
||||||
|
handleUpdate={(val) => handleFiltersUpdate("target_date", val)}
|
||||||
|
searchQuery={filtersSearchQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
65
web/components/modules/dropdowns/filters/start-date.tsx
Normal file
65
web/components/modules/dropdowns/filters/start-date.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { DateFilterModal } from "components/core";
|
||||||
|
import { FilterHeader, FilterOption } from "components/issues";
|
||||||
|
// constants
|
||||||
|
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appliedFilters: string[] | null;
|
||||||
|
handleUpdate: (val: string | string[]) => void;
|
||||||
|
searchQuery: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterStartDate: React.FC<Props> = observer((props) => {
|
||||||
|
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||||
|
|
||||||
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) =>
|
||||||
|
d.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isDateFilterModalOpen && (
|
||||||
|
<DateFilterModal
|
||||||
|
handleClose={() => setIsDateFilterModalOpen(false)}
|
||||||
|
isOpen={isDateFilterModalOpen}
|
||||||
|
onSelect={(val) => handleUpdate(val)}
|
||||||
|
title="Start date"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<FilterHeader
|
||||||
|
title={`Start date${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||||
|
isPreviewEnabled={previewEnabled}
|
||||||
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
|
/>
|
||||||
|
{previewEnabled && (
|
||||||
|
<div>
|
||||||
|
{filteredOptions.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{filteredOptions.map((option) => (
|
||||||
|
<FilterOption
|
||||||
|
key={option.value}
|
||||||
|
isChecked={appliedFilters?.includes(option.value) ? true : false}
|
||||||
|
onClick={() => handleUpdate(option.value)}
|
||||||
|
title={option.name}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<FilterOption isChecked={false} onClick={() => setIsDateFilterModalOpen(true)} title="Custom" multiple />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
52
web/components/modules/dropdowns/filters/status.tsx
Normal file
52
web/components/modules/dropdowns/filters/status.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "components/issues";
|
||||||
|
// ui
|
||||||
|
import { ModuleStatusIcon } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { TModuleStatus } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { MODULE_STATUS } from "constants/module";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appliedFilters: TModuleStatus[] | null;
|
||||||
|
handleUpdate: (val: string) => void;
|
||||||
|
searchQuery: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterStatus: React.FC<Props> = observer((props) => {
|
||||||
|
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||||
|
// states
|
||||||
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
|
||||||
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
const filteredOptions = MODULE_STATUS.filter((p) => p.value.includes(searchQuery.toLowerCase()));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FilterHeader
|
||||||
|
title={`Status${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||||
|
isPreviewEnabled={previewEnabled}
|
||||||
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
|
/>
|
||||||
|
{previewEnabled && (
|
||||||
|
<div>
|
||||||
|
{filteredOptions.length > 0 ? (
|
||||||
|
filteredOptions.map((status) => (
|
||||||
|
<FilterOption
|
||||||
|
key={status.value}
|
||||||
|
isChecked={appliedFilters?.includes(status.value) ? true : false}
|
||||||
|
onClick={() => handleUpdate(status.value)}
|
||||||
|
icon={<ModuleStatusIcon status={status.value} />}
|
||||||
|
title={status.label}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
65
web/components/modules/dropdowns/filters/target-date.tsx
Normal file
65
web/components/modules/dropdowns/filters/target-date.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { DateFilterModal } from "components/core";
|
||||||
|
import { FilterHeader, FilterOption } from "components/issues";
|
||||||
|
// constants
|
||||||
|
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appliedFilters: string[] | null;
|
||||||
|
handleUpdate: (val: string | string[]) => void;
|
||||||
|
searchQuery: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterTargetDate: React.FC<Props> = observer((props) => {
|
||||||
|
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||||
|
|
||||||
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) =>
|
||||||
|
d.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isDateFilterModalOpen && (
|
||||||
|
<DateFilterModal
|
||||||
|
handleClose={() => setIsDateFilterModalOpen(false)}
|
||||||
|
isOpen={isDateFilterModalOpen}
|
||||||
|
onSelect={(val) => handleUpdate(val)}
|
||||||
|
title="Due date"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<FilterHeader
|
||||||
|
title={`Due date${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||||
|
isPreviewEnabled={previewEnabled}
|
||||||
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
|
/>
|
||||||
|
{previewEnabled && (
|
||||||
|
<div>
|
||||||
|
{filteredOptions.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{filteredOptions.map((option) => (
|
||||||
|
<FilterOption
|
||||||
|
key={option.value}
|
||||||
|
isChecked={appliedFilters?.includes(option.value) ? true : false}
|
||||||
|
onClick={() => handleUpdate(option.value)}
|
||||||
|
title={option.name}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<FilterOption isChecked={false} onClick={() => setIsDateFilterModalOpen(true)} title="Custom" multiple />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
2
web/components/modules/dropdowns/index.ts
Normal file
2
web/components/modules/dropdowns/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./filters";
|
||||||
|
export * from "./order-by";
|
70
web/components/modules/dropdowns/order-by.tsx
Normal file
70
web/components/modules/dropdowns/order-by.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { ArrowDownWideNarrow, Check, ChevronDown } from "lucide-react";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu, getButtonStyling } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
// types
|
||||||
|
import { TModuleOrderByOptions } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { MODULE_ORDER_BY_OPTIONS } from "constants/module";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onChange: (value: TModuleOrderByOptions) => void;
|
||||||
|
value: TModuleOrderByOptions | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModuleOrderByDropdown: React.FC<Props> = (props) => {
|
||||||
|
const { onChange, value } = props;
|
||||||
|
|
||||||
|
const orderByDetails = MODULE_ORDER_BY_OPTIONS.find((option) => value?.includes(option.key));
|
||||||
|
|
||||||
|
const isDescending = value?.[0] === "-";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomMenu
|
||||||
|
customButton={
|
||||||
|
<div className={cn(getButtonStyling("neutral-primary", "sm"), "px-2 text-custom-text-300")}>
|
||||||
|
<ArrowDownWideNarrow className="h-3 w-3" />
|
||||||
|
{orderByDetails?.label}
|
||||||
|
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="bottom-end"
|
||||||
|
maxHeight="lg"
|
||||||
|
closeOnSelect
|
||||||
|
>
|
||||||
|
{MODULE_ORDER_BY_OPTIONS.map((option) => (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={option.key}
|
||||||
|
className="flex items-center justify-between gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
if (isDescending) onChange(`-${option.key}` as TModuleOrderByOptions);
|
||||||
|
else onChange(option.key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
{value?.includes(option.key) && <Check className="h-3 w-3" />}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
<hr className="my-2" />
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
className="flex items-center justify-between gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
if (isDescending) onChange(value.slice(1) as TModuleOrderByOptions);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ascending
|
||||||
|
{!isDescending && <Check className="h-3 w-3" />}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
className="flex items-center justify-between gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
if (!isDescending) onChange(`-${value}` as TModuleOrderByOptions);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Descending
|
||||||
|
{isDescending && <Check className="h-3 w-3" />}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,8 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, ModuleStatusIcon } from "@plane/ui";
|
import { Tooltip, ModuleStatusIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -24,6 +26,8 @@ export const ModuleGanttBlock: React.FC<Props> = observer((props) => {
|
|||||||
const { getModuleById } = useModule();
|
const { getModuleById } = useModule();
|
||||||
// derived values
|
// derived values
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
|
// hooks
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -35,6 +39,7 @@ export const ModuleGanttBlock: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h5>{moduleDetails?.name}</h5>
|
<h5>{moduleDetails?.name}</h5>
|
||||||
@ -54,8 +59,6 @@ export const ModuleGanttBlock: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
export const ModuleGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
export const ModuleGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
||||||
const { moduleId } = props;
|
const { moduleId } = props;
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
router: { workspaceSlug },
|
router: { workspaceSlug },
|
||||||
@ -65,14 +68,12 @@ export const ModuleGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Link
|
||||||
className="relative flex h-full w-full items-center gap-2"
|
className="relative flex h-full w-full items-center gap-2"
|
||||||
onClick={() =>
|
href={`/${workspaceSlug}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`}
|
||||||
router.push(`/${workspaceSlug}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<ModuleStatusIcon status={moduleDetails?.status ?? "backlog"} height="16px" width="16px" />
|
<ModuleStatusIcon status={moduleDetails?.status ?? "backlog"} height="16px" width="16px" />
|
||||||
<h6 className="flex-grow truncate text-sm font-medium">{moduleDetails?.name}</h6>
|
<h6 className="flex-grow truncate text-sm font-medium">{moduleDetails?.name}</h6>
|
||||||
</div>
|
</Link>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -11,10 +11,12 @@ import { IModule } from "@plane/types";
|
|||||||
export const ModulesListGanttChartView: React.FC = observer(() => {
|
export const ModulesListGanttChartView: React.FC = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// store
|
// store
|
||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
const { projectModuleIds, moduleMap, updateModuleDetails } = useModule();
|
const { getFilteredModuleIds, moduleMap, updateModuleDetails } = useModule();
|
||||||
|
// derived values
|
||||||
|
const filteredModuleIds = projectId ? getFilteredModuleIds(projectId.toString()) : undefined;
|
||||||
|
|
||||||
const handleModuleUpdate = async (module: IModule, data: IBlockUpdateData) => {
|
const handleModuleUpdate = async (module: IModule, data: IBlockUpdateData) => {
|
||||||
if (!workspaceSlug || !module) return;
|
if (!workspaceSlug || !module) return;
|
||||||
@ -44,7 +46,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
|
|||||||
<GanttChartRoot
|
<GanttChartRoot
|
||||||
title="Modules"
|
title="Modules"
|
||||||
loaderTitle="Modules"
|
loaderTitle="Modules"
|
||||||
blocks={projectModuleIds ? blockFormat(projectModuleIds) : null}
|
blocks={filteredModuleIds ? blockFormat(filteredModuleIds) : null}
|
||||||
sidebarToRender={(props) => <ModuleGanttSidebar {...props} />}
|
sidebarToRender={(props) => <ModuleGanttSidebar {...props} />}
|
||||||
blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)}
|
blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)}
|
||||||
blockToRender={(data: IModule) => <ModuleGanttBlock moduleId={data.id} />}
|
blockToRender={(data: IModule) => <ModuleGanttBlock moduleId={data.id} />}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
export * from "./applied-filters";
|
||||||
|
export * from "./dropdowns";
|
||||||
export * from "./select";
|
export * from "./select";
|
||||||
export * from "./sidebar-select";
|
export * from "./sidebar-select";
|
||||||
export * from "./delete-module-modal";
|
export * from "./delete-module-modal";
|
||||||
|
@ -12,6 +12,7 @@ import { EUserProjectRoles } from "constants/project";
|
|||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||||
import { useEventTracker, useMember, useModule, useUser } from "hooks/store";
|
import { useEventTracker, useMember, useModule, useUser } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
// ui
|
// ui
|
||||||
// helpers
|
// helpers
|
||||||
@ -39,7 +40,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
// derived values
|
// derived values
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -179,7 +180,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
|
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<Tooltip tooltipContent={moduleDetails.name} position="top">
|
<Tooltip tooltipContent={moduleDetails.name} position="top" isMobile={isMobile}>
|
||||||
<span className="truncate text-base font-medium">{moduleDetails.name}</span>
|
<span className="truncate text-base font-medium">{moduleDetails.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -208,7 +209,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
<span className="text-xs text-custom-text-300">{issueCount ?? "0 Issue"}</span>
|
<span className="text-xs text-custom-text-300">{issueCount ?? "0 Issue"}</span>
|
||||||
</div>
|
</div>
|
||||||
{moduleDetails.member_ids?.length > 0 && (
|
{moduleDetails.member_ids?.length > 0 && (
|
||||||
<Tooltip tooltipContent={`${moduleDetails.member_ids.length} Members`}>
|
<Tooltip tooltipContent={`${moduleDetails.member_ids.length} Members`} isMobile={isMobile}>
|
||||||
<div className="flex cursor-default items-center gap-1">
|
<div className="flex cursor-default items-center gap-1">
|
||||||
<AvatarGroup showTooltip={false}>
|
<AvatarGroup showTooltip={false}>
|
||||||
{moduleDetails.member_ids.map((member_id) => {
|
{moduleDetails.member_ids.map((member_id) => {
|
||||||
@ -222,6 +223,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
tooltipContent={isNaN(completionPercentage) ? "0" : `${completionPercentage.toFixed(0)}%`}
|
tooltipContent={isNaN(completionPercentage) ? "0" : `${completionPercentage.toFixed(0)}%`}
|
||||||
position="top-left"
|
position="top-left"
|
||||||
>
|
>
|
||||||
|
@ -21,6 +21,7 @@ import { EUserProjectRoles } from "constants/project";
|
|||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||||
import { useModule, useUser, useEventTracker, useMember } from "hooks/store";
|
import { useModule, useUser, useEventTracker, useMember } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
// ui
|
// ui
|
||||||
// helpers
|
// helpers
|
||||||
@ -48,7 +49,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
// derived values
|
// derived values
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -194,7 +195,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
</CircularProgressIndicator>
|
</CircularProgressIndicator>
|
||||||
</span>
|
</span>
|
||||||
<Tooltip tooltipContent={moduleDetails.name} position="top">
|
<Tooltip tooltipContent={moduleDetails.name} position="top" isMobile={isMobile}>
|
||||||
<span className="truncate text-base font-medium">{moduleDetails.name}</span>
|
<span className="truncate text-base font-medium">{moduleDetails.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@ -227,7 +228,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex flex-shrink-0 items-center gap-3">
|
<div className="relative flex flex-shrink-0 items-center gap-3">
|
||||||
<Tooltip tooltipContent={`${moduleDetails?.member_ids?.length || 0} Members`}>
|
<Tooltip tooltipContent={`${moduleDetails?.member_ids?.length || 0} Members`} isMobile={isMobile}>
|
||||||
<div className="flex w-10 cursor-default items-center justify-center gap-1">
|
<div className="flex w-10 cursor-default items-center justify-center gap-1">
|
||||||
{moduleDetails.member_ids.length > 0 ? (
|
{moduleDetails.member_ids.length > 0 ? (
|
||||||
<AvatarGroup showTooltip={false}>
|
<AvatarGroup showTooltip={false}>
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
|
import Image from "next/image";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useEventTracker, useModule } from "hooks/store";
|
import { useApplication, useEventTracker, useModule, useModuleFilter } from "hooks/store";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
|
||||||
// components
|
// components
|
||||||
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules";
|
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules";
|
||||||
import { EmptyState } from "components/empty-state";
|
import { EmptyState } from "components/empty-state";
|
||||||
// ui
|
// ui
|
||||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui";
|
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui";
|
||||||
|
// assets
|
||||||
|
import NameFilterImage from "public/empty-state/module/name-filter.svg";
|
||||||
|
import AllFiltersImage from "public/empty-state/module/all-filters.svg";
|
||||||
// constants
|
// constants
|
||||||
import { EmptyStateType } from "constants/empty-state";
|
import { EmptyStateType } from "constants/empty-state";
|
||||||
|
|
||||||
@ -18,29 +21,48 @@ export const ModulesListView: React.FC = observer(() => {
|
|||||||
// store hooks
|
// store hooks
|
||||||
const { commandPalette: commandPaletteStore } = useApplication();
|
const { commandPalette: commandPaletteStore } = useApplication();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
|
const { getFilteredModuleIds, loader } = useModule();
|
||||||
|
const { currentProjectDisplayFilters: displayFilters, searchQuery } = useModuleFilter();
|
||||||
|
// derived values
|
||||||
|
const filteredModuleIds = projectId ? getFilteredModuleIds(projectId.toString()) : undefined;
|
||||||
|
|
||||||
const { projectModuleIds, loader } = useModule();
|
if (loader || !filteredModuleIds)
|
||||||
|
|
||||||
const { storedValue: modulesView } = useLocalStorage("modules_view", "grid");
|
|
||||||
|
|
||||||
if (loader || !projectModuleIds)
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{modulesView === "list" && <CycleModuleListLayout />}
|
{displayFilters?.layout === "list" && <CycleModuleListLayout />}
|
||||||
{modulesView === "grid" && <CycleModuleBoardLayout />}
|
{displayFilters?.layout === "board" && <CycleModuleBoardLayout />}
|
||||||
{modulesView === "gantt_chart" && <GanttLayoutLoader />}
|
{displayFilters?.layout === "gantt" && <GanttLayoutLoader />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (filteredModuleIds.length === 0)
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full grid place-items-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Image
|
||||||
|
src={searchQuery.trim() === "" ? AllFiltersImage : NameFilterImage}
|
||||||
|
className="h-36 sm:h-48 w-36 sm:w-48 mx-auto"
|
||||||
|
alt="No matching modules"
|
||||||
|
/>
|
||||||
|
<h5 className="text-xl font-medium mt-7 mb-1">No matching modules</h5>
|
||||||
|
<p className="text-custom-text-400 text-base">
|
||||||
|
{searchQuery.trim() === ""
|
||||||
|
? "Remove the filters to see all modules"
|
||||||
|
: "Remove the search criteria to see all modules"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{projectModuleIds.length > 0 ? (
|
{filteredModuleIds.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{modulesView === "list" && (
|
{displayFilters?.layout === "list" && (
|
||||||
<div className="h-full overflow-y-auto">
|
<div className="h-full overflow-y-auto">
|
||||||
<div className="flex h-full w-full justify-between">
|
<div className="flex h-full w-full justify-between">
|
||||||
<div className="flex h-full w-full flex-col overflow-y-auto vertical-scrollbar scrollbar-lg">
|
<div className="flex h-full w-full flex-col overflow-y-auto vertical-scrollbar scrollbar-lg">
|
||||||
{projectModuleIds.map((moduleId) => (
|
{filteredModuleIds.map((moduleId) => (
|
||||||
<ModuleListItem key={moduleId} moduleId={moduleId} />
|
<ModuleListItem key={moduleId} moduleId={moduleId} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -51,7 +73,7 @@ export const ModulesListView: React.FC = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{modulesView === "grid" && (
|
{displayFilters?.layout === "board" && (
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<div className="flex h-full w-full justify-between">
|
<div className="flex h-full w-full justify-between">
|
||||||
<div
|
<div
|
||||||
@ -61,7 +83,7 @@ export const ModulesListView: React.FC = observer(() => {
|
|||||||
: "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4"
|
: "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4"
|
||||||
} auto-rows-max transition-all vertical-scrollbar scrollbar-lg`}
|
} auto-rows-max transition-all vertical-scrollbar scrollbar-lg`}
|
||||||
>
|
>
|
||||||
{projectModuleIds.map((moduleId) => (
|
{filteredModuleIds.map((moduleId) => (
|
||||||
<ModuleCardItem key={moduleId} moduleId={moduleId} />
|
<ModuleCardItem key={moduleId} moduleId={moduleId} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -72,7 +94,7 @@ export const ModulesListView: React.FC = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{modulesView === "gantt_chart" && <ModulesListGanttChartView />}
|
{displayFilters?.layout === "gantt" && <ModulesListGanttChartView />}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
|
@ -15,6 +15,7 @@ import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "help
|
|||||||
import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker } from "hooks/store";
|
import { useEventTracker } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// type
|
// type
|
||||||
import type { IUserNotification, NotificationType } from "@plane/types";
|
import type { IUserNotification, NotificationType } from "@plane/types";
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
} = props;
|
} = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
// states
|
// states
|
||||||
@ -358,7 +359,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<Tooltip tooltipContent={item.name} key={item.id}>
|
<Tooltip tooltipContent={item.name} key={item.id} isMobile={isMobile}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -373,7 +374,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
<Tooltip tooltipContent="Snooze">
|
<Tooltip tooltipContent="Snooze" isMobile={isMobile}>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
customButton={
|
customButton={
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
} from "constants/event-tracker";
|
} from "constants/event-tracker";
|
||||||
import { getNumberCount } from "helpers/string.helper";
|
import { getNumberCount } from "helpers/string.helper";
|
||||||
import { useEventTracker } from "hooks/store";
|
import { useEventTracker } from "hooks/store";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// helpers
|
// helpers
|
||||||
// type
|
// type
|
||||||
import type { NotificationType, NotificationCount } from "@plane/types";
|
import type { NotificationType, NotificationCount } from "@plane/types";
|
||||||
@ -52,6 +53,8 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
} = props;
|
} = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
|
// hooks
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
const notificationTabs: Array<{
|
const notificationTabs: Array<{
|
||||||
label: string;
|
label: string;
|
||||||
@ -84,7 +87,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-x-4 text-custom-text-200">
|
<div className="flex items-center justify-center gap-x-4 text-custom-text-200">
|
||||||
<Tooltip tooltipContent="Refresh">
|
<Tooltip tooltipContent="Refresh" isMobile={isMobile}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -94,7 +97,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
<RefreshCw className={`h-3.5 w-3.5 ${isRefreshing ? "animate-spin" : ""}`} />
|
<RefreshCw className={`h-3.5 w-3.5 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip tooltipContent="Unread notifications">
|
<Tooltip tooltipContent="Unread notifications" isMobile={isMobile}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -154,7 +157,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
<Tooltip tooltipContent="Close">
|
<Tooltip tooltipContent="Close" isMobile={isMobile}>
|
||||||
<button type="button" onClick={() => closePopover()}>
|
<button type="button" onClick={() => closePopover()}>
|
||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -11,6 +11,7 @@ import { getNumberCount } from "helpers/string.helper";
|
|||||||
import { useApplication } from "hooks/store";
|
import { useApplication } from "hooks/store";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
import useUserNotification from "hooks/use-user-notifications";
|
import useUserNotification from "hooks/use-user-notifications";
|
||||||
|
import { usePlatformOS } from "hooks/use-platform-os";
|
||||||
// components
|
// components
|
||||||
// images
|
// images
|
||||||
import emptyNotification from "public/empty-state/notification.svg";
|
import emptyNotification from "public/empty-state/notification.svg";
|
||||||
@ -23,6 +24,8 @@ export const NotificationPopover = observer(() => {
|
|||||||
const { theme: themeStore } = useApplication();
|
const { theme: themeStore } = useApplication();
|
||||||
// refs
|
// refs
|
||||||
const notificationPopoverRef = React.useRef<HTMLDivElement | null>(null);
|
const notificationPopoverRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
// hooks
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
notifications,
|
notifications,
|
||||||
@ -67,7 +70,7 @@ export const NotificationPopover = observer(() => {
|
|||||||
/>
|
/>
|
||||||
<Popover ref={notificationPopoverRef} className="md:relative w-full">
|
<Popover ref={notificationPopoverRef} className="md:relative w-full">
|
||||||
<>
|
<>
|
||||||
<Tooltip tooltipContent="Notifications" position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
<Tooltip tooltipContent="Notifications" position="right" className="ml-2" disabled={!isSidebarCollapsed} isMobile={isMobile}>
|
||||||
<button
|
<button
|
||||||
className={`group relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
className={`group relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
||||||
isActive
|
isActive
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user