conflicts: resolved merge conflicts

This commit is contained in:
gurusainath 2024-03-14 13:18:38 +05:30
commit 2b27e9c339
156 changed files with 3545 additions and 588 deletions

View File

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

View File

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

View File

@ -1,82 +1,79 @@
# 1-Click Self-Hosting # One-click deploy
In this guide, we will walk you through the process of setting up a 1-click self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization. Deployment methods for Plane have improved significantly to make self-managing super-easy. One of those is a single-line-command installation of Plane.
Let's get started! This short guide will guide you through the process, the background tasks that run with the command for the Community, One, and Enterprise editions, and the post-deployment configuration options available to you.
## Installing Plane ### Requirements
Installing Plane is a very easy and minimal step process. - Operating systems: Debian, Ubuntu, CentOS
- Supported CPU architectures: AMD64, ARM64, x86_64, AArch64
### Prerequisite ### Download the latest stable release
- Operating System (latest): Debian / Ubuntu / Centos Run ↓ on any CLI.
- Supported CPU Architechture: AMD64 / ARM64 / x86_64 / aarch64
### Downloading Latest Stable Release
``` ```
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh - curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh -
``` ```
<details> ### Download the Preview release
<summary>Downloading Preview Release</summary>
`Preview` builds do not support ARM64, AArch64 CPU architectures
Run ↓ on any CLI.
``` ```
export BRANCH=preview export BRANCH=preview
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh - curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh -
``` ```
NOTE: `Preview` builds do not support ARM64/AARCH64 CPU architecture
</details>
--
Expect this after a successful install
![Install Output](images/install.png)
Access the application on a browser via http://server-ip-address
--- ---
### Get Control of your Plane Server Setup ### Successful installation
Plane App is available via the command `plane-app`. Running the command `plane-app --help` helps you to manage Plane You should see ↓ if there are no hitches. That output will also list the IP address you can use to access your Plane instance.
![Install Output](images/install.png)
---
### Manage your Plane instance
Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of all operators with `plane-app ---help`.
![Plane Help](images/help.png) ![Plane Help](images/help.png)
<ins>Basic Operations</ins>: 1. Basic operators
1. Start Server using `plane-app start` 1. `plane-app start` starts the Plane server.
1. Stop Server using `plane-app stop` 2. `plane-app restart` restarts the Plane server.
1. Restart Server using `plane-app restart` 3. `plane-app stop` stops the Plane server.
<ins>Advanced Operations</ins>: 2. Advanced operators
1. Configure Plane using `plane-app --configure`. This will give you options to modify `plane-app --configure` will show advanced configurators.
- NGINX Port (default 80) - Change your proxy or listening port
- Domain Name (default is the local server public IP address) <br>Default: 80
- File Upload Size (default 5MB) - Change your domain name
- External Postgres DB Url (optional - default empty) <br>Default: Deployed server's public IP address
- External Redis URL (optional - default empty) - File upload size
- AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket) <br>Default: 5MB
- Specify external database address when using an external database
<br>Default: `Empty`
<br>`Default folder: /opt/plane/data/postgres`
- Specify external Redis URL when using external Redis
<br>Default: `Empty`
<br>`Default folder: /opt/plane/data/redis`
- Configure AWS S3 bucket
<br>Use only when you or your users want to use S3
<br>`Default folder: /opt/plane/data/minio`
1. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images) 3. Version operators
1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility. 1. `plane-app --upgrade` gets the latest stable version of `docker-compose.yaml`, `.env`, and Docker images
2. `plane-app --update-installer` updates the installer and the `plane-app` utility.
1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio. 3. `plane-app --uninstall` uninstalls the Plane application and all Docker containers from the server but leaves the data stored in
Postgres, Redis, and Minio alone.
1. Plane App can be reinstalled using `plane-app --install`. 4. `plane-app --install` installs the Plane app again.
<ins>Application Data is stored in the mentioned folders</ins>:
1. DB Data: /opt/plane/data/postgres
1. Redis Data: /opt/plane/data/redis
1. Minio Data: /opt/plane/data/minio

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
export * from "./module_filters";
export * from "./modules";

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

View File

@ -0,0 +1,2 @@
export * from "./project_filters";
export * from "./projects";

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -58,6 +58,7 @@ export interface IGroupByKanBan {
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean; isDragStarted?: boolean;
showEmptyGroup?: boolean; showEmptyGroup?: boolean;
subGroupIssueHeaderCount?: (listId: string) => number;
} }
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => { const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
@ -83,6 +84,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
scrollableContainerRef, scrollableContainerRef,
isDragStarted, isDragStarted,
showEmptyGroup = true, showEmptyGroup = true,
subGroupIssueHeaderCount,
} = props; } = props;
const member = useMember(); const member = useMember();
@ -105,44 +107,57 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
if (!list) return null; if (!list) return null;
const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0); const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
const groupList = showEmptyGroup ? list : groupWithIssues;
const visibilityGroupBy = (_list: IGroupByColumn) => {
if (sub_group_by) { if (sub_group_by) {
if (kanbanFilters?.sub_group_by.includes(_list.id)) return true; const groupVisibility = {
return false; showGroup: true,
showIssues: true,
};
if (!showEmptyGroup) {
groupVisibility.showGroup = subGroupIssueHeaderCount ? subGroupIssueHeaderCount(_list.id) > 0 : true;
}
return groupVisibility;
} else { } else {
if (kanbanFilters?.group_by.includes(_list.id)) return true; const groupVisibility = {
return false; showGroup: true,
showIssues: true,
};
if (!showEmptyGroup) {
if ((issueIds as TGroupedIssues)?.[_list.id]?.length > 0) groupVisibility.showGroup = true;
else groupVisibility.showGroup = false;
}
if (kanbanFilters?.group_by.includes(_list.id)) groupVisibility.showIssues = false;
return groupVisibility;
} }
}; };
const isGroupByCreatedBy = group_by === "created_by"; const isGroupByCreatedBy = group_by === "created_by";
return ( return (
<div className={`relative flex w-full gap-2 ${sub_group_by ? "h-full" : "h-full"}`}> <div className={`relative w-full flex gap-2 ${sub_group_by ? "h-full" : "h-full"}`}>
{groupList && {list &&
groupList.length > 0 && list.length > 0 &&
groupList.map((_list: IGroupByColumn) => { list.map((subList: IGroupByColumn) => {
const groupByVisibilityToggle = visibilityGroupBy(_list); const groupByVisibilityToggle = visibilityGroupBy(subList);
if (groupByVisibilityToggle.showGroup === false) return <></>;
return ( return (
<div <div
key={_list.id} key={subList.id}
className={`group relative flex flex-shrink-0 flex-col ${groupByVisibilityToggle ? `` : `w-[350px]`}`} className={`relative flex flex-shrink-0 flex-col group ${
groupByVisibilityToggle.showIssues ? `w-[350px]` : ``
} `}
> >
{sub_group_by === null && ( {sub_group_by === null && (
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1"> <div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
<HeaderGroupByCard <HeaderGroupByCard
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
column_id={_list.id} column_id={subList.id}
icon={_list.icon} icon={subList.icon}
title={_list.name} title={subList.name}
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0} count={(issueIds as TGroupedIssues)?.[subList.id]?.length || 0}
issuePayload={_list.payload} issuePayload={subList.payload}
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy} disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
storeType={storeType} storeType={storeType}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
@ -152,9 +167,9 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
</div> </div>
)} )}
{!groupByVisibilityToggle && ( {groupByVisibilityToggle.showIssues && (
<KanbanGroup <KanbanGroup
groupId={_list.id} groupId={subList.id}
issuesMap={issuesMap} issuesMap={issuesMap}
issueIds={issueIds} issueIds={issueIds}
peekIssueId={peekIssue?.issueId ?? ""} peekIssueId={peekIssue?.issueId ?? ""}
@ -170,7 +185,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
viewId={viewId} viewId={viewId}
disableIssueCreation={disableIssueCreation} disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
groupByVisibilityToggle={groupByVisibilityToggle}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
/> />
@ -208,6 +222,7 @@ export interface IKanBan {
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean; isDragStarted?: boolean;
subGroupIssueHeaderCount?: (listId: string) => number;
} }
export const KanBan: React.FC<IKanBan> = observer((props) => { export const KanBan: React.FC<IKanBan> = observer((props) => {
@ -232,6 +247,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
scrollableContainerRef, scrollableContainerRef,
isDragStarted, isDragStarted,
showEmptyGroup, showEmptyGroup,
subGroupIssueHeaderCount,
} = props; } = props;
const issueKanBanView = useKanbanView(); const issueKanBanView = useKanbanView();
@ -259,6 +275,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
subGroupIssueHeaderCount={subGroupIssueHeaderCount}
/> />
); );
}); });

View File

@ -36,7 +36,7 @@ interface IKanbanGroup {
viewId?: string; viewId?: string;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
groupByVisibilityToggle: boolean; groupByVisibilityToggle?: boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean; isDragStarted?: boolean;
} }

View File

@ -29,6 +29,7 @@ interface ISubGroupSwimlaneHeader {
kanbanFilters: TIssueKanbanFilters; kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
storeType: KanbanStoreType; storeType: KanbanStoreType;
showEmptyGroup: boolean;
} }
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => { const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
@ -39,6 +40,22 @@ const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: st
return headerCount; return headerCount;
}; };
const visibilitySubGroupByGroupCount = (
issueIds: TSubGroupedIssues,
_list: IGroupByColumn,
showEmptyGroup: boolean
): boolean => {
let subGroupHeaderVisibility = true;
if (showEmptyGroup) subGroupHeaderVisibility = true;
else {
if (getSubGroupHeaderIssuesCount(issueIds, _list.id) > 0) subGroupHeaderVisibility = true;
else subGroupHeaderVisibility = false;
}
return subGroupHeaderVisibility;
};
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
issueIds, issueIds,
sub_group_by, sub_group_by,
@ -47,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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -0,0 +1,4 @@
export * from "./date";
export * from "./members";
export * from "./root";
export * from "./status";

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

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

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

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

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

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

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

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

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

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

View File

@ -0,0 +1,2 @@
export * from "./filters";
export * from "./order-by";

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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