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",
"progress_snapshot",
# meta fields
"total_issues",
"is_favorite",
"cancelled_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(
completed_issues=Count(
"issue_module__issue__state__group",
@ -214,6 +223,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"external_source",
"external_id",
# computed fields
"total_issues",
"is_favorite",
"cancelled_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
- Supported CPU Architechture: AMD64 / ARM64 / x86_64 / aarch64
### Downloading Latest Stable Release
Run ↓ on any CLI.
```
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh -
```
<details>
<summary>Downloading Preview Release</summary>
### Download the Preview release
`Preview` builds do not support ARM64, AArch64 CPU architectures
Run ↓ on any CLI.
```
export BRANCH=preview
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh -
```
NOTE: `Preview` builds do not support ARM64/AARCH64 CPU architecture
</details>
--
Expect this after a successful install
![Install Output](images/install.png)
Access the application on a browser via http://server-ip-address
---
### Get Control of your Plane Server Setup
### Successful installation
Plane App is available via the command `plane-app`. Running the command `plane-app --help` helps you to manage Plane
You should see ↓ if there are no hitches. That output will also list the IP address you can use to access your Plane instance.
![Install Output](images/install.png)
---
### Manage your Plane instance
Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of all operators with `plane-app ---help`.
![Plane Help](images/help.png)
<ins>Basic Operations</ins>:
1. Basic operators
1. Start Server using `plane-app start`
1. Stop Server using `plane-app stop`
1. Restart Server using `plane-app restart`
1. `plane-app start` starts the Plane server.
2. `plane-app restart` restarts the Plane server.
3. `plane-app stop` stops the Plane server.
<ins>Advanced Operations</ins>:
2. Advanced operators
1. Configure Plane using `plane-app --configure`. This will give you options to modify
`plane-app --configure` will show advanced configurators.
- NGINX Port (default 80)
- Domain Name (default is the local server public IP address)
- File Upload Size (default 5MB)
- External Postgres DB Url (optional - default empty)
- External Redis URL (optional - default empty)
- AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket)
- Change your proxy or listening port
<br>Default: 80
- Change your domain name
<br>Default: Deployed server's public IP address
- File upload size
<br>Default: 5MB
- Specify external database address when using an external database
<br>Default: `Empty`
<br>`Default folder: /opt/plane/data/postgres`
- Specify external Redis URL when using external Redis
<br>Default: `Empty`
<br>`Default folder: /opt/plane/data/redis`
- Configure AWS S3 bucket
<br>Use only when you or your users want to use S3
<br>`Default folder: /opt/plane/data/minio`
1. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images)
3. Version operators
1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility.
1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio.
1. Plane App can be reinstalled using `plane-app --install`.
<ins>Application Data is stored in the mentioned folders</ins>:
1. DB Data: /opt/plane/data/postgres
1. Redis Data: /opt/plane/data/redis
1. Minio Data: /opt/plane/data/minio
1. `plane-app --upgrade` gets the latest stable version of `docker-compose.yaml`, `.env`, and Docker images
2. `plane-app --update-installer` updates the installer and the `plane-app` utility.
3. `plane-app --uninstall` uninstalls the Plane application and all Docker containers from the server but leaves the data stored in
Postgres, Redis, and Minio alone.
4. `plane-app --install` installs the Plane app again.

View File

@ -1,7 +1,7 @@
export * from "./github-importer";
export * from "./jira-importer";
import { IProjectLite } from "../projects";
import { IProjectLite } from "../project";
// types
import { IUserLite } from "../users";

View File

@ -1,5 +1,5 @@
import { TIssue } from "../issues/base";
import type { IProjectLite } from "../projects";
import type { IProjectLite } from "../project";
export type TInboxIssueExtended = {
completed_at: string | null;

View File

@ -2,10 +2,10 @@ export * from "./users";
export * from "./workspace";
export * from "./cycle";
export * from "./dashboard";
export * from "./projects";
export * from "./project";
export * from "./state";
export * from "./issues";
export * from "./modules";
export * from "./module";
export * from "./views";
export * from "./integration";
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,
IWorkspaceLite,
TStateGroups,
} from ".";
} from "..";
export type TProjectLogoProps = {
in_use: "emoji" | "icon";

View File

@ -103,7 +103,7 @@ export const CustomEmojiIconPicker: React.FC<TCustomEmojiPicker> = (props) => {
style={styles.popper}
{...attributes.popper}
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
)}
>
@ -146,7 +146,7 @@ export const CustomEmojiIconPicker: React.FC<TCustomEmojiPicker> = (props) => {
}}
/>
</Tab.Panel>
<Tab.Panel>
<Tab.Panel className="h-80 w-full">
<IconsList
defaultColor={defaultIconColor}
onChange={(val) => {

View File

@ -29,6 +29,7 @@ interface ITooltipProps {
className?: string;
openDelay?: number;
closeDelay?: number;
isMobile?: boolean;
}
export const Tooltip: React.FC<ITooltipProps> = ({
@ -40,6 +41,7 @@ export const Tooltip: React.FC<ITooltipProps> = ({
className = "",
openDelay = 200,
closeDelay,
isMobile = false,
}) => (
<Tooltip2
disabled={disabled}
@ -47,7 +49,7 @@ export const Tooltip: React.FC<ITooltipProps> = ({
hoverCloseDelay={closeDelay}
content={
<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>}
{tooltipContent}

View File

@ -6,6 +6,8 @@ import { renderFormattedDate } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
// types
import { IApiToken } from "@plane/types";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
type Props = {
handleClose: () => void;
@ -14,7 +16,7 @@ type Props = {
export const GeneratedTokenDetails: React.FC<Props> = (props) => {
const { handleClose, tokenDetails } = props;
const { isMobile } = usePlatformOS();
const copyApiToken = (token: string) => {
copyTextToClipboard(token).then(() =>
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"
>
{tokenDetails.token}
<Tooltip tooltipContent="Copy secret key">
<Tooltip tooltipContent="Copy secret key" isMobile={isMobile}>
<Copy className="h-4 w-4 text-custom-text-400" />
</Tooltip>
</button>

View File

@ -3,6 +3,7 @@ import { XCircle } from "lucide-react";
// components
import { Tooltip } from "@plane/ui";
import { DeleteApiTokenModal } from "components/api-token";
import { usePlatformOS } from "hooks/use-platform-os";
// ui
// helpers
import { renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper";
@ -17,12 +18,14 @@ export const ApiTokenListItem: React.FC<Props> = (props) => {
const { token } = props;
// states
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
// hooks
const { isMobile } = usePlatformOS();
return (
<>
<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">
<Tooltip tooltipContent="Delete token">
<Tooltip tooltipContent="Delete token" isMobile={isMobile}>
<button
onClick={() => setDeleteModalOpen(true)}
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">
<h5 className="truncate text-sm font-medium">{token.label}</h5>
<span
className={`${
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`}
className={`${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`}
>
{token.is_active ? "Active" : "Expired"}
</span>

View File

@ -20,12 +20,11 @@ import {
} from "components/command-palette";
import { ISSUE_DETAILS } from "constants/fetch-keys";
import { useApplication, useEventTracker, useProject } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// services
import useDebounce from "hooks/use-debounce";
import { IssueService } from "services/issue";
import { WorkspaceService } from "services/workspace.service";
// hooks
// components
// types
import { IWorkspaceSearchResults } from "@plane/types";
// fetch-keys
@ -37,6 +36,7 @@ const issueService = new IssueService();
export const CommandModal: React.FC = observer(() => {
// hooks
const { getProjectById } = useProject();
const { isMobile } = usePlatformOS();
// states
const [placeholder, setPlaceholder] = useState("Type a command or search...");
const [resultsCount, setResultsCount] = useState(0);
@ -197,7 +197,7 @@ export const CommandModal: React.FC = observer(() => {
</div>
)}
{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">
<button
type="button"

View File

@ -1,5 +1,6 @@
import Link from "next/link";
import { Tooltip } from "@plane/ui";
import { usePlatformOS } from "hooks/use-platform-os";
type Props = {
label?: string;
@ -9,8 +10,9 @@ type Props = {
export const BreadcrumbLink: React.FC<Props> = (props) => {
const { href, label, icon } = props;
const { isMobile } = usePlatformOS();
return (
<Tooltip tooltipContent={label} position="bottom">
<Tooltip tooltipContent={label} position="bottom" isMobile={isMobile}>
<li className="flex items-center space-x-2" tabIndex={-1}>
<div className="flex flex-wrap items-center gap-2.5">
{href ? (

View File

@ -1,6 +1,7 @@
import { useEffect } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { usePlatformOS } from "hooks/use-platform-os";
// store hooks
// icons
import {
@ -29,9 +30,13 @@ import { IIssueActivity } from "@plane/types";
export const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { isMobile } = usePlatformOS();
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 ? (
<a
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 useDebounce from "hooks/use-debounce";
import { usePlatformOS } from "hooks/use-platform-os";
import { ProjectService } from "services/project";
// ui
// types
@ -40,7 +40,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
const [isSearching, setIsSearching] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
const { isMobile } = usePlatformOS();
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
const handleClose = () => {
@ -154,7 +154,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
</div>
)}
{workspaceLevelToggle && (
<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 text-xs ${
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";
// hooks
import { useMember } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// types
import { ILinkDetails, UserAuth } from "@plane/types";
@ -19,7 +20,7 @@ type Props = {
export const LinksList: React.FC<Props> = observer(({ links, handleDeleteLink, handleEditLink, userAuth }) => {
const { getUserDetails } = useMember();
const { isMobile } = usePlatformOS();
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
const copyToClipboard = (text: string) => {
@ -42,7 +43,7 @@ export const LinksList: React.FC<Props> = observer(({ links, handleDeleteLink, h
<span className="py-1">
<LinkIcon className="h-3 w-3 flex-shrink-0" />
</span>
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url}>
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url} isMobile={isMobile}>
<span
className="cursor-pointer truncate text-xs"
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";
// hooks
import { useCycle, useCycleFilter, useIssues, useMember, useProject } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// ui
import { SingleProgressStats } from "components/core";
import {
@ -46,6 +47,8 @@ interface IActiveCycleDetails {
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
// props
const { workspaceSlug, projectId } = props;
// hooks
const { isMobile } = usePlatformOS();
// store hooks
const {
issues: { fetchActiveCycleIssues },
@ -197,7 +200,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
<span className="h-5 w-5">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-4 w-4" />
</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>
</Tooltip>
</span>
@ -325,6 +328,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
<PriorityIcon priority={issue.priority} withContainer size={12} />
<Tooltip
isMobile={isMobile}
tooltipHeading="Issue ID"
tooltipContent={`${currentProjectDetails?.identifier}-${issue.sequence_id}`}
>
@ -332,7 +336,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
{currentProjectDetails?.identifier}-{issue.sequence_id}
</span>
</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>
</Tooltip>
</div>
@ -345,7 +349,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
buttonVariant="background-with-text"
/>
{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">
<CalendarCheck className="h-3 w-3 flex-shrink-0" />
<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 { capitalizeFirstLetter } from "helpers/string.helper";
// constants
import { DATE_FILTER_OPTIONS } from "constants/filters";
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
type Props = {
editable: boolean | undefined;
@ -18,7 +18,7 @@ export const AppliedDateFilters: React.FC<Props> = observer((props) => {
const getDateLabel = (value: string): string => {
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;
else {

View File

@ -3,6 +3,7 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { useRouter } from "next/router";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
// components
import { Info, Star } from "lucide-react";
import { Avatar, AvatarGroup, Tooltip, LayersIcon, CycleGroupIcon, setPromiseToast } from "@plane/ui";
@ -38,6 +39,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
const { getUserDetails } = useMember();
// computed
const cycleDetails = getCycleById(cycleId);
// hooks
const { isMobile } = usePlatformOS();
if (!cycleDetails) return null;
@ -145,7 +148,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
<span className="flex-shrink-0">
<CycleGroupIcon cycleGroup={cycleStatus as TCycleGroups} className="h-3.5 w-3.5" />
</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>
</Tooltip>
</div>
@ -176,7 +179,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
<span className="text-xs text-custom-text-300">{issueCount}</span>
</div>
{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">
<AvatarGroup showTooltip={false}>
{cycleDetails.assignee_ids.map((assigne_id) => {
@ -190,6 +193,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
</div>
<Tooltip
isMobile={isMobile}
tooltipContent={isNaN(completionPercentage) ? "0" : `${completionPercentage.toFixed(0)}%`}
position="top-left"
>

View File

@ -5,6 +5,7 @@ import { ListFilter, Search, X } from "lucide-react";
// hooks
import { useCycleFilter } from "hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { usePlatformOS } from "hooks/use-platform-os";
// components
import { CycleFiltersSelection } from "components/cycles";
import { FiltersDropdown } from "components/issues";
@ -36,6 +37,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
updateFilters,
updateSearchQuery,
} = useCycleFilter();
const { isMobile } = usePlatformOS();
// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
@ -62,7 +64,10 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
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" />
<input
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"
value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)}
@ -131,7 +136,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
</FiltersDropdown>
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
{CYCLE_VIEW_LAYOUTS.map((layout) => (
<Tooltip key={layout.key} tooltipContent={layout.title}>
<Tooltip key={layout.key} tooltipContent={layout.title} isMobile={isMobile}>
<button
type="button"
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 { FilterHeader, FilterOption } from "components/issues";
// constants
import { DATE_FILTER_OPTIONS } from "constants/filters";
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
type Props = {
appliedFilters: string[] | null;
@ -21,7 +21,9 @@ export const FilterEndDate: React.FC<Props> = observer((props) => {
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 (
<>

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
import { DateFilterModal } from "components/core";
import { FilterHeader, FilterOption } from "components/issues";
// constants
import { DATE_FILTER_OPTIONS } from "constants/filters";
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
type Props = {
appliedFilters: string[] | null;
@ -21,7 +21,9 @@ export const FilterStartDate: React.FC<Props> = observer((props) => {
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 (
<>

View File

@ -1,6 +1,8 @@
import Link from "next/link";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
// ui
import { Tooltip, ContrastIcon } from "@plane/ui";
// helpers
@ -22,7 +24,7 @@ export const CycleGanttBlock: React.FC<Props> = observer((props) => {
const { getCycleById } = useCycle();
// derived values
const cycleDetails = getCycleById(cycleId);
const { isMobile } = usePlatformOS();
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
return (
@ -33,17 +35,18 @@ export const CycleGanttBlock: React.FC<Props> = observer((props) => {
cycleStatus === "current"
? "#09a953"
: cycleStatus === "upcoming"
? "#f7ae59"
: cycleStatus === "completed"
? "#3f76ff"
: cycleStatus === "draft"
? "rgb(var(--color-text-200))"
: "",
? "#f7ae59"
: cycleStatus === "completed"
? "#3f76ff"
: cycleStatus === "draft"
? "rgb(var(--color-text-200))"
: "",
}}
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" />
<Tooltip
isMobile={isMobile}
tooltipContent={
<div className="space-y-1">
<h5>{cycleDetails?.name}</h5>
@ -63,8 +66,6 @@ export const CycleGanttBlock: React.FC<Props> = observer((props) => {
export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
const { cycleId } = props;
// router
const router = useRouter();
// store hooks
const {
router: { workspaceSlug },
@ -76,9 +77,9 @@ export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
return (
<div
<Link
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
className="h-5 w-5 flex-shrink-0"
@ -86,15 +87,15 @@ export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
cycleStatus === "current"
? "#09a953"
: cycleStatus === "upcoming"
? "#f7ae59"
: cycleStatus === "completed"
? "#3f76ff"
: cycleStatus === "draft"
? "rgb(var(--color-text-200))"
: ""
? "#f7ae59"
: cycleStatus === "completed"
? "#3f76ff"
: cycleStatus === "draft"
? "rgb(var(--color-text-200))"
: ""
}`}
/>
<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 { useRouter } from "next/router";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
import { Check, Info, Star, User2 } from "lucide-react";
import { Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarGroup, Avatar, setPromiseToast } from "@plane/ui";
import { CycleQuickActions } from "components/cycles";
@ -33,6 +34,8 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
const { cycleId, workspaceSlug, projectId } = props;
// router
const router = useRouter();
// hooks
const { isMobile } = usePlatformOS();
// store hooks
const { captureEvent } = useEventTracker();
const {
@ -164,7 +167,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
<div className="relative flex items-center gap-2.5 overflow-hidden">
<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">
{cycleDetails.name}
</span>
@ -196,7 +199,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
</div>
<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">
{cycleDetails.assignee_ids?.length > 0 ? (
<AvatarGroup showTooltip={false}>

View File

@ -4,7 +4,7 @@ import { cn } from "helpers/common.helper";
// types
import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS } from "./constants";
import { TButtonVariants } from "./types";
// constants
import { usePlatformOS } from "hooks/use-platform-os";
export type DropdownButtonProps = {
children: React.ReactNode;
@ -27,7 +27,6 @@ type ButtonProps = {
export const DropdownButton: React.FC<DropdownButtonProps> = (props) => {
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip, variant } = props;
const ButtonToRender: React.FC<ButtonProps> = BORDER_BUTTON_VARIANTS.includes(variant)
? BorderButton
: BACKGROUND_BUTTON_VARIANTS.includes(variant)
@ -49,9 +48,10 @@ export const DropdownButton: React.FC<DropdownButtonProps> = (props) => {
const BorderButton: React.FC<ButtonProps> = (props) => {
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
const { isMobile } = usePlatformOS();
return (
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip} isMobile={isMobile}>
<div
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",
@ -67,9 +67,9 @@ const BorderButton: React.FC<ButtonProps> = (props) => {
const BackgroundButton: React.FC<ButtonProps> = (props) => {
const { children, className, tooltipContent, tooltipHeading, showTooltip } = props;
const { isMobile } = usePlatformOS();
return (
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip} isMobile={isMobile}>
<div
className={cn(
"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 { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
const { isMobile } = usePlatformOS();
return (
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}>
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip} isMobile={isMobile}>
<div
className={cn(
"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 { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { usePlatformOS } from "hooks/use-platform-os";
// components
import { DropdownButton } from "../buttons";
// icons
@ -47,6 +48,7 @@ type ButtonContentProps = {
onChange: (moduleIds: string[]) => void;
placeholder: string;
showCount: boolean;
showTooltip?: boolean;
value: string | string[] | null;
};
@ -60,10 +62,12 @@ const ButtonContent: React.FC<ButtonContentProps> = (props) => {
onChange,
placeholder,
showCount,
showTooltip = false,
value,
} = props;
// store hooks
const { getModuleById } = useModule();
const { isMobile } = usePlatformOS();
if (Array.isArray(value))
return (
@ -90,12 +94,12 @@ const ButtonContent: React.FC<ButtonContentProps> = (props) => {
>
{!hideIcon && <DiceIcon className="h-2.5 w-2.5 flex-shrink-0" />}
{!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>
</Tooltip>
)}
{!disabled && (
<Tooltip tooltipContent="Remove">
<Tooltip tooltipContent="Remove" disabled={!showTooltip} isMobile={isMobile}>
<button
type="button"
className="flex-shrink-0"
@ -265,6 +269,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
placeholder={placeholder}
showCount={showCount}
showTooltip={showTooltip}
value={value}
onChange={onChange as any}
/>

View File

@ -9,6 +9,7 @@ import { ISSUE_PRIORITIES } from "constants/issue";
import { cn } from "helpers/common.helper";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { usePlatformOS } from "hooks/use-platform-os";
// icons
// helpers
// types
@ -61,8 +62,10 @@ const BorderButton = (props: ButtonProps) => {
none: "hover:bg-custom-background-80 border-custom-border-300",
};
const { isMobile } = usePlatformOS();
return (
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip} isMobile={isMobile}>
<div
className={cn(
"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",
};
const { isMobile } = usePlatformOS();
return (
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip} isMobile={isMobile}>
<div
className={cn(
"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",
};
const { isMobile } = usePlatformOS();
return (
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}>
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip} isMobile={isMobile}>
<div
className={cn(
"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) => (
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
<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={{
height: `${HEADER_HEIGHT}px`,
}}
@ -55,7 +55,7 @@ export const MonthChartView: FC<any> = observer(() => {
))}
</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) => (
<div
key={`column-${rootIndex}-${index}`}

View File

@ -9,6 +9,8 @@ import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-ti
// types
import { useGanttChart } from "../hooks/use-gantt-chart";
import { IBlockUpdateData, IGanttBlock } from "../types";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
type Props = {
block: IGanttBlock;
@ -23,6 +25,8 @@ export const ChartAddBlock: React.FC<Props> = observer((props) => {
const [buttonStartDate, setButtonStartDate] = useState<Date | null>(null);
// refs
const containerRef = useRef<HTMLDivElement>(null);
// hooks
const { isMobile } = usePlatformOS();
// chart hook
const { currentViewData } = useGanttChart();
@ -73,7 +77,7 @@ export const ChartAddBlock: React.FC<Props> = observer((props) => {
>
<div ref={containerRef} className="h-full w-full" />
{isButtonVisible && (
<Tooltip tooltipContent={buttonStartDate && renderFormattedDate(buttonStartDate)}>
<Tooltip tooltipContent={buttonStartDate && renderFormattedDate(buttonStartDate)} isMobile={isMobile}>
<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"

View File

@ -3,6 +3,8 @@ import { observer } from "mobx-react-lite";
import Link from "next/link";
import { useRouter } from "next/router";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
// components
import { ArrowRight, Plus, PanelRight } from "lucide-react";
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui";
import { ProjectAnalyticsModal } from "components/analytics";
@ -26,7 +28,6 @@ import {
useIssues,
} from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
// components
// ui
// icons
// helpers
@ -43,6 +44,7 @@ const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
const { getCycleById } = useCycle();
// derived values
const cycle = getCycleById(cycleId);
//
if (!cycle) return null;
@ -84,6 +86,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
const {
project: { projectMemberIds },
} = useMember();
const { isMobile } = usePlatformOS()
const activeLayout = issueFilters?.displayFilters?.layout;
@ -207,6 +210,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
<p className="truncate">{cycleDetails?.name && cycleDetails.name}</p>
{issueCount && issueCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issueCount} ${
issueCount > 1 ? "issues" : "issue"
} in this cycle`}

View File

@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
import Link from "next/link";
import { useRouter } from "next/router";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
import { List, PlusIcon, Sheet } from "lucide-react";
import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui";
import { BreadcrumbLink } from "components/common";
@ -46,6 +47,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
const {
workspace: { workspaceMemberIds },
} = useMember();
const { isMobile } = usePlatformOS();
const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined;
@ -133,7 +135,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
{GLOBAL_VIEW_LAYOUTS.map((layout) => (
<Link key={layout.key} href={`/${workspaceSlug}/${layout.link}`}>
<span>
<Tooltip tooltipContent={layout.title}>
<Tooltip tooltipContent={layout.title} isMobile={isMobile}>
<div
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
activeLayout === layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""

View File

@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
import Link from "next/link";
import { useRouter } from "next/router";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
import { ArrowRight, PanelRight, Plus } from "lucide-react";
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui";
import { ProjectAnalyticsModal } from "components/analytics";
@ -66,6 +67,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query;
// hooks
const { isMobile } = usePlatformOS();
// store hooks
const {
issuesFilter: { issueFilters },
@ -208,6 +211,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
<p className="truncate">{moduleDetails?.name && moduleDetails.name}</p>
{issueCount && issueCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issueCount} ${
issueCount > 1 ? "issues" : "issue"
} in this module`}

View File

@ -1,24 +1,36 @@
import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// icons
import { GanttChartSquare, LayoutGrid, List, Plus } from "lucide-react";
// ui
import { Breadcrumbs, Button, Tooltip, DiceIcon, CustomMenu } from "@plane/ui";
import { GanttChartSquare, LayoutGrid, List, ListFilter, Plus, Search, X } from "lucide-react";
// hooks
import { useApplication, useEventTracker, useMember, useModuleFilter, useProject, useUser } from "hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { ProjectLogo } from "components/project";
// constants
import { MODULE_VIEW_LAYOUTS } from "constants/module";
import { EUserProjectRoles } from "constants/project";
// hooks
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
import { ProjectLogo } from "components/project";
import { usePlatformOS } from "hooks/use-platform-os";
import { ModuleFiltersSelection, ModuleOrderByDropdown } from "components/modules";
import { FiltersDropdown } from "components/issues";
// ui
import { Breadcrumbs, Button, Tooltip, DiceIcon, CustomMenu } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// types
import { TModuleFilters } from "@plane/types";
export const ModulesListHeader: React.FC = observer(() => {
// states
const [isSearchOpen, setIsSearchOpen] = useState(false);
// refs
const inputRef = useRef<HTMLInputElement>(null);
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const { workspaceSlug, projectId } = router.query;
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
const { setTrackElement } = useEventTracker();
@ -26,11 +38,56 @@ export const ModulesListHeader: React.FC = observer(() => {
membership: { currentProjectRole },
} = useUser();
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 =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
return (
<div>
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
@ -62,26 +119,97 @@ export const ModulesListHeader: React.FC = observer(() => {
</div>
</div>
<div className="flex items-center gap-2">
<div className="items-center gap-1 rounded bg-custom-background-80 p-1 hidden md:flex">
{MODULE_VIEW_LAYOUTS.map((layout) => (
<Tooltip key={layout.key} tooltipContent={layout.title}>
<div className="flex items-center">
{!isSearchOpen && (
<button
type="button"
className="-mr-1 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
placeholder="Search"
value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
modulesView == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
}`}
onClick={() => setModulesView(layout.key)}
className="grid place-items-center"
onClick={() => {
// updateSearchQuery("");
setIsSearchOpen(false);
}}
>
<X className="h-3 w-3" />
</button>
)}
</div>
</div>
<div className="hidden md:flex items-center gap-1 rounded bg-custom-background-80 p-1">
{MODULE_VIEW_LAYOUTS.map((layout) => (
<Tooltip key={layout.key} tooltipContent={layout.title} isMobile={isMobile}>
<button
type="button"
className={cn(
"group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100",
{
"bg-custom-background-100 shadow-custom-shadow-2xs": displayFilters?.layout === layout.key,
}
)}
onClick={() => {
if (!projectId) return;
updateDisplayFilters(projectId.toString(), { layout: layout.key });
}}
>
<layout.icon
strokeWidth={2}
className={`h-3.5 w-3.5 ${
modulesView == layout.key ? "text-custom-text-100" : "text-custom-text-200"
}`}
className={cn("h-3.5 w-3.5 text-custom-text-200", {
"text-custom-text-100": displayFilters?.layout === layout.key,
})}
/>
</button>
</Tooltip>
))}
</div>
<ModuleOrderByDropdown
value={displayFilters?.order_by}
onChange={(val) => {
if (!projectId || val === displayFilters?.order_by) return;
updateDisplayFilters(projectId.toString(), {
order_by: val,
});
}}
/>
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
<ModuleFiltersSelection
displayFilters={displayFilters ?? {}}
filters={filters ?? {}}
handleDisplayFiltersUpdate={(val) => {
if (!projectId) return;
updateDisplayFilters(projectId.toString(), val);
}}
handleFiltersUpdate={handleFilters}
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
{canUserCreateModule && (
<Button
variant="primary"
@ -104,9 +232,9 @@ export const ModulesListHeader: React.FC = observer(() => {
// placement="bottom-start"
customButton={
<span className="flex items-center gap-2">
{modulesView === "gantt_chart" ? (
{displayFilters?.layout === "gantt" ? (
<GanttChartSquare className="w-3 h-3" />
) : modulesView === "grid" ? (
) : displayFilters?.layout === "board" ? (
<LayoutGrid 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) => (
<CustomMenu.MenuItem
key={layout.key}
onClick={() => setModulesView(layout.key)}
onClick={() => {
if (!projectId) return;
updateDisplayFilters(projectId.toString(), { layout: layout.key });
}}
className="flex items-center gap-2"
>
<layout.icon className="w-3 h-3" />

View File

@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { ArrowLeft } from "lucide-react";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
// constants
// ui
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
@ -31,10 +32,10 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
const {
project: { projectMemberIds },
} = useMember();
// for archived issues list layout is the only option
const activeLayout = "list";
// hooks
const { isMobile } = usePlatformOS();
const handleFiltersUpdate = (key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
@ -119,6 +120,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
</Breadcrumbs>
{issueCount && issueCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in project's archived`}
position="bottom"
>

View File

@ -2,6 +2,7 @@ import { FC, useCallback } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
// components
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
import { BreadcrumbLink } from "components/common";
@ -28,7 +29,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
const {
project: { projectMemberIds },
} = useMember();
const { isMobile } = usePlatformOS();
const activeLayout = issueFilters?.displayFilters?.layout;
const handleFiltersUpdate = useCallback(
@ -112,6 +113,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
</Breadcrumbs>
{issueCount && issueCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in project's draft`}
position="bottom"
>

View File

@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
import { ProjectAnalyticsModal } from "components/analytics";
import { BreadcrumbLink } from "components/common";
@ -52,7 +53,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
const { isMobile } = usePlatformOS();
const activeLayout = issueFilters?.displayFilters?.layout;
const handleFiltersUpdate = useCallback(
@ -153,6 +154,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
</Breadcrumbs>
{issueCount && issueCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in this project`}
position="bottom"
>

View File

@ -1,26 +1,81 @@
import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Search, Plus, Briefcase } from "lucide-react";
import { Search, Plus, Briefcase, X, ListFilter } from "lucide-react";
// hooks
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// constants
import { useApplication, useEventTracker, useMember, useProject, useProjectFilter, useUser } from "hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
// components
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
import { FiltersDropdown } from "components/issues";
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "components/project";
import { TProjectFilters } from "@plane/types";
export const ProjectsHeader = observer(() => {
// states
const [isSearchOpen, setIsSearchOpen] = useState(false);
// refs
const inputRef = useRef<HTMLInputElement>(null);
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
const {
commandPalette: commandPaletteStore,
router: { workspaceSlug },
} = useApplication();
const { setTrackElement } = useEventTracker();
const {
membership: { currentWorkspaceRole },
} = 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 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 (
<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">
@ -34,18 +89,74 @@ export const ProjectsHeader = observer(() => {
</Breadcrumbs>
</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 && (
<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">
<Search className="h-3.5" />
<input
className="border-none w-full bg-transparent text-sm focus:outline-none"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search"
/>
<div className="flex items-center">
{!isSearchOpen && (
<button
type="button"
className="-mr-1 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 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>
)}
<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 && (
<Button
prependIcon={<Plus />}
@ -54,9 +165,9 @@ export const ProjectsHeader = observer(() => {
setTrackElement("Projects page");
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>
)}
</div>

View File

@ -5,6 +5,7 @@ import { useRouter } from "next/router";
// icons
import { CalendarDays } from "lucide-react";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
// ui
import { Tooltip, PriorityIcon } from "@plane/ui";
// helpers
@ -33,7 +34,7 @@ export const InboxIssueListItem: FC<TInboxIssueListItem> = observer((props) => {
const {
issue: { getIssueById },
} = useIssueDetail();
const { isMobile } = usePlatformOS();
const inboxIssueDetail = getInboxIssueByIssueId(inboxId, issueId);
const issue = getIssueById(issueId);
@ -83,10 +84,10 @@ export const InboxIssueListItem: FC<TInboxIssueListItem> = observer((props) => {
</div>
<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" />
</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">
<CalendarDays size={12} strokeWidth={1.5} />
<span>{renderFormattedDate(issue.created_at ?? "")}</span>

View File

@ -9,6 +9,7 @@ import { Menu, Transition } from "@headlessui/react";
// icons
import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
import { Avatar, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
import { useApplication, useUser } from "hooks/store";
// ui
@ -34,7 +35,7 @@ export const InstanceSidebarDropdown = observer(() => {
const { signOut, currentUser, currentUserSettings } = useUser();
// hooks
const { setTheme } = useTheme();
const { isMobile } = usePlatformOS();
// redirect url for normal mode
const redirectWorkspaceSlug =
workspaceSlug ||
@ -73,7 +74,7 @@ export const InstanceSidebarDropdown = observer(() => {
{!sidebarCollapsed && (
<div className="flex w-full gap-2">
<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">
<Link href={`/${redirectWorkspaceSlug}`}>
<span>

View File

@ -2,6 +2,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
import { Tooltip } from "@plane/ui";
import { useApplication } from "hooks/store";
// ui
@ -46,6 +47,7 @@ export const InstanceAdminSidebarMenu = () => {
} = useApplication();
// router
const router = useRouter();
const { isMobile } = usePlatformOS();
return (
<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 (
<Link key={index} href={item.href}>
<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
className={`group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none ${
isActive

View File

@ -11,6 +11,7 @@ import { WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
// hooks
import { useApplication, useUser } from "hooks/store";
import useIntegrationPopup from "hooks/use-integration-popup";
import { usePlatformOS } from "hooks/use-platform-os";
// services
import { IntegrationService } from "services/integrations";
// icons
@ -55,7 +56,7 @@ export const SingleIntegrationCard: React.FC<Props> = observer(({ integration })
} = useUser();
const isUserAdmin = currentWorkspaceRole === 20;
const { isMobile } = usePlatformOS();
const { startAuth, isConnecting: isInstalling } = useIntegrationPopup({
provider: integration.provider,
github_app_name: envConfig?.github_app_name || "",
@ -129,6 +130,7 @@ export const SingleIntegrationCard: React.FC<Props> = observer(({ integration })
{workspaceIntegrations ? (
isInstalled ? (
<Tooltip
isMobile={isMobile}
disabled={isUserAdmin}
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
isMobile={isMobile}
disabled={isUserAdmin}
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 { AlertCircle, X } from "lucide-react";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
// ui
import { Tooltip } from "@plane/ui";
// components
@ -34,7 +35,7 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = (props) => {
} = useIssueDetail();
// states
const [attachmentDeleteModal, setAttachmentDeleteModal] = useState<boolean>(false);
const { isMobile } = usePlatformOS();
const attachment = attachmentId && getAttachmentById(attachmentId);
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="flex flex-col gap-1">
<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>
</Tooltip>
<Tooltip
isMobile={isMobile}
tooltipContent={`${
getUserDetails(attachment.updated_by)?.display_name ?? ""
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}

View File

@ -4,6 +4,7 @@ import { Network } from "lucide-react";
import { Tooltip } from "@plane/ui";
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper";
import { useIssueDetail } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// ui
// components
import { IssueUser } from "../";
@ -25,7 +26,7 @@ export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (pr
} = useIssueDetail();
const activity = getActivityById(activityId);
const { isMobile } = usePlatformOS();
if (!activity) return <></>;
return (
<div
@ -42,6 +43,7 @@ export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (pr
<span> {children} </span>
<span>
<Tooltip
isMobile={isMobile}
tooltipContent={`${renderFormattedDate(activity.created_at)}, ${renderFormattedTime(activity.created_at)}`}
>
<span className="whitespace-nowrap"> {calculateTimeAgo(activity.created_at)}</span>

View File

@ -2,6 +2,7 @@ import { FC } from "react";
// hooks
import { Tooltip } from "@plane/ui";
import { useIssueDetail } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// ui
type TIssueLink = {
@ -14,12 +15,12 @@ export const IssueLink: FC<TIssueLink> = (props) => {
const {
activity: { getActivityById },
} = useIssueDetail();
const { isMobile } = usePlatformOS();
const activity = getActivityById(activityId);
if (!activity) 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
aria-disabled={activity.issue === null}
href={`${

View File

@ -1,4 +1,5 @@
import { FC } from "react";
import { observer } from "mobx-react";
// components
import { useIssueDetail } from "hooks/store";
import { LabelListItem } from "./label-list-item";
@ -14,7 +15,7 @@ type TLabelList = {
disabled: boolean;
};
export const LabelList: FC<TLabelList> = (props) => {
export const LabelList: FC<TLabelList> = observer((props) => {
const { workspaceSlug, projectId, issueId, labelOperations, disabled } = props;
// hooks
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 { copyTextToClipboard } from "helpers/string.helper";
import { useIssueDetail, useMember } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal";
export type TIssueLinkDetail = {
@ -33,7 +34,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
toggleIssueLinkModalStore(modalToggle);
setIsIssueLinkModalOpen(modalToggle);
};
const { isMobile } = usePlatformOS();
const linkDetail = getLinkById(linkId);
if (!linkDetail) return <></>;
@ -64,7 +65,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
<span className="py-1">
<LinkIcon className="h-3 w-3 flex-shrink-0" />
</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">
{linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url}
</span>

View File

@ -71,7 +71,6 @@ export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props)
hideIcon
dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
showTooltip
multiple
/>
</div>

View File

@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
import Link from "next/link";
import { Pencil, X } from "lucide-react";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
// components
import { Tooltip } from "@plane/ui";
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 parentIssueProjectDetails =
parentIssue && parentIssue.project_id ? getProjectById(parentIssue.project_id) : undefined;
const { isMobile } = usePlatformOS();
const handleParentIssue = async (_issueId: string | null = null) => {
try {
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 ? (
<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
href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`}
target="_blank"
@ -86,7 +87,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
</Tooltip>
{!disabled && (
<Tooltip tooltipContent="Remove" position="bottom">
<Tooltip tooltipContent="Remove" position="bottom" isMobile={isMobile}>
<span
onClick={(e) => {
e.preventDefault();

View File

@ -1,10 +1,11 @@
import { FC, useMemo } from "react";
import { observer } from "mobx-react-lite";
// components
import { TOAST_TYPE, setToast } from "@plane/ui";
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
import { renderEmoji } from "helpers/emoji.helper";
import { useIssueDetail } from "hooks/store";
// ui
import { useIssueDetail, useMember } from "hooks/store";
// helper
import { formatTextList } from "helpers/issue.helper";
// types
import { IUser } from "@plane/types";
import { ReactionSelector } from "./reaction-selector";
@ -21,10 +22,11 @@ export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props)
// hooks
const {
commentReaction: { getCommentReactionsByCommentId, commentReactionsByUser },
commentReaction: { getCommentReactionsByCommentId, commentReactionsByUser, getCommentReactionById },
createCommentReaction,
removeCommentReaction,
} = useIssueDetail();
const { getUserDetails } = useMember();
const reactionIds = getCommentReactionsByCommentId(commentId);
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]
);
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 (
<div className="mt-4 relative flex items-center gap-1.5">
<ReactionSelector
@ -87,19 +100,21 @@ export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props)
(reaction) =>
reactionIds[reaction]?.length > 0 && (
<>
<button
type="button"
onClick={() => issueCommentReactionOperations.react(reaction)}
key={reaction}
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" : ""}>
{(reactionIds || {})[reaction].length}{" "}
</span>
</button>
<Tooltip tooltipContent={getReactionUsers(reaction)}>
<button
type="button"
onClick={() => issueCommentReactionOperations.react(reaction)}
key={reaction}
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" : ""}>
{(reactionIds || {})[reaction].length}{" "}
</span>
</button>
</Tooltip>
</>
)
)}

View File

@ -1,10 +1,12 @@
import { FC, useMemo } from "react";
import { observer } from "mobx-react-lite";
// components
import { TOAST_TYPE, setToast } from "@plane/ui";
import { renderEmoji } from "helpers/emoji.helper";
import { useIssueDetail } from "hooks/store";
// hooks
import { useIssueDetail, useMember } from "hooks/store";
// ui
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
import { formatTextList } from "helpers/issue.helper";
// types
import { IUser } from "@plane/types";
import { ReactionSelector } from "./reaction-selector";
@ -20,10 +22,11 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
const { workspaceSlug, projectId, issueId, currentUser } = props;
// hooks
const {
reaction: { getReactionsByIssueId, reactionsByUser },
reaction: { getReactionsByIssueId, reactionsByUser, getReactionById },
createReaction,
removeReaction,
} = useIssueDetail();
const { getUserDetails } = useMember();
const reactionIds = getReactionsByIssueId(issueId);
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]
);
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 (
<div className="mt-4 relative flex items-center gap-1.5">
<ReactionSelector size="md" position="top" value={userReactions} onSelect={issueReactionOperations.react} />
@ -81,19 +96,21 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
(reaction) =>
reactionIds[reaction]?.length > 0 && (
<>
<button
type="button"
onClick={() => issueReactionOperations.react(reaction)}
key={reaction}
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" : ""}>
{(reactionIds || {})[reaction].length}{" "}
</span>
</button>
<Tooltip tooltipContent={getReactionUsers(reaction)}>
<button
type="button"
onClick={() => issueReactionOperations.react(reaction)}
key={reaction}
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" : ""}>
{(reactionIds || {})[reaction].length}{" "}
</span>
</button>
</Tooltip>
</>
)
)}

View File

@ -7,6 +7,7 @@ import { RelatedIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
import { ExistingIssuesListModal } from "components/core";
import { cn } from "helpers/common.helper";
import { useIssueDetail, useIssues, useProject } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// components
// ui
// helpers
@ -59,7 +60,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
toggleRelationModal,
} = useIssueDetail();
const { issueMap } = useIssues();
const { isMobile } = usePlatformOS();
const relationIssueIds = getRelationByIssueIdRelationType(issueId, relationKey);
const onSubmit = async (data: ISearchIssueResponse[]) => {
@ -124,7 +125,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
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}`}
>
<Tooltip tooltipHeading="Title" tooltipContent={currentIssue.name}>
<Tooltip tooltipHeading="Title" tooltipContent={currentIssue.name} isMobile={isMobile}>
<Link
href={`/${workspaceSlug}/projects/${projectDetails?.id}/issues/${currentIssue.id}`}
target="_blank"
@ -136,7 +137,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
</Link>
</Tooltip>
{!disabled && (
<Tooltip tooltipContent="Remove" position="bottom">
<Tooltip tooltipContent="Remove" position="bottom" isMobile={isMobile}>
<span
onClick={(e) => {
e.preventDefault();

View File

@ -376,7 +376,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
/>
</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` } : {}}
>
<IssueDetailsSidebar

View File

@ -15,6 +15,7 @@ import {
CalendarCheck2,
} from "lucide-react";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
// components
import {
ArchiveIcon,
@ -78,7 +79,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
issue: { getIssueById },
} = useIssueDetail();
const { getStateById } = useProjectState();
const { isMobile } = usePlatformOS();
const issue = getIssueById(issueId);
if (!issue) return <></>;
@ -138,7 +139,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
)}
<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
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"
@ -149,6 +150,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
</Tooltip>
{isArchivingAllowed && (
<Tooltip
isMobile={isMobile}
tooltipContent={isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"}
>
<button
@ -170,7 +172,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
</Tooltip>
)}
{is_editable && (
<Tooltip tooltipContent="Delete">
<Tooltip tooltipContent="Delete" isMobile={isMobile}>
<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"

View File

@ -8,6 +8,7 @@ import { Tooltip, ControlLink } from "@plane/ui";
import { cn } from "helpers/common.helper";
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { usePlatformOS } from "hooks/use-platform-os";
// helpers
// types
import { TIssue, TIssueMap } from "@plane/types";
@ -29,6 +30,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
const { getProjectIdentifierById } = useProject();
const { getProjectStates } = useProjectState();
const { peekIssue, setPeekIssue } = useIssueDetail();
const { isMobile } = usePlatformOS();
// states
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">
{getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
</div>
<Tooltip tooltipContent={issue.name}>
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
<div className="truncate text-xs">{issue.name}</div>
</Tooltip>
</div>

View File

@ -2,7 +2,7 @@ import { observer } from "mobx-react-lite";
// icons
import { X } from "lucide-react";
// helpers
import { DATE_FILTER_OPTIONS } from "constants/filters";
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
import { renderFormattedDate } from "helpers/date-time.helper";
import { capitalizeFirstLetter } from "helpers/string.helper";
// constants
@ -18,7 +18,7 @@ export const AppliedDateFilters: React.FC<Props> = observer((props) => {
const getDateLabel = (value: string): string => {
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;
else {

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
import { DateFilterModal } from "components/core";
import { FilterHeader, FilterOption } from "components/issues";
// constants
import { DATE_FILTER_OPTIONS } from "constants/filters";
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
type Props = {
appliedFilters: string[] | null;
@ -21,7 +21,9 @@ export const FilterStartDate: React.FC<Props> = observer((props) => {
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 (
<>

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
import { DateFilterModal } from "components/core";
import { FilterHeader, FilterOption } from "components/issues";
// constants
import { DATE_FILTER_OPTIONS } from "constants/filters";
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
type Props = {
appliedFilters: string[] | null;
@ -21,7 +21,9 @@ export const FilterTargetDate: React.FC<Props> = observer((props) => {
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 (
<>

View File

@ -5,7 +5,8 @@ import { Tooltip } from "@plane/ui";
// types
import { ISSUE_LAYOUTS } from "constants/issue";
import { TIssueLayouts } from "@plane/types";
// constants
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
type Props = {
layouts: TIssueLayouts[];
@ -15,11 +16,12 @@ type Props = {
export const LayoutSelection: React.FC<Props> = (props) => {
const { layouts, onChange, selectedLayout } = props;
const { isMobile } = usePlatformOS();
return (
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
{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
type="button"
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";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
// ui
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
// helpers
@ -31,6 +32,7 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
issueDetails &&
!issueDetails.tempId &&
setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id });
const { isMobile } = usePlatformOS();
return (
<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" />
<Tooltip
isMobile={isMobile}
tooltipContent={
<div className="space-y-1">
<h5>{issueDetails?.name}</h5>
@ -83,6 +86,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
workspaceSlug &&
issueDetails &&
setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id });
const { isMobile } = usePlatformOS();
return (
<ControlLink
@ -97,7 +101,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
<div className="flex-shrink-0 text-xs text-custom-text-300">
{projectIdentifier} {issueDetails?.sequence_id}
</div>
<Tooltip tooltipContent={issueDetails?.name}>
<Tooltip tooltipContent={issueDetails?.name} isMobile={isMobile}>
<span className="flex-grow truncate text-sm font-medium">{issueDetails?.name}</span>
</Tooltip>
</div>

View File

@ -6,6 +6,7 @@ import { Tooltip, ControlLink } from "@plane/ui";
import RenderIfVisible from "components/core/render-if-visible-HOC";
import { cn } from "helpers/common.helper";
import { useApplication, useIssueDetail, useProject } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// components
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
import { IssueProperties } from "../properties/all-properties";
@ -41,6 +42,7 @@ interface IssueDetailsBlockProps {
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props: IssueDetailsBlockProps) => {
const { issue, updateIssue, quickActions, isReadOnly, displayProperties } = props;
// hooks
const { isMobile } = usePlatformOS();
const { getProjectIdentifierById } = useProject();
const {
router: { workspaceSlug },
@ -66,7 +68,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
</WithDisplayPropertiesHOC>
{issue?.is_draft ? (
<Tooltip tooltipContent={issue.name}>
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
<span>{issue.name}</span>
</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"
disabled={!!issue?.tempId}
>
<Tooltip tooltipContent={issue.name}>
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
<span>{issue.name}</span>
</Tooltip>
</ControlLink>

View File

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

View File

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

View File

@ -29,6 +29,7 @@ interface ISubGroupSwimlaneHeader {
kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
storeType: KanbanStoreType;
showEmptyGroup: boolean;
}
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
@ -39,6 +40,22 @@ const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: st
return headerCount;
};
const visibilitySubGroupByGroupCount = (
issueIds: TSubGroupedIssues,
_list: IGroupByColumn,
showEmptyGroup: boolean
): boolean => {
let subGroupHeaderVisibility = true;
if (showEmptyGroup) subGroupHeaderVisibility = true;
else {
if (getSubGroupHeaderIssuesCount(issueIds, _list.id) > 0) subGroupHeaderVisibility = true;
else subGroupHeaderVisibility = false;
}
return subGroupHeaderVisibility;
};
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
issueIds,
sub_group_by,
@ -47,26 +64,37 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
list,
kanbanFilters,
handleKanbanFilters,
showEmptyGroup,
}) => (
<div className="relative flex h-max min-h-full w-full items-center gap-2">
{list &&
list.length > 0 &&
list.map((_list: IGroupByColumn) => (
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={_list.id}
icon={_list.icon}
title={_list.name}
count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
issuePayload={_list.payload}
storeType={storeType}
/>
</div>
))}
list.map((_list: IGroupByColumn) => {
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(
issueIds as TSubGroupedIssues,
_list,
showEmptyGroup
);
if (subGroupByVisibilityToggle === false) return <></>;
return (
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={_list.id}
icon={_list.icon}
title={_list.name}
count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
issuePayload={_list.payload}
storeType={storeType}
/>
</div>
);
})}
</div>
);
@ -127,53 +155,74 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
return issueCount;
};
const visibilitySubGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
const subGroupVisibility = {
showGroup: true,
showIssues: true,
};
if (showEmptyGroup) subGroupVisibility.showGroup = true;
else {
if (calculateIssueCount(_list.id) > 0) subGroupVisibility.showGroup = true;
else subGroupVisibility.showGroup = false;
}
if (kanbanFilters?.sub_group_by.includes(_list.id)) subGroupVisibility.showIssues = false;
return subGroupVisibility;
};
return (
<div className="relative h-max min-h-full w-full">
{list &&
list.length > 0 &&
list.map((_list: any) => (
<div key={_list.id} className="flex flex-shrink-0 flex-col">
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
<HeaderSubGroupByCard
column_id={_list.id}
icon={_list.Icon}
title={_list.name || ""}
count={calculateIssueCount(_list.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
/>
list.map((_list: any) => {
const subGroupByVisibilityToggle = visibilitySubGroupBy(_list);
if (subGroupByVisibilityToggle.showGroup === false) return <></>;
return (
<div key={_list.id} className="flex flex-shrink-0 flex-col">
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
<HeaderSubGroupByCard
column_id={_list.id}
icon={_list.Icon}
title={_list.name || ""}
count={calculateIssueCount(_list.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
/>
</div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div>
{!kanbanFilters?.sub_group_by.includes(_list.id) && (
<div className="relative">
<KanBan
issuesMap={issuesMap}
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={_list.id}
updateIssue={updateIssue}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
showEmptyGroup={showEmptyGroup}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback}
viewId={viewId}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
storeType={storeType}
/>
</div>
)}
</div>
))}
{subGroupByVisibilityToggle.showIssues && (
<div className="relative">
<KanBan
issuesMap={issuesMap}
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={_list.id}
storeType={storeType}
updateIssue={updateIssue}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
showEmptyGroup={showEmptyGroup}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback}
viewId={viewId}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
subGroupIssueHeaderCount={(groupByListId: string) =>
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
}
/>
</div>
)}
</div>
);
})}
</div>
);
});
@ -267,6 +316,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
handleKanbanFilters={handleKanbanFilters}
list={groupByList}
storeType={storeType}
showEmptyGroup={showEmptyGroup}
/>
</div>

View File

@ -1,11 +1,12 @@
import { observer } from "mobx-react-lite";
// components
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
import { useApplication, useIssueDetail, useProject } from "hooks/store";
// ui
import { Spinner, Tooltip, ControlLink } from "@plane/ui";
// helper
import { cn } from "helpers/common.helper";
import { useApplication, useIssueDetail, useProject } from "hooks/store";
// types
import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types";
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 });
const issue = issuesMap[issueId];
const { isMobile } = usePlatformOS();
if (!issue) return null;
const canEditIssueProperties = canEditProperties(issue.project_id);
@ -65,7 +66,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
)}
{issue?.is_draft ? (
<Tooltip tooltipContent={issue.name}>
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
<span>{issue.name}</span>
</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"
disabled={!!issue?.tempId}
>
<Tooltip tooltipContent={issue.name}>
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
<span>{issue.name}</span>
</Tooltip>
</ControlLink>

View File

@ -20,6 +20,7 @@ import { cn } from "helpers/common.helper";
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// components
import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types";
import { IssuePropertyLabels } from "../properties/labels";
@ -50,6 +51,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
} = useIssues(EIssuesStoreType.CYCLE);
const { areEstimatesEnabledForCurrentProject } = useEstimate();
const { getStateById } = useProjectState();
const { isMobile } = usePlatformOS();
// router
const router = useRouter();
const { workspaceSlug } = router.query;
@ -391,7 +393,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
displayPropertyKey="sub_issue_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
onClick={issue.sub_issues_count ? redirectToIssueDetail : () => {}}
className={cn(
@ -413,7 +415,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
displayPropertyKey="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">
<Paperclip className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<div className="text-xs">{issue.attachment_count}</div>
@ -427,7 +429,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
displayPropertyKey="link"
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">
<Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<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 { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { usePlatformOS } from "hooks/use-platform-os";
// components
// types
import { IIssueLabel } from "@plane/types";
@ -62,7 +63,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
router: { workspaceSlug },
} = useApplication();
const { fetchProjectLabels, getProjectLabels } = useLabel();
const { isMobile } = usePlatformOS();
const storeLabels = getProjectLabels(projectId);
const onOpen = () => {
@ -149,7 +150,13 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
{projectLabels
?.filter((l) => value.includes(l?.id))
.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
key={label?.id}
className={`flex overflow-hidden hover:bg-custom-background-80 ${
@ -176,6 +183,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
}`}
>
<Tooltip
isMobile={isMobile}
position="top"
tooltipHeading="Labels"
tooltipContent={projectLabels
@ -191,7 +199,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
</div>
)
) : (
<Tooltip position="top" tooltipHeading="Labels" tooltipContent="None">
<Tooltip position="top" tooltipHeading="Labels" tooltipContent="None" isMobile={isMobile}>
<div
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"
@ -224,8 +232,8 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
disabled
? "cursor-not-allowed text-custom-text-200"
: value.length <= maxRender
? "cursor-pointer"
: "cursor-pointer hover:bg-custom-background-80"
? "cursor-pointer"
: "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
onClick={handleOnClick}
>

View File

@ -14,6 +14,7 @@ import { cn } from "helpers/common.helper";
// hooks
import { useIssueDetail, useProject } from "hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { usePlatformOS } from "hooks/use-platform-os";
// types
import { IIssueDisplayProperties, TIssue } from "@plane/types";
// local components
@ -144,6 +145,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
//hooks
const { getProjectIdentifierById } = useProject();
const { peekIssue, setPeekIssue } = useIssueDetail();
const { isMobile } = usePlatformOS();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
const menuActionRef = useRef<HTMLDivElement | null>(null);
@ -241,7 +243,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
disabled={!!issueDetail?.tempId}
>
<div className="w-full overflow-hidden">
<Tooltip tooltipContent={issueDetail.name}>
<Tooltip tooltipContent={issueDetail.name} isMobile={isMobile}>
<div
className="h-full w-full cursor-pointer truncate px-4 text-left text-[0.825rem] text-custom-text-100 focus:outline-none"
tabIndex={-1}

View File

@ -1,14 +1,15 @@
import React from "react";
// components
import { Tooltip } from "@plane/ui";
import { usePlatformOS } from "hooks/use-platform-os";
type Props = {
labelDetails: any[];
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 <= maxRender ? (
<>
@ -17,7 +18,7 @@ export const ViewIssueLabel: React.FC<Props> = ({ labelDetails, maxRender = 1 })
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"
>
<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">
<span
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">
<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">
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
{`${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 { ProjectService } from "services/project";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
// ui
// icons
// types
@ -37,7 +38,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
const { isMobile } = usePlatformOS();
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
const router = useRouter();
@ -113,7 +114,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
/>
</div>
<div className="flex p-2 sm:justify-end">
<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 text-xs ${
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";
// store hooks
import { useIssueDetail, useProjectState, useUser } from "hooks/store";
// helpers
// components
// helpers
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
export type TPeekModes = "side-peek" | "modal" | "full-screen";
@ -83,6 +82,7 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
issue: { getIssueById },
} = useIssueDetail();
const { getStateById } = useProjectState();
const { isMobile } = usePlatformOS();
// derived values
const issueDetails = getIssueById(issueId);
const stateDetails = issueDetails ? getStateById(issueDetails?.state_id) : undefined;
@ -160,13 +160,14 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
{currentUser && !isArchived && (
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
)}
<Tooltip tooltipContent="Copy link">
<Tooltip tooltipContent="Copy link" isMobile={isMobile}>
<button type="button" onClick={handleCopyText}>
<Link2 className="h-4 w-4 -rotate-45 text-custom-text-300 hover:text-custom-text-200" />
</button>
</Tooltip>
{isArchivingAllowed && (
<Tooltip
isMobile={isMobile}
tooltipContent={isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"}
>
<button
@ -185,14 +186,14 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
</Tooltip>
)}
{isRestoringAllowed && (
<Tooltip tooltipContent="Restore">
<Tooltip tooltipContent="Restore" isMobile={isMobile}>
<button type="button" onClick={handleRestoreIssue}>
<RotateCcw className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
</button>
</Tooltip>
)}
{!disabled && (
<Tooltip tooltipContent="Delete">
<Tooltip tooltipContent="Delete" isMobile={isMobile}>
<button type="button" onClick={() => toggleDeleteIssueModal(true)}>
<Trash2 className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
</button>

View File

@ -93,7 +93,6 @@ export const IssueView: FC<IIssueView> = observer((props) => {
isOpen={isDeleteIssueModalOpen}
handleClose={() => {
toggleDeleteIssueModal(false);
removeRoutePeekId();
}}
data={issue}
onSubmit={() => issueOperations.remove(workspaceSlug, projectId, issueId)}

View File

@ -4,6 +4,7 @@ import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader }
// components
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
import { TIssue } from "@plane/types";
import { IssueList } from "./issues-list";
import { IssueProperty } from "./properties";
@ -46,7 +47,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
} = useIssueDetail();
const project = useProject();
const { getProjectStates } = useProjectState();
const { isMobile } = usePlatformOS();
const issue = getIssueById(issueId);
const projectDetail = (issue && issue.project_id && project.getProjectById(issue.project_id)) || undefined;
const currentIssueStateDetail =
@ -117,7 +118,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
onClick={() => handleIssuePeekOverview(issue)}
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>
</Tooltip>
</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 { useRouter } from "next/router";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
// ui
import { Tooltip, ModuleStatusIcon } from "@plane/ui";
// helpers
@ -24,6 +26,8 @@ export const ModuleGanttBlock: React.FC<Props> = observer((props) => {
const { getModuleById } = useModule();
// derived values
const moduleDetails = getModuleById(moduleId);
// hooks
const { isMobile } = usePlatformOS();
return (
<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" />
<Tooltip
isMobile={isMobile}
tooltipContent={
<div className="space-y-1">
<h5>{moduleDetails?.name}</h5>
@ -54,8 +59,6 @@ export const ModuleGanttBlock: React.FC<Props> = observer((props) => {
export const ModuleGanttSidebarBlock: React.FC<Props> = observer((props) => {
const { moduleId } = props;
// router
const router = useRouter();
// store hooks
const {
router: { workspaceSlug },
@ -65,14 +68,12 @@ export const ModuleGanttSidebarBlock: React.FC<Props> = observer((props) => {
const moduleDetails = getModuleById(moduleId);
return (
<div
<Link
className="relative flex h-full w-full items-center gap-2"
onClick={() =>
router.push(`/${workspaceSlug}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`)
}
href={`/${workspaceSlug}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`}
>
<ModuleStatusIcon status={moduleDetails?.status ?? "backlog"} height="16px" width="16px" />
<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(() => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const { workspaceSlug, projectId } = router.query;
// store
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) => {
if (!workspaceSlug || !module) return;
@ -44,7 +46,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
<GanttChartRoot
title="Modules"
loaderTitle="Modules"
blocks={projectModuleIds ? blockFormat(projectModuleIds) : null}
blocks={filteredModuleIds ? blockFormat(filteredModuleIds) : null}
sidebarToRender={(props) => <ModuleGanttSidebar {...props} />}
blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)}
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 "./sidebar-select";
export * from "./delete-module-modal";

View File

@ -12,6 +12,7 @@ import { EUserProjectRoles } from "constants/project";
import { renderFormattedDate } from "helpers/date-time.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
import { useEventTracker, useMember, useModule, useUser } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// components
// ui
// helpers
@ -39,7 +40,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
// derived values
const moduleDetails = getModuleById(moduleId);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const { isMobile } = usePlatformOS();
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
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>
<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>
</Tooltip>
<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>
</div>
{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">
<AvatarGroup showTooltip={false}>
{moduleDetails.member_ids.map((member_id) => {
@ -222,6 +223,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
</div>
<Tooltip
isMobile={isMobile}
tooltipContent={isNaN(completionPercentage) ? "0" : `${completionPercentage.toFixed(0)}%`}
position="top-left"
>

View File

@ -21,6 +21,7 @@ import { EUserProjectRoles } from "constants/project";
import { renderFormattedDate } from "helpers/date-time.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
import { useModule, useUser, useEventTracker, useMember } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// components
// ui
// helpers
@ -48,7 +49,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
// derived values
const moduleDetails = getModuleById(moduleId);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const { isMobile } = usePlatformOS();
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
@ -194,7 +195,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
)}
</CircularProgressIndicator>
</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>
</Tooltip>
</div>
@ -227,7 +228,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
</div>
<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">
{moduleDetails.member_ids.length > 0 ? (
<AvatarGroup showTooltip={false}>

View File

@ -1,13 +1,16 @@
import Image from "next/image";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// hooks
import { useApplication, useEventTracker, useModule } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
import { useApplication, useEventTracker, useModule, useModuleFilter } from "hooks/store";
// components
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules";
import { EmptyState } from "components/empty-state";
// 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
import { EmptyStateType } from "constants/empty-state";
@ -18,29 +21,48 @@ export const ModulesListView: React.FC = observer(() => {
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
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();
const { storedValue: modulesView } = useLocalStorage("modules_view", "grid");
if (loader || !projectModuleIds)
if (loader || !filteredModuleIds)
return (
<>
{modulesView === "list" && <CycleModuleListLayout />}
{modulesView === "grid" && <CycleModuleBoardLayout />}
{modulesView === "gantt_chart" && <GanttLayoutLoader />}
{displayFilters?.layout === "list" && <CycleModuleListLayout />}
{displayFilters?.layout === "board" && <CycleModuleBoardLayout />}
{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 (
<>
{projectModuleIds.length > 0 ? (
{filteredModuleIds.length > 0 ? (
<>
{modulesView === "list" && (
{displayFilters?.layout === "list" && (
<div className="h-full overflow-y-auto">
<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">
{projectModuleIds.map((moduleId) => (
{filteredModuleIds.map((moduleId) => (
<ModuleListItem key={moduleId} moduleId={moduleId} />
))}
</div>
@ -51,7 +73,7 @@ export const ModulesListView: React.FC = observer(() => {
</div>
</div>
)}
{modulesView === "grid" && (
{displayFilters?.layout === "board" && (
<div className="h-full w-full">
<div className="flex h-full w-full justify-between">
<div
@ -61,7 +83,7 @@ export const ModulesListView: React.FC = observer(() => {
: "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4"
} auto-rows-max transition-all vertical-scrollbar scrollbar-lg`}
>
{projectModuleIds.map((moduleId) => (
{filteredModuleIds.map((moduleId) => (
<ModuleCardItem key={moduleId} moduleId={moduleId} />
))}
</div>
@ -72,7 +94,7 @@ export const ModulesListView: React.FC = observer(() => {
</div>
</div>
)}
{modulesView === "gantt_chart" && <ModulesListGanttChartView />}
{displayFilters?.layout === "gantt" && <ModulesListGanttChartView />}
</>
) : (
<EmptyState

View File

@ -15,6 +15,7 @@ import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "help
import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
// hooks
import { useEventTracker } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// type
import type { IUserNotification, NotificationType } from "@plane/types";
@ -44,7 +45,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
} = props;
// store hooks
const { captureEvent } = useEventTracker();
const { isMobile } = usePlatformOS();
const router = useRouter();
const { workspaceSlug } = router.query;
// states
@ -358,7 +359,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
},
},
].map((item) => (
<Tooltip tooltipContent={item.name} key={item.id}>
<Tooltip tooltipContent={item.name} key={item.id} isMobile={isMobile}>
<button
type="button"
onClick={(e) => {
@ -373,7 +374,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
</button>
</Tooltip>
))}
<Tooltip tooltipContent="Snooze">
<Tooltip tooltipContent="Snooze" isMobile={isMobile}>
<CustomMenu
className="flex items-center"
customButton={

View File

@ -13,6 +13,7 @@ import {
} from "constants/event-tracker";
import { getNumberCount } from "helpers/string.helper";
import { useEventTracker } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// helpers
// type
import type { NotificationType, NotificationCount } from "@plane/types";
@ -52,6 +53,8 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
} = props;
// store hooks
const { captureEvent } = useEventTracker();
// hooks
const { isMobile } = usePlatformOS();
const notificationTabs: Array<{
label: string;
@ -84,7 +87,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
</div>
<div className="flex items-center justify-center gap-x-4 text-custom-text-200">
<Tooltip tooltipContent="Refresh">
<Tooltip tooltipContent="Refresh" isMobile={isMobile}>
<button
type="button"
onClick={() => {
@ -94,7 +97,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
<RefreshCw className={`h-3.5 w-3.5 ${isRefreshing ? "animate-spin" : ""}`} />
</button>
</Tooltip>
<Tooltip tooltipContent="Unread notifications">
<Tooltip tooltipContent="Unread notifications" isMobile={isMobile}>
<button
type="button"
onClick={() => {
@ -154,7 +157,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
</CustomMenu.MenuItem>
</CustomMenu>
<div className="hidden md:block">
<Tooltip tooltipContent="Close">
<Tooltip tooltipContent="Close" isMobile={isMobile}>
<button type="button" onClick={() => closePopover()}>
<X className="h-3.5 w-3.5" />
</button>

View File

@ -11,6 +11,7 @@ import { getNumberCount } from "helpers/string.helper";
import { useApplication } from "hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import useUserNotification from "hooks/use-user-notifications";
import { usePlatformOS } from "hooks/use-platform-os";
// components
// images
import emptyNotification from "public/empty-state/notification.svg";
@ -23,6 +24,8 @@ export const NotificationPopover = observer(() => {
const { theme: themeStore } = useApplication();
// refs
const notificationPopoverRef = React.useRef<HTMLDivElement | null>(null);
// hooks
const { isMobile } = usePlatformOS();
const {
notifications,
@ -67,7 +70,7 @@ export const NotificationPopover = observer(() => {
/>
<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
className={`group relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
isActive

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