Merge branch 'develop' of github.com:makeplane/plane into dev/api_logging

This commit is contained in:
pablohashescobar 2024-03-13 17:34:50 +05:30
commit 7a5c6e0e96
178 changed files with 4084 additions and 822 deletions

View File

@ -2,6 +2,27 @@ name: Branch Build
on: on:
workflow_dispatch: workflow_dispatch:
inputs:
build-web:
required: false
description: "Build Web"
type: boolean
default: false
build-space:
required: false
description: "Build Space"
type: boolean
default: false
build-api:
required: false
description: "Build API"
type: boolean
default: false
build-proxy:
required: false
description: "Build Proxy"
type: boolean
default: false
push: push:
branches: branches:
- master - master
@ -74,7 +95,7 @@ jobs:
- nginx/** - nginx/**
branch_build_push_frontend: branch_build_push_frontend:
if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event.inputs.build-web=='true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: [branch_build_setup] needs: [branch_build_setup]
env: env:
@ -126,7 +147,7 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_space: branch_build_push_space:
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event.inputs.build-space=='true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: [branch_build_setup] needs: [branch_build_setup]
env: env:
@ -178,7 +199,7 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_backend: branch_build_push_backend:
if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event.inputs.build-api=='true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: [branch_build_setup] needs: [branch_build_setup]
env: env:
@ -230,7 +251,7 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_proxy: branch_build_push_proxy:
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event.inputs.build-web=='true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: [branch_build_setup] needs: [branch_build_setup]
env: env:
@ -280,4 +301,3 @@ jobs:
DOCKER_BUILDKIT: 1 DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@ -4,60 +4,185 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
web-build: web-build:
required: true required: false
description: 'Build Web'
type: boolean type: boolean
default: true default: true
space-build: space-build:
required: true required: false
description: 'Build Space'
type: boolean type: boolean
default: false default: false
env:
BUILD_WEB: ${{ github.event.inputs.web-build }}
BUILD_SPACE: ${{ github.event.inputs.space-build }}
jobs: jobs:
setup-feature-build:
name: Feature Build Setup
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
echo "BUILD_WEB=$BUILD_WEB"
echo "BUILD_SPACE=$BUILD_SPACE"
outputs:
web-build: ${{ env.BUILD_WEB}}
space-build: ${{env.BUILD_SPACE}}
feature-build-web:
if: ${{ needs.setup-feature-build.outputs.web-build == 'true' }}
needs: setup-feature-build
name: Feature Build Web
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
steps:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install AWS cli
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
pip3 install awscli
- name: Checkout
uses: actions/checkout@v4
with:
path: plane
- name: Install Dependencies
run: |
cd $GITHUB_WORKSPACE/plane
yarn install
- name: Build Web
id: build-web
run: |
cd $GITHUB_WORKSPACE/plane
yarn build --filter=web
cd $GITHUB_WORKSPACE
TAR_NAME="web.tar.gz"
tar -czf $TAR_NAME ./plane
FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ")
aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY
feature-build-space:
if: ${{ needs.setup-feature-build.outputs.space-build == 'true' }}
needs: setup-feature-build
name: Feature Build Space
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
outputs:
do-build: ${{ needs.setup-feature-build.outputs.space-build }}
s3-url: ${{ steps.build-space.outputs.S3_PRESIGNED_URL }}
steps:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install AWS cli
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
pip3 install awscli
- name: Checkout
uses: actions/checkout@v4
with:
path: plane
- name: Install Dependencies
run: |
cd $GITHUB_WORKSPACE/plane
yarn install
- name: Build Space
id: build-space
run: |
cd $GITHUB_WORKSPACE/plane
yarn build --filter=space
cd $GITHUB_WORKSPACE
TAR_NAME="space.tar.gz"
tar -czf $TAR_NAME ./plane
FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ")
aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY
feature-deploy: feature-deploy:
if: ${{ always() && (needs.setup-feature-build.outputs.web-build == 'true' || needs.setup-feature-build.outputs.space-build == 'true') }}
needs: [feature-build-web, feature-build-space]
name: Feature Deploy name: Feature Deploy
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG }} AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
BUILD_WEB: ${{ (github.event.inputs.web-build == '' && true) || github.event.inputs.web-build }} AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
BUILD_SPACE: ${{ (github.event.inputs.space-build == '' && false) || github.event.inputs.space-build }} AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
KUBE_CONFIG_FILE: ${{ secrets.FEATURE_PREVIEW_KUBE_CONFIG }}
steps: steps:
- name: Install AWS cli
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
pip3 install awscli
- name: Tailscale - name: Tailscale
uses: tailscale/github-action@v2 uses: tailscale/github-action@v2
with: with:
oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }} oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }} oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }}
tags: tag:ci tags: tag:ci
- name: Kubectl Setup - name: Kubectl Setup
run: | run: |
curl -LO "https://dl.k8s.io/release/${{secrets.KUBE_VERSION}}/bin/linux/amd64/kubectl" curl -LO "https://dl.k8s.io/release/${{ vars.FEATURE_PREVIEW_KUBE_VERSION }}/bin/linux/amd64/kubectl"
chmod +x kubectl chmod +x kubectl
mkdir -p ~/.kube mkdir -p ~/.kube
echo "$KUBE_CONFIG_FILE" > ~/.kube/config echo "$KUBE_CONFIG_FILE" > ~/.kube/config
chmod 600 ~/.kube/config chmod 600 ~/.kube/config
- name: HELM Setup - name: HELM Setup
run: | run: |
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh chmod 700 get_helm.sh
./get_helm.sh ./get_helm.sh
- name: App Deploy - name: App Deploy
run: | run: |
helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ secrets.FEATURE_PREVIEW_HELM_CHART_URL }} WEB_S3_URL=""
GIT_BRANCH=${{ github.ref_name }} if [ ${{ env.BUILD_WEB }} == true ]; then
APP_NAMESPACE=${{ secrets.FEATURE_PREVIEW_NAMESPACE }} WEB_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/web.tar.gz --expires-in 3600)
fi
METADATA=$(helm install feature-preview/${{ secrets.FEATURE_PREVIEW_HELM_CHART_NAME }} \ SPACE_S3_URL=""
--kube-insecure-skip-tls-verify \ if [ ${{ env.BUILD_SPACE }} == true ]; then
SPACE_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/space.tar.gz --expires-in 3600)
fi
if [ ${{ env.BUILD_WEB }} == true ] || [ ${{ env.BUILD_SPACE }} == true ]; then
helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ vars.FEATURE_PREVIEW_HELM_CHART_URL }}
APP_NAMESPACE="${{ vars.FEATURE_PREVIEW_NAMESPACE }}"
DEPLOY_SCRIPT_URL="${{ vars.FEATURE_PREVIEW_DEPLOY_SCRIPT_URL }}"
METADATA=$(helm --kube-insecure-skip-tls-verify install feature-preview/${{ vars.FEATURE_PREVIEW_HELM_CHART_NAME }} \
--generate-name \ --generate-name \
--namespace $APP_NAMESPACE \ --namespace $APP_NAMESPACE \
--set shared_config.git_repo=${{github.server_url}}/${{ github.repository }}.git \ --set ingress.primaryDomain=${{vars.FEATURE_PREVIEW_PRIMARY_DOMAIN || 'feature.plane.tools' }} \
--set shared_config.git_branch="$GIT_BRANCH" \ --set web.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \
--set web.enabled=${{ env.BUILD_WEB }} \ --set web.enabled=${{ env.BUILD_WEB || false }} \
--set space.enabled=${{ env.BUILD_SPACE }} \ --set web.artifact_url=$WEB_S3_URL \
--set space.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \
--set space.enabled=${{ env.BUILD_SPACE || false }} \
--set space.artifact_url=$SPACE_S3_URL \
--set shared_config.deploy_script_url=$DEPLOY_SCRIPT_URL \
--set shared_config.api_base_url=${{vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL}} \
--output json \ --output json \
--timeout 1000s) --timeout 1000s)
@ -71,3 +196,4 @@ jobs:
echo "APP NAME ::: $APP_NAME" echo "APP NAME ::: $APP_NAME"
echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME" echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME"
echo "****************************************" echo "****************************************"
fi

View File

@ -192,7 +192,15 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
) )
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
queryset = self.get_queryset() queryset = self.get_queryset().annotate(
total_issues=Count(
"issue_cycle",
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
cycle_view = request.GET.get("cycle_view", "all") cycle_view = request.GET.get("cycle_view", "all")
# Update the order by # Update the order by
@ -223,6 +231,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"progress_snapshot", "progress_snapshot",
# meta fields # meta fields
"is_favorite", "is_favorite",
"total_issues",
"cancelled_issues", "cancelled_issues",
"completed_issues", "completed_issues",
"started_issues", "started_issues",
@ -345,6 +354,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"external_id", "external_id",
"progress_snapshot", "progress_snapshot",
# meta fields # meta fields
"total_issues",
"is_favorite", "is_favorite",
"cancelled_issues", "cancelled_issues",
"completed_issues", "completed_issues",

View File

@ -79,6 +79,15 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
), ),
) )
) )
.annotate(
total_issues=Count(
"issue_module",
filter=Q(
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
),
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"issue_module__issue__state__group", "issue_module__issue__state__group",
@ -214,6 +223,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"external_source", "external_source",
"external_id", "external_id",
# computed fields # computed fields
"total_issues",
"is_favorite", "is_favorite",
"cancelled_issues", "cancelled_issues",
"completed_issues", "completed_issues",

View File

@ -0,0 +1,61 @@
from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.management import BaseCommand, CommandError
from plane.license.utils.instance_value import get_email_configuration
class Command(BaseCommand):
"""Django command to pause execution until db is available"""
def add_arguments(self, parser):
# Positional argument
parser.add_argument("to_email", type=str, help="receiver's email")
def handle(self, *args, **options):
receiver_email = options.get("to_email")
if not receiver_email:
raise CommandError("Reciever email is required")
(
EMAIL_HOST,
EMAIL_HOST_USER,
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_FROM,
) = get_email_configuration()
connection = get_connection(
host=EMAIL_HOST,
port=int(EMAIL_PORT),
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
timeout=30,
)
# Prepare email details
subject = "Email Notification from Plane"
message = (
"This is a sample email notification sent from Plane application."
)
self.stdout.write(self.style.SUCCESS("Trying to send test email..."))
# Send the email
try:
msg = EmailMultiAlternatives(
subject=subject,
body=message,
from_email=EMAIL_FROM,
to=[receiver_email],
connection=connection,
)
msg.send()
self.stdout.write(self.style.SUCCESS("Email succesfully sent"))
except Exception as e:
self.stdout.write(
self.style.ERROR(
f"Error: Email could not be delivered due to {e}"
)
)

View File

@ -495,13 +495,6 @@ function install() {
update_config "INSTALLATION_DATE" "$(date '+%Y-%m-%d')" update_config "INSTALLATION_DATE" "$(date '+%Y-%m-%d')"
if command -v crontab &> /dev/null; then
sudo touch /etc/cron.daily/makeplane
sudo chmod +x /etc/cron.daily/makeplane
sudo echo "0 2 * * * root /usr/local/bin/plane-app --upgrade" > /etc/cron.daily/makeplane
sudo crontab /etc/cron.daily/makeplane
fi
show_message "Plane Installed Successfully ✅" show_message "Plane Installed Successfully ✅"
show_message "" show_message ""
else else
@ -607,11 +600,6 @@ function uninstall() {
sudo rm $PLANE_INSTALL_DIR/config.env &> /dev/null sudo rm $PLANE_INSTALL_DIR/config.env &> /dev/null
sudo rm $PLANE_INSTALL_DIR/docker-compose.yaml &> /dev/null sudo rm $PLANE_INSTALL_DIR/docker-compose.yaml &> /dev/null
if command -v crontab &> /dev/null; then
sudo crontab -r &> /dev/null
sudo rm /etc/cron.daily/makeplane &> /dev/null
fi
# rm -rf $PLANE_INSTALL_DIR &> /dev/null # rm -rf $PLANE_INSTALL_DIR &> /dev/null
show_message "- Configuration Cleaned ✅" show_message "- Configuration Cleaned ✅"

View File

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

View File

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

View File

@ -2,10 +2,10 @@ export * from "./users";
export * from "./workspace"; export * from "./workspace";
export * from "./cycle"; export * from "./cycle";
export * from "./dashboard"; export * from "./dashboard";
export * from "./projects"; export * from "./project";
export * from "./state"; export * from "./state";
export * from "./issues"; export * from "./issues";
export * from "./modules"; export * from "./module";
export * from "./views"; export * from "./views";
export * from "./integration"; export * from "./integration";
export * from "./pages"; export * from "./pages";

View File

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

View File

@ -0,0 +1,32 @@
export type TModuleOrderByOptions =
| "name"
| "-name"
| "progress"
| "-progress"
| "issues_length"
| "-issues_length"
| "target_date"
| "-target_date"
| "created_at"
| "-created_at";
export type TModuleLayoutOptions = "list" | "board" | "gantt";
export type TModuleDisplayFilters = {
favorites?: boolean;
layout?: TModuleLayoutOptions;
order_by?: TModuleOrderByOptions;
};
export type TModuleFilters = {
lead?: string[] | null;
members?: string[] | null;
start_date?: string[] | null;
status?: string[] | null;
target_date?: string[] | null;
};
export type TModuleStoredFilters = {
display_filters?: TModuleDisplayFilters;
filters?: TModuleFilters;
};

View File

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

View File

@ -0,0 +1,25 @@
export type TProjectOrderByOptions =
| "sort_order"
| "name"
| "-name"
| "created_at"
| "-created_at"
| "members_length"
| "-members_length";
export type TProjectDisplayFilters = {
my_projects?: boolean;
order_by?: TProjectOrderByOptions;
};
export type TProjectFilters = {
access?: string[] | null;
lead?: string[] | null;
members?: string[] | null;
created_at?: string[] | null;
};
export type TProjectStoredFilters = {
display_filters?: TProjectDisplayFilters;
filters?: TProjectFilters;
};

View File

@ -7,7 +7,7 @@ import type {
IWorkspace, IWorkspace,
IWorkspaceLite, IWorkspaceLite,
TStateGroups, TStateGroups,
} from "."; } from "..";
export type TProjectLogoProps = { export type TProjectLogoProps = {
in_use: "emoji" | "icon"; in_use: "emoji" | "icon";

View File

@ -103,7 +103,7 @@ export const CustomEmojiIconPicker: React.FC<TCustomEmojiPicker> = (props) => {
style={styles.popper} style={styles.popper}
{...attributes.popper} {...attributes.popper}
className={cn( className={cn(
"h-80 w-80 bg-custom-background-100 rounded-md border-[0.5px] border-custom-border-300 overflow-hidden", "w-80 bg-custom-background-100 rounded-md border-[0.5px] border-custom-border-300 overflow-hidden",
dropdownClassName dropdownClassName
)} )}
> >
@ -146,7 +146,7 @@ export const CustomEmojiIconPicker: React.FC<TCustomEmojiPicker> = (props) => {
}} }}
/> />
</Tab.Panel> </Tab.Panel>
<Tab.Panel> <Tab.Panel className="h-80 w-full">
<IconsList <IconsList
defaultColor={defaultIconColor} defaultColor={defaultIconColor}
onChange={(val) => { onChange={(val) => {

View File

@ -29,6 +29,7 @@ interface ITooltipProps {
className?: string; className?: string;
openDelay?: number; openDelay?: number;
closeDelay?: number; closeDelay?: number;
isMobile?: boolean;
} }
export const Tooltip: React.FC<ITooltipProps> = ({ export const Tooltip: React.FC<ITooltipProps> = ({
@ -40,6 +41,7 @@ export const Tooltip: React.FC<ITooltipProps> = ({
className = "", className = "",
openDelay = 200, openDelay = 200,
closeDelay, closeDelay,
isMobile = false,
}) => ( }) => (
<Tooltip2 <Tooltip2
disabled={disabled} disabled={disabled}
@ -47,7 +49,7 @@ export const Tooltip: React.FC<ITooltipProps> = ({
hoverCloseDelay={closeDelay} hoverCloseDelay={closeDelay}
content={ content={
<div <div
className={`relative z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md ${className}`} className={`relative ${isMobile ? "hidden" : "block"} z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md ${className}`}
> >
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>} {tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>}
{tooltipContent} {tooltipContent}

View File

@ -6,6 +6,8 @@ import { renderFormattedDate } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types // types
import { IApiToken } from "@plane/types"; import { IApiToken } from "@plane/types";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
type Props = { type Props = {
handleClose: () => void; handleClose: () => void;
@ -14,7 +16,7 @@ type Props = {
export const GeneratedTokenDetails: React.FC<Props> = (props) => { export const GeneratedTokenDetails: React.FC<Props> = (props) => {
const { handleClose, tokenDetails } = props; const { handleClose, tokenDetails } = props;
const { isMobile } = usePlatformOS();
const copyApiToken = (token: string) => { const copyApiToken = (token: string) => {
copyTextToClipboard(token).then(() => copyTextToClipboard(token).then(() =>
setToast({ setToast({
@ -40,7 +42,7 @@ export const GeneratedTokenDetails: React.FC<Props> = (props) => {
className="mt-4 flex w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-2 text-sm font-medium outline-none" className="mt-4 flex w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-2 text-sm font-medium outline-none"
> >
{tokenDetails.token} {tokenDetails.token}
<Tooltip tooltipContent="Copy secret key"> <Tooltip tooltipContent="Copy secret key" isMobile={isMobile}>
<Copy className="h-4 w-4 text-custom-text-400" /> <Copy className="h-4 w-4 text-custom-text-400" />
</Tooltip> </Tooltip>
</button> </button>

View File

@ -3,6 +3,7 @@ import { XCircle } from "lucide-react";
// components // components
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
import { DeleteApiTokenModal } from "components/api-token"; import { DeleteApiTokenModal } from "components/api-token";
import { usePlatformOS } from "hooks/use-platform-os";
// ui // ui
// helpers // helpers
import { renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper"; import { renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper";
@ -17,12 +18,14 @@ export const ApiTokenListItem: React.FC<Props> = (props) => {
const { token } = props; const { token } = props;
// states // states
const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false);
// hooks
const { isMobile } = usePlatformOS();
return ( return (
<> <>
<DeleteApiTokenModal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)} tokenId={token.id} /> <DeleteApiTokenModal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)} tokenId={token.id} />
<div className="group relative flex flex-col justify-center border-b border-custom-border-200 px-4 py-3"> <div className="group relative flex flex-col justify-center border-b border-custom-border-200 px-4 py-3">
<Tooltip tooltipContent="Delete token"> <Tooltip tooltipContent="Delete token" isMobile={isMobile}>
<button <button
onClick={() => setDeleteModalOpen(true)} onClick={() => setDeleteModalOpen(true)}
className="absolute right-4 hidden place-items-center group-hover:grid" className="absolute right-4 hidden place-items-center group-hover:grid"
@ -33,8 +36,7 @@ export const ApiTokenListItem: React.FC<Props> = (props) => {
<div className="flex w-4/5 items-center"> <div className="flex w-4/5 items-center">
<h5 className="truncate text-sm font-medium">{token.label}</h5> <h5 className="truncate text-sm font-medium">{token.label}</h5>
<span <span
className={`${ className={`${token.is_active ? "bg-green-500/10 text-green-500" : "bg-custom-background-80 text-custom-text-400"
token.is_active ? "bg-green-500/10 text-green-500" : "bg-custom-background-80 text-custom-text-400"
} ml-2 flex h-4 max-h-fit items-center rounded-sm px-2 text-xs font-medium`} } ml-2 flex h-4 max-h-fit items-center rounded-sm px-2 text-xs font-medium`}
> >
{token.is_active ? "Active" : "Expired"} {token.is_active ? "Active" : "Expired"}

View File

@ -20,12 +20,11 @@ import {
} from "components/command-palette"; } from "components/command-palette";
import { ISSUE_DETAILS } from "constants/fetch-keys"; import { ISSUE_DETAILS } from "constants/fetch-keys";
import { useApplication, useEventTracker, useProject } from "hooks/store"; import { useApplication, useEventTracker, useProject } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// services // services
import useDebounce from "hooks/use-debounce"; import useDebounce from "hooks/use-debounce";
import { IssueService } from "services/issue"; import { IssueService } from "services/issue";
import { WorkspaceService } from "services/workspace.service"; import { WorkspaceService } from "services/workspace.service";
// hooks
// components
// types // types
import { IWorkspaceSearchResults } from "@plane/types"; import { IWorkspaceSearchResults } from "@plane/types";
// fetch-keys // fetch-keys
@ -37,6 +36,7 @@ const issueService = new IssueService();
export const CommandModal: React.FC = observer(() => { export const CommandModal: React.FC = observer(() => {
// hooks // hooks
const { getProjectById } = useProject(); const { getProjectById } = useProject();
const { isMobile } = usePlatformOS();
// states // states
const [placeholder, setPlaceholder] = useState("Type a command or search..."); const [placeholder, setPlaceholder] = useState("Type a command or search...");
const [resultsCount, setResultsCount] = useState(0); const [resultsCount, setResultsCount] = useState(0);
@ -197,7 +197,7 @@ export const CommandModal: React.FC = observer(() => {
</div> </div>
)} )}
{projectId && ( {projectId && (
<Tooltip tooltipContent="Toggle workspace level search"> <Tooltip tooltipContent="Toggle workspace level search" isMobile={isMobile}>
<div className="flex flex-shrink-0 cursor-pointer items-center gap-1 self-end text-xs sm:self-center"> <div className="flex flex-shrink-0 cursor-pointer items-center gap-1 self-end text-xs sm:self-center">
<button <button
type="button" type="button"

View File

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

View File

@ -1,6 +1,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { usePlatformOS } from "hooks/use-platform-os";
// store hooks // store hooks
// icons // icons
import { import {
@ -29,9 +30,13 @@ import { IIssueActivity } from "@plane/types";
export const IssueLink = ({ activity }: { activity: IIssueActivity }) => { export const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { isMobile } = usePlatformOS();
return ( return (
<Tooltip tooltipContent={activity?.issue_detail ? activity.issue_detail.name : "This issue has been deleted"}> <Tooltip
tooltipContent={activity?.issue_detail ? activity.issue_detail.name : "This issue has been deleted"}
isMobile={isMobile}
>
{activity?.issue_detail ? ( {activity?.issue_detail ? (
<a <a
aria-disabled={activity.issue === null} aria-disabled={activity.issue === null}

View File

@ -5,7 +5,7 @@ import { Rocket, Search, X } from "lucide-react";
import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
import useDebounce from "hooks/use-debounce"; import useDebounce from "hooks/use-debounce";
import { usePlatformOS } from "hooks/use-platform-os";
import { ProjectService } from "services/project"; import { ProjectService } from "services/project";
// ui // ui
// types // types
@ -40,7 +40,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
const { isMobile } = usePlatformOS();
const debouncedSearchTerm: string = useDebounce(searchTerm, 500); const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
const handleClose = () => { const handleClose = () => {
@ -154,7 +154,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
</div> </div>
)} )}
{workspaceLevelToggle && ( {workspaceLevelToggle && (
<Tooltip tooltipContent="Toggle workspace level search"> <Tooltip tooltipContent="Toggle workspace level search" isMobile={isMobile}>
<div <div
className={`flex flex-shrink-0 cursor-pointer items-center gap-1 text-xs ${ className={`flex flex-shrink-0 cursor-pointer items-center gap-1 text-xs ${
isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200" isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200"

View File

@ -7,6 +7,7 @@ import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
import { calculateTimeAgo } from "helpers/date-time.helper"; import { calculateTimeAgo } from "helpers/date-time.helper";
// hooks // hooks
import { useMember } from "hooks/store"; import { useMember } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// types // types
import { ILinkDetails, UserAuth } from "@plane/types"; import { ILinkDetails, UserAuth } from "@plane/types";
@ -19,7 +20,7 @@ type Props = {
export const LinksList: React.FC<Props> = observer(({ links, handleDeleteLink, handleEditLink, userAuth }) => { export const LinksList: React.FC<Props> = observer(({ links, handleDeleteLink, handleEditLink, userAuth }) => {
const { getUserDetails } = useMember(); const { getUserDetails } = useMember();
const { isMobile } = usePlatformOS();
const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
const copyToClipboard = (text: string) => { const copyToClipboard = (text: string) => {
@ -42,7 +43,7 @@ export const LinksList: React.FC<Props> = observer(({ links, handleDeleteLink, h
<span className="py-1"> <span className="py-1">
<LinkIcon className="h-3 w-3 flex-shrink-0" /> <LinkIcon className="h-3 w-3 flex-shrink-0" />
</span> </span>
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url}> <Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url} isMobile={isMobile}>
<span <span
className="cursor-pointer truncate text-xs" className="cursor-pointer truncate text-xs"
onClick={() => copyToClipboard(link.title && link.title !== "" ? link.title : link.url)} onClick={() => copyToClipboard(link.title && link.title !== "" ? link.title : link.url)}

View File

@ -4,6 +4,7 @@ import Link from "next/link";
import useSWR from "swr"; import useSWR from "swr";
// hooks // hooks
import { useCycle, useCycleFilter, useIssues, useMember, useProject } from "hooks/store"; import { useCycle, useCycleFilter, useIssues, useMember, useProject } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// ui // ui
import { SingleProgressStats } from "components/core"; import { SingleProgressStats } from "components/core";
import { import {
@ -46,6 +47,8 @@ interface IActiveCycleDetails {
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => { export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
// props // props
const { workspaceSlug, projectId } = props; const { workspaceSlug, projectId } = props;
// hooks
const { isMobile } = usePlatformOS();
// store hooks // store hooks
const { const {
issues: { fetchActiveCycleIssues }, issues: { fetchActiveCycleIssues },
@ -197,7 +200,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
<span className="h-5 w-5"> <span className="h-5 w-5">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-4 w-4" /> <CycleGroupIcon cycleGroup={cycleStatus} className="h-4 w-4" />
</span> </span>
<Tooltip tooltipContent={activeCycle.name} position="top-left"> <Tooltip tooltipContent={activeCycle.name} position="top-left" isMobile={isMobile}>
<h3 className="break-words text-lg font-semibold">{truncateText(activeCycle.name, 70)}</h3> <h3 className="break-words text-lg font-semibold">{truncateText(activeCycle.name, 70)}</h3>
</Tooltip> </Tooltip>
</span> </span>
@ -325,6 +328,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
<PriorityIcon priority={issue.priority} withContainer size={12} /> <PriorityIcon priority={issue.priority} withContainer size={12} />
<Tooltip <Tooltip
isMobile={isMobile}
tooltipHeading="Issue ID" tooltipHeading="Issue ID"
tooltipContent={`${currentProjectDetails?.identifier}-${issue.sequence_id}`} tooltipContent={`${currentProjectDetails?.identifier}-${issue.sequence_id}`}
> >
@ -332,7 +336,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
{currentProjectDetails?.identifier}-{issue.sequence_id} {currentProjectDetails?.identifier}-{issue.sequence_id}
</span> </span>
</Tooltip> </Tooltip>
<Tooltip position="top-left" tooltipContent={issue.name}> <Tooltip position="top-left" tooltipContent={issue.name} isMobile={isMobile}>
<span className="text-[0.825rem] text-custom-text-100">{truncateText(issue.name, 30)}</span> <span className="text-[0.825rem] text-custom-text-100">{truncateText(issue.name, 30)}</span>
</Tooltip> </Tooltip>
</div> </div>
@ -345,7 +349,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
buttonVariant="background-with-text" buttonVariant="background-with-text"
/> />
{issue.target_date && ( {issue.target_date && (
<Tooltip tooltipHeading="Target Date" tooltipContent={renderFormattedDate(issue.target_date)}> <Tooltip tooltipHeading="Target Date" tooltipContent={renderFormattedDate(issue.target_date)} isMobile={isMobile}>
<div className="flex h-full cursor-not-allowed items-center gap-1.5 rounded bg-custom-background-80 px-2 py-0.5 text-xs"> <div className="flex h-full cursor-not-allowed items-center gap-1.5 rounded bg-custom-background-80 px-2 py-0.5 text-xs">
<CalendarCheck className="h-3 w-3 flex-shrink-0" /> <CalendarCheck className="h-3 w-3 flex-shrink-0" />
<span className="text-xs">{renderFormattedDateWithoutYear(issue.target_date)}</span> <span className="text-xs">{renderFormattedDateWithoutYear(issue.target_date)}</span>

View File

@ -4,7 +4,7 @@ import { X } from "lucide-react";
import { renderFormattedDate } from "helpers/date-time.helper"; import { renderFormattedDate } from "helpers/date-time.helper";
import { capitalizeFirstLetter } from "helpers/string.helper"; import { capitalizeFirstLetter } from "helpers/string.helper";
// constants // constants
import { DATE_FILTER_OPTIONS } from "constants/filters"; import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
type Props = { type Props = {
editable: boolean | undefined; editable: boolean | undefined;
@ -18,7 +18,7 @@ export const AppliedDateFilters: React.FC<Props> = observer((props) => {
const getDateLabel = (value: string): string => { const getDateLabel = (value: string): string => {
let dateLabel = ""; let dateLabel = "";
const dateDetails = DATE_FILTER_OPTIONS.find((d) => d.value === value); const dateDetails = DATE_AFTER_FILTER_OPTIONS.find((d) => d.value === value);
if (dateDetails) dateLabel = dateDetails.name; if (dateDetails) dateLabel = dateDetails.name;
else { else {

View File

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

View File

@ -5,6 +5,7 @@ import { ListFilter, Search, X } from "lucide-react";
// hooks // hooks
import { useCycleFilter } from "hooks/store"; import { useCycleFilter } from "hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { usePlatformOS } from "hooks/use-platform-os";
// components // components
import { CycleFiltersSelection } from "components/cycles"; import { CycleFiltersSelection } from "components/cycles";
import { FiltersDropdown } from "components/issues"; import { FiltersDropdown } from "components/issues";
@ -36,6 +37,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
updateFilters, updateFilters,
updateSearchQuery, updateSearchQuery,
} = useCycleFilter(); } = useCycleFilter();
const { isMobile } = usePlatformOS();
// outside click detector hook // outside click detector hook
useOutsideClickDetector(inputRef, () => { useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false); if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
@ -62,7 +64,10 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") { if (e.key === "Escape") {
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery(""); if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
else setIsSearchOpen(false); else {
setIsSearchOpen(false);
inputRef.current?.blur();
}
} }
}; };
@ -107,7 +112,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
<Search className="h-3.5 w-3.5" /> <Search className="h-3.5 w-3.5" />
<input <input
ref={inputRef} ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 focus:outline-none" className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
placeholder="Search" placeholder="Search"
value={searchQuery} value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)} onChange={(e) => updateSearchQuery(e.target.value)}
@ -131,7 +136,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
</FiltersDropdown> </FiltersDropdown>
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1"> <div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
{CYCLE_VIEW_LAYOUTS.map((layout) => ( {CYCLE_VIEW_LAYOUTS.map((layout) => (
<Tooltip key={layout.key} tooltipContent={layout.title}> <Tooltip key={layout.key} tooltipContent={layout.title} isMobile={isMobile}>
<button <button
type="button" type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${ className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
import { DateFilterModal } from "components/core"; import { DateFilterModal } from "components/core";
import { FilterHeader, FilterOption } from "components/issues"; import { FilterHeader, FilterOption } from "components/issues";
// constants // constants
import { DATE_FILTER_OPTIONS } from "constants/filters"; import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
type Props = { type Props = {
appliedFilters: string[] | null; appliedFilters: string[] | null;
@ -21,7 +21,9 @@ export const FilterEndDate: React.FC<Props> = observer((props) => {
const appliedFiltersCount = appliedFilters?.length ?? 0; const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase())); const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) =>
d.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return ( return (
<> <>

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
import { DateFilterModal } from "components/core"; import { DateFilterModal } from "components/core";
import { FilterHeader, FilterOption } from "components/issues"; import { FilterHeader, FilterOption } from "components/issues";
// constants // constants
import { DATE_FILTER_OPTIONS } from "constants/filters"; import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
type Props = { type Props = {
appliedFilters: string[] | null; appliedFilters: string[] | null;
@ -21,7 +21,9 @@ export const FilterStartDate: React.FC<Props> = observer((props) => {
const appliedFiltersCount = appliedFilters?.length ?? 0; const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase())); const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) =>
d.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return ( return (
<> <>

View File

@ -1,6 +1,8 @@
import Link from "next/link";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// hooks // hooks
import { usePlatformOS } from "hooks/use-platform-os";
// ui // ui
import { Tooltip, ContrastIcon } from "@plane/ui"; import { Tooltip, ContrastIcon } from "@plane/ui";
// helpers // helpers
@ -22,7 +24,7 @@ export const CycleGanttBlock: React.FC<Props> = observer((props) => {
const { getCycleById } = useCycle(); const { getCycleById } = useCycle();
// derived values // derived values
const cycleDetails = getCycleById(cycleId); const cycleDetails = getCycleById(cycleId);
const { isMobile } = usePlatformOS();
const cycleStatus = cycleDetails?.status.toLocaleLowerCase(); const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
return ( return (
@ -44,6 +46,7 @@ export const CycleGanttBlock: React.FC<Props> = observer((props) => {
> >
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" /> <div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
<Tooltip <Tooltip
isMobile={isMobile}
tooltipContent={ tooltipContent={
<div className="space-y-1"> <div className="space-y-1">
<h5>{cycleDetails?.name}</h5> <h5>{cycleDetails?.name}</h5>
@ -63,8 +66,6 @@ export const CycleGanttBlock: React.FC<Props> = observer((props) => {
export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => { export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
const { cycleId } = props; const { cycleId } = props;
// router
const router = useRouter();
// store hooks // store hooks
const { const {
router: { workspaceSlug }, router: { workspaceSlug },
@ -76,9 +77,9 @@ export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
const cycleStatus = cycleDetails?.status.toLocaleLowerCase(); const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
return ( return (
<div <Link
className="relative flex h-full w-full items-center gap-2" className="relative flex h-full w-full items-center gap-2"
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project_id}/cycles/${cycleDetails?.id}`)} href={`/${workspaceSlug}/projects/${cycleDetails?.project_id}/cycles/${cycleDetails?.id}`}
> >
<ContrastIcon <ContrastIcon
className="h-5 w-5 flex-shrink-0" className="h-5 w-5 flex-shrink-0"
@ -95,6 +96,6 @@ export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
}`} }`}
/> />
<h6 className="flex-grow truncate text-sm font-medium">{cycleDetails?.name}</h6> <h6 className="flex-grow truncate text-sm font-medium">{cycleDetails?.name}</h6>
</div> </Link>
); );
}); });

View File

@ -3,6 +3,7 @@ import { observer } from "mobx-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// hooks // hooks
import { usePlatformOS } from "hooks/use-platform-os";
import { Check, Info, Star, User2 } from "lucide-react"; import { Check, Info, Star, User2 } from "lucide-react";
import { Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarGroup, Avatar, setPromiseToast } from "@plane/ui"; import { Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarGroup, Avatar, setPromiseToast } from "@plane/ui";
import { CycleQuickActions } from "components/cycles"; import { CycleQuickActions } from "components/cycles";
@ -33,6 +34,8 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
const { cycleId, workspaceSlug, projectId } = props; const { cycleId, workspaceSlug, projectId } = props;
// router // router
const router = useRouter(); const router = useRouter();
// hooks
const { isMobile } = usePlatformOS();
// store hooks // store hooks
const { captureEvent } = useEventTracker(); const { captureEvent } = useEventTracker();
const { const {
@ -164,7 +167,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
<div className="relative flex items-center gap-2.5 overflow-hidden"> <div className="relative flex items-center gap-2.5 overflow-hidden">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" /> <CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
<Tooltip tooltipContent={cycleDetails.name} position="top"> <Tooltip tooltipContent={cycleDetails.name} position="top" isMobile={isMobile}>
<span className="line-clamp-1 inline-block overflow-hidden truncate text-base font-medium"> <span className="line-clamp-1 inline-block overflow-hidden truncate text-base font-medium">
{cycleDetails.name} {cycleDetails.name}
</span> </span>
@ -196,7 +199,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
</div> </div>
<div className="relative flex flex-shrink-0 items-center gap-3"> <div className="relative flex flex-shrink-0 items-center gap-3">
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`}> <Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
<div className="flex w-10 cursor-default items-center justify-center"> <div className="flex w-10 cursor-default items-center justify-center">
{cycleDetails.assignee_ids?.length > 0 ? ( {cycleDetails.assignee_ids?.length > 0 ? (
<AvatarGroup showTooltip={false}> <AvatarGroup showTooltip={false}>

View File

@ -7,6 +7,7 @@ import { useDashboard } from "hooks/store";
// components // components
import { import {
DurationFilterDropdown, DurationFilterDropdown,
IssuesErrorState,
TabsList, TabsList,
WidgetIssuesList, WidgetIssuesList,
WidgetLoader, WidgetLoader,
@ -26,10 +27,12 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
// states // states
const [fetching, setFetching] = useState(false); const [fetching, setFetching] = useState(false);
// store hooks // store hooks
const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } =
useDashboard();
// derived values // derived values
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStats = getWidgetStats<TAssignedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats<TAssignedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStatsError = getWidgetStatsError(workspaceSlug, dashboardId, WIDGET_KEY);
const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE;
const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab);
const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? [];
@ -73,10 +76,23 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab);
if (!widgetDetails || !widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />; if ((!widgetDetails || !widgetStats) && !widgetStatsError) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return ( return (
<div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full hover:shadow-custom-shadow-4xl duration-300 flex flex-col min-h-96"> <div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full hover:shadow-custom-shadow-4xl duration-300 flex flex-col min-h-96">
{widgetStatsError ? (
<IssuesErrorState
isRefreshing={fetching}
onClick={() =>
handleUpdateFilters({
duration: EDurationFilters.NONE,
tab: "pending",
})
}
/>
) : (
widgetStats && (
<>
<div className="flex items-center justify-between gap-2 p-6 pl-7"> <div className="flex items-center justify-between gap-2 p-6 pl-7">
<Link <Link
href={`/${workspaceSlug}/workspace-views/assigned/${filterParams}`} href={`/${workspaceSlug}/workspace-views/assigned/${filterParams}`}
@ -102,7 +118,8 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
// switch to pending tab if target date is changed to none // switch to pending tab if target date is changed to none
if (val === "none" && selectedTab !== "completed") newTab = "pending"; if (val === "none" && selectedTab !== "completed") newTab = "pending";
// switch to upcoming tab if target date is changed to other than none // switch to upcoming tab if target date is changed to other than none
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") newTab = "upcoming"; if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed")
newTab = "upcoming";
handleUpdateFilters({ handleUpdateFilters({
duration: val, duration: val,
@ -141,6 +158,9 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
})} })}
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>
</>
)
)}
</div> </div>
); );
}); });

View File

@ -7,6 +7,7 @@ import { useDashboard } from "hooks/store";
// components // components
import { import {
DurationFilterDropdown, DurationFilterDropdown,
IssuesErrorState,
TabsList, TabsList,
WidgetIssuesList, WidgetIssuesList,
WidgetLoader, WidgetLoader,
@ -26,10 +27,12 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
// states // states
const [fetching, setFetching] = useState(false); const [fetching, setFetching] = useState(false);
// store hooks // store hooks
const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } =
useDashboard();
// derived values // derived values
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStats = getWidgetStats<TCreatedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats<TCreatedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStatsError = getWidgetStatsError(workspaceSlug, dashboardId, WIDGET_KEY);
const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE;
const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab);
const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? [];
@ -70,10 +73,23 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab);
if (!widgetDetails || !widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />; if ((!widgetDetails || !widgetStats) && !widgetStatsError) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return ( return (
<div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full hover:shadow-custom-shadow-4xl duration-300 flex flex-col min-h-96"> <div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full hover:shadow-custom-shadow-4xl duration-300 flex flex-col min-h-96">
{widgetStatsError ? (
<IssuesErrorState
isRefreshing={fetching}
onClick={() =>
handleUpdateFilters({
duration: EDurationFilters.NONE,
tab: "pending",
})
}
/>
) : (
widgetStats && (
<>
<div className="flex items-center justify-between gap-2 p-6 pl-7"> <div className="flex items-center justify-between gap-2 p-6 pl-7">
<Link <Link
href={`/${workspaceSlug}/workspace-views/created/${filterParams}`} href={`/${workspaceSlug}/workspace-views/created/${filterParams}`}
@ -99,7 +115,8 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
// switch to pending tab if target date is changed to none // switch to pending tab if target date is changed to none
if (val === "none" && selectedTab !== "completed") newTab = "pending"; if (val === "none" && selectedTab !== "completed") newTab = "pending";
// switch to upcoming tab if target date is changed to other than none // switch to upcoming tab if target date is changed to other than none
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") newTab = "upcoming"; if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed")
newTab = "upcoming";
handleUpdateFilters({ handleUpdateFilters({
duration: val, duration: val,
@ -138,6 +155,9 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
})} })}
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>
</>
)
)}
</div> </div>
); );
}); });

View File

@ -0,0 +1 @@
export * from "./issues";

View File

@ -0,0 +1,32 @@
import { AlertTriangle, RefreshCcw } from "lucide-react";
// ui
import { Button } from "@plane/ui";
type Props = {
isRefreshing: boolean;
onClick: () => void;
};
export const IssuesErrorState: React.FC<Props> = (props) => {
const { isRefreshing, onClick } = props;
return (
<div className="h-full w-full grid place-items-center">
<div className="text-center">
<div className="h-24 w-24 bg-red-500/20 rounded-full grid place-items-center mx-auto">
<AlertTriangle className="h-12 w-12 text-red-500" />
</div>
<p className="mt-7 text-custom-text-300 text-sm font-medium">There was an error in fetching widget details</p>
<Button
variant="neutral-primary"
prependIcon={<RefreshCcw className="h-3 w-3" />}
className="mt-2 mx-auto"
onClick={onClick}
loading={isRefreshing}
>
{isRefreshing ? "Retrying" : "Retry"}
</Button>
</div>
</div>
);
};

View File

@ -1,5 +1,6 @@
export * from "./dropdowns"; export * from "./dropdowns";
export * from "./empty-states"; export * from "./empty-states";
export * from "./error-states";
export * from "./issue-panels"; export * from "./issue-panels";
export * from "./loaders"; export * from "./loaders";
export * from "./assigned-issues"; export * from "./assigned-issues";

View File

@ -37,21 +37,36 @@ export const DefaultCollaboratorsList: React.FC<Props> = (props) => {
/> />
); );
const showViewMoreButton = pageCount < totalPages && resultsCount !== 0;
const showViewLessButton = pageCount > 1;
return ( return (
<> <>
<div className="mt-7 mb-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 gap-2 gap-y-8"> <div className="mt-7 mb-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 gap-2 gap-y-8">
{collaboratorsPages} {collaboratorsPages}
</div> </div>
{pageCount < totalPages && resultsCount !== 0 && ( {(showViewLessButton || showViewMoreButton) && (
<div className="flex items-center justify-center text-xs w-full"> <div className="flex items-center justify-center text-xs w-full">
{showViewLessButton && (
<Button
variant="link-primary"
size="sm"
className="my-3 hover:bg-custom-primary-100/20"
onClick={() => setPageCount(1)}
>
View less
</Button>
)}
{showViewMoreButton && (
<Button <Button
variant="link-primary" variant="link-primary"
size="sm" size="sm"
className="my-3 hover:bg-custom-primary-100/20" className="my-3 hover:bg-custom-primary-100/20"
onClick={handleLoadMore} onClick={handleLoadMore}
> >
Load more View more
</Button> </Button>
)}
</div> </div>
)} )}
</> </>

View File

@ -17,9 +17,9 @@ export const RecentCollaboratorsWidget: React.FC<WidgetProps> = (props) => {
<div className="w-full rounded-xl border-[0.5px] border-custom-border-200 bg-custom-background-100 duration-300 hover:shadow-custom-shadow-4xl"> <div className="w-full rounded-xl border-[0.5px] border-custom-border-200 bg-custom-background-100 duration-300 hover:shadow-custom-shadow-4xl">
<div className="flex items-start justify-between px-7 pt-6"> <div className="flex items-start justify-between px-7 pt-6">
<div> <div>
<h4 className="text-lg font-semibold text-custom-text-300">Most active members</h4> <h4 className="text-lg font-semibold text-custom-text-300">Collaborators</h4>
<p className="mt-2 text-xs font-medium text-custom-text-300"> <p className="mt-2 text-xs font-medium text-custom-text-300">
Top eight active members in your project by last activity View and find all members you collaborate with across projects
</p> </p>
</div> </div>
<div className="flex min-w-72 items-center justify-start gap-2 rounded-md border border-custom-border-200 px-2.5 py-1.5 placeholder:text-custom-text-400"> <div className="flex min-w-72 items-center justify-start gap-2 rounded-md border border-custom-border-200 px-2.5 py-1.5 placeholder:text-custom-text-400">

View File

@ -48,6 +48,9 @@ export const SearchedCollaboratorsList: React.FC<Props> = (props) => {
/> />
); );
const showViewMoreButton = pageCount < totalPages && resultsCount !== 0;
const showViewLessButton = pageCount > 1;
const emptyStateImage = resolvedTheme === "dark" ? DarkImage : LightImage; const emptyStateImage = resolvedTheme === "dark" ? DarkImage : LightImage;
return ( return (
@ -63,16 +66,28 @@ export const SearchedCollaboratorsList: React.FC<Props> = (props) => {
<p className="font-medium text-sm">No matching member</p> <p className="font-medium text-sm">No matching member</p>
</div> </div>
)} )}
{pageCount < totalPages && resultsCount !== 0 && ( {(showViewLessButton || showViewMoreButton) && (
<div className="flex items-center justify-center text-xs w-full"> <div className="flex items-center justify-center text-xs w-full">
{showViewLessButton && (
<Button
variant="link-primary"
size="sm"
className="my-3 hover:bg-custom-primary-100/20"
onClick={() => setPageCount(1)}
>
View less
</Button>
)}
{showViewMoreButton && (
<Button <Button
variant="link-primary" variant="link-primary"
size="sm" size="sm"
className="my-3 hover:bg-custom-primary-100/20" className="my-3 hover:bg-custom-primary-100/20"
onClick={handleLoadMore} onClick={handleLoadMore}
> >
Load more View more
</Button> </Button>
)}
</div> </div>
)} )}
</> </>

View File

@ -4,7 +4,7 @@ import { cn } from "helpers/common.helper";
// types // types
import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS } from "./constants"; import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS } from "./constants";
import { TButtonVariants } from "./types"; import { TButtonVariants } from "./types";
// constants import { usePlatformOS } from "hooks/use-platform-os";
export type DropdownButtonProps = { export type DropdownButtonProps = {
children: React.ReactNode; children: React.ReactNode;
@ -27,7 +27,6 @@ type ButtonProps = {
export const DropdownButton: React.FC<DropdownButtonProps> = (props) => { export const DropdownButton: React.FC<DropdownButtonProps> = (props) => {
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip, variant } = props; const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip, variant } = props;
const ButtonToRender: React.FC<ButtonProps> = BORDER_BUTTON_VARIANTS.includes(variant) const ButtonToRender: React.FC<ButtonProps> = BORDER_BUTTON_VARIANTS.includes(variant)
? BorderButton ? BorderButton
: BACKGROUND_BUTTON_VARIANTS.includes(variant) : BACKGROUND_BUTTON_VARIANTS.includes(variant)
@ -49,9 +48,10 @@ export const DropdownButton: React.FC<DropdownButtonProps> = (props) => {
const BorderButton: React.FC<ButtonProps> = (props) => { const BorderButton: React.FC<ButtonProps> = (props) => {
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props; const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
const { isMobile } = usePlatformOS();
return ( return (
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}> <Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip} isMobile={isMobile}>
<div <div
className={cn( className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5", "h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
@ -67,9 +67,9 @@ const BorderButton: React.FC<ButtonProps> = (props) => {
const BackgroundButton: React.FC<ButtonProps> = (props) => { const BackgroundButton: React.FC<ButtonProps> = (props) => {
const { children, className, tooltipContent, tooltipHeading, showTooltip } = props; const { children, className, tooltipContent, tooltipHeading, showTooltip } = props;
const { isMobile } = usePlatformOS();
return ( return (
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}> <Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip} isMobile={isMobile}>
<div <div
className={cn( className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80", "h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
@ -84,9 +84,9 @@ const BackgroundButton: React.FC<ButtonProps> = (props) => {
const TransparentButton: React.FC<ButtonProps> = (props) => { const TransparentButton: React.FC<ButtonProps> = (props) => {
const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props; const { children, className, isActive, tooltipContent, tooltipHeading, showTooltip } = props;
const { isMobile } = usePlatformOS();
return ( return (
<Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip}> <Tooltip tooltipHeading={tooltipHeading} tooltipContent={tooltipContent} disabled={!showTooltip} isMobile={isMobile}>
<div <div
className={cn( className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80", "h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",

View File

@ -8,6 +8,7 @@ import { cn } from "helpers/common.helper";
import { useModule } from "hooks/store"; import { useModule } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { usePlatformOS } from "hooks/use-platform-os";
// components // components
import { DropdownButton } from "../buttons"; import { DropdownButton } from "../buttons";
// icons // icons
@ -47,6 +48,7 @@ type ButtonContentProps = {
onChange: (moduleIds: string[]) => void; onChange: (moduleIds: string[]) => void;
placeholder: string; placeholder: string;
showCount: boolean; showCount: boolean;
showTooltip?: boolean;
value: string | string[] | null; value: string | string[] | null;
}; };
@ -60,10 +62,12 @@ const ButtonContent: React.FC<ButtonContentProps> = (props) => {
onChange, onChange,
placeholder, placeholder,
showCount, showCount,
showTooltip = false,
value, value,
} = props; } = props;
// store hooks // store hooks
const { getModuleById } = useModule(); const { getModuleById } = useModule();
const { isMobile } = usePlatformOS();
if (Array.isArray(value)) if (Array.isArray(value))
return ( return (
@ -90,12 +94,12 @@ const ButtonContent: React.FC<ButtonContentProps> = (props) => {
> >
{!hideIcon && <DiceIcon className="h-2.5 w-2.5 flex-shrink-0" />} {!hideIcon && <DiceIcon className="h-2.5 w-2.5 flex-shrink-0" />}
{!hideText && ( {!hideText && (
<Tooltip tooltipHeading="Title" tooltipContent={moduleDetails?.name}> <Tooltip tooltipHeading="Title" tooltipContent={moduleDetails?.name} disabled={!showTooltip} isMobile={isMobile}>
<span className="max-w-40 flex-grow truncate text-xs font-medium">{moduleDetails?.name}</span> <span className="max-w-40 flex-grow truncate text-xs font-medium">{moduleDetails?.name}</span>
</Tooltip> </Tooltip>
)} )}
{!disabled && ( {!disabled && (
<Tooltip tooltipContent="Remove"> <Tooltip tooltipContent="Remove" disabled={!showTooltip} isMobile={isMobile}>
<button <button
type="button" type="button"
className="flex-shrink-0" className="flex-shrink-0"
@ -265,6 +269,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)} hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
placeholder={placeholder} placeholder={placeholder}
showCount={showCount} showCount={showCount}
showTooltip={showTooltip}
value={value} value={value}
onChange={onChange as any} onChange={onChange as any}
/> />

View File

@ -9,6 +9,7 @@ import { ISSUE_PRIORITIES } from "constants/issue";
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { usePlatformOS } from "hooks/use-platform-os";
// icons // icons
// helpers // helpers
// types // types
@ -61,8 +62,10 @@ const BorderButton = (props: ButtonProps) => {
none: "hover:bg-custom-background-80 border-custom-border-300", none: "hover:bg-custom-background-80 border-custom-border-300",
}; };
const { isMobile } = usePlatformOS();
return ( return (
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}> <Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip} isMobile={isMobile}>
<div <div
className={cn( className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5", "h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5",
@ -130,8 +133,10 @@ const BackgroundButton = (props: ButtonProps) => {
none: "bg-custom-background-80", none: "bg-custom-background-80",
}; };
const { isMobile } = usePlatformOS();
return ( return (
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}> <Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip} isMobile={isMobile}>
<div <div
className={cn( className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5", "h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5",
@ -200,8 +205,10 @@ const TransparentButton = (props: ButtonProps) => {
none: "hover:text-custom-text-300", none: "hover:text-custom-text-300",
}; };
const { isMobile } = usePlatformOS();
return ( return (
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip}> <Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!showTooltip} isMobile={isMobile}>
<div <div
className={cn( className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80", "h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",

View File

@ -9,12 +9,12 @@ import { useUser } from "hooks/store";
import { Button, TButtonVariant } from "@plane/ui"; import { Button, TButtonVariant } from "@plane/ui";
import { ComicBoxButton } from "./comic-box-button"; import { ComicBoxButton } from "./comic-box-button";
// constant // constant
import { EMPTY_STATE_DETAILS, EmptyStateKeys } from "constants/empty-state"; import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
export type EmptyStateProps = { export type EmptyStateProps = {
type: EmptyStateKeys; type: EmptyStateType;
size?: "sm" | "md" | "lg"; size?: "sm" | "md" | "lg";
layout?: "widget-simple" | "screen-detailed" | "screen-simple"; layout?: "widget-simple" | "screen-detailed" | "screen-simple";
additionalPath?: string; additionalPath?: string;

View File

@ -19,7 +19,7 @@ export const MonthChartView: FC<any> = observer(() => {
{monthBlocks?.map((block, rootIndex) => ( {monthBlocks?.map((block, rootIndex) => (
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col"> <div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
<div <div
className="w-full sticky top-0 z-[5] bg-custom-background-100" className="w-full sticky top-0 z-[5] bg-custom-background-100 flex-shrink-0"
style={{ style={{
height: `${HEADER_HEIGHT}px`, height: `${HEADER_HEIGHT}px`,
}} }}
@ -55,7 +55,7 @@ export const MonthChartView: FC<any> = observer(() => {
))} ))}
</div> </div>
</div> </div>
<div className="h-full w-full flex divide-x divide-custom-border-100/50"> <div className="h-full w-full flex-grow flex divide-x divide-custom-border-100/50">
{block?.children?.map((monthDay, index) => ( {block?.children?.map((monthDay, index) => (
<div <div
key={`column-${rootIndex}-${index}`} key={`column-${rootIndex}-${index}`}

View File

@ -9,6 +9,8 @@ import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-ti
// types // types
import { useGanttChart } from "../hooks/use-gantt-chart"; import { useGanttChart } from "../hooks/use-gantt-chart";
import { IBlockUpdateData, IGanttBlock } from "../types"; import { IBlockUpdateData, IGanttBlock } from "../types";
// hooks
import { usePlatformOS } from "hooks/use-platform-os";
type Props = { type Props = {
block: IGanttBlock; block: IGanttBlock;
@ -23,6 +25,8 @@ export const ChartAddBlock: React.FC<Props> = observer((props) => {
const [buttonStartDate, setButtonStartDate] = useState<Date | null>(null); const [buttonStartDate, setButtonStartDate] = useState<Date | null>(null);
// refs // refs
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
// hooks
const { isMobile } = usePlatformOS();
// chart hook // chart hook
const { currentViewData } = useGanttChart(); const { currentViewData } = useGanttChart();
@ -73,7 +77,7 @@ export const ChartAddBlock: React.FC<Props> = observer((props) => {
> >
<div ref={containerRef} className="h-full w-full" /> <div ref={containerRef} className="h-full w-full" />
{isButtonVisible && ( {isButtonVisible && (
<Tooltip tooltipContent={buttonStartDate && renderFormattedDate(buttonStartDate)}> <Tooltip tooltipContent={buttonStartDate && renderFormattedDate(buttonStartDate)} isMobile={isMobile}>
<button <button
type="button" type="button"
className="absolute top-1/2 -translate-x-1/2 -translate-y-1/2 h-8 w-8 bg-custom-background-80 p-1.5 rounded border border-custom-border-300 grid place-items-center text-custom-text-200 hover:text-custom-text-100" className="absolute top-1/2 -translate-x-1/2 -translate-y-1/2 h-8 w-8 bg-custom-background-80 p-1.5 rounded border border-custom-border-300 grid place-items-center text-custom-text-200 hover:text-custom-text-100"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,81 @@
import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Search, Plus, Briefcase } from "lucide-react"; import { Search, Plus, Briefcase, X, ListFilter } from "lucide-react";
// hooks // hooks
// ui import { useApplication, useEventTracker, useMember, useProject, useProjectFilter, useUser } from "hooks/store";
import { Breadcrumbs, Button } from "@plane/ui"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// constants // components
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
// constants
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
// components import { FiltersDropdown } from "components/issues";
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import { ProjectFiltersSelection, ProjectOrderByDropdown } from "components/project";
import { TProjectFilters } from "@plane/types";
export const ProjectsHeader = observer(() => { export const ProjectsHeader = observer(() => {
// states
const [isSearchOpen, setIsSearchOpen] = useState(false);
// refs
const inputRef = useRef<HTMLInputElement>(null);
// store hooks // store hooks
const { commandPalette: commandPaletteStore } = useApplication(); const {
commandPalette: commandPaletteStore,
router: { workspaceSlug },
} = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
} = useUser(); } = useUser();
const { workspaceProjectIds, searchQuery, setSearchQuery } = useProject(); const { workspaceProjectIds } = useProject();
const {
currentWorkspaceDisplayFilters: displayFilters,
currentWorkspaceFilters: filters,
updateFilters,
updateDisplayFilters,
searchQuery,
updateSearchQuery,
} = useProjectFilter();
const {
workspace: { workspaceMemberIds },
} = useMember();
// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
});
// auth
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const handleFilters = useCallback(
(key: keyof TProjectFilters, value: string | string[]) => {
if (!workspaceSlug) return;
const newValues = filters?.[key] ?? [];
if (Array.isArray(value))
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
else {
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(workspaceSlug, { [key]: newValues });
},
[filters, updateFilters, workspaceSlug]
);
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
else setIsSearchOpen(false);
}
};
return ( return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> <div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap"> <div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
@ -34,18 +89,74 @@ export const ProjectsHeader = observer(() => {
</Breadcrumbs> </Breadcrumbs>
</div> </div>
</div> </div>
<div className="flex w-full justify-end items-center gap-3"> <div className="w-full flex items-center justify-end gap-3">
{workspaceProjectIds && workspaceProjectIds?.length > 0 && ( {workspaceProjectIds && workspaceProjectIds?.length > 0 && (
<div className=" flex items-center justify-start gap-1 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5 text-custom-text-400"> <div className="flex items-center">
<Search className="h-3.5" /> {!isSearchOpen && (
<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 <input
className="border-none w-full bg-transparent text-sm focus:outline-none" ref={inputRef}
value={searchQuery} className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 focus:outline-none"
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search" placeholder="Search"
value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/> />
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
updateSearchQuery("");
setIsSearchOpen(false);
}}
>
<X className="h-3 w-3" />
</button>
)}
</div>
</div> </div>
)} )}
<ProjectOrderByDropdown
value={displayFilters?.order_by}
onChange={(val) => {
if (!workspaceSlug || val === displayFilters?.order_by) return;
updateDisplayFilters(workspaceSlug, {
order_by: val,
});
}}
/>
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
<ProjectFiltersSelection
displayFilters={displayFilters ?? {}}
filters={filters ?? {}}
handleFiltersUpdate={handleFilters}
handleDisplayFiltersUpdate={(val) => {
if (!workspaceSlug) return;
updateDisplayFilters(workspaceSlug, val);
}}
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
{isAuthorizedUser && ( {isAuthorizedUser && (
<Button <Button
prependIcon={<Plus />} prependIcon={<Plus />}
@ -54,9 +165,9 @@ export const ProjectsHeader = observer(() => {
setTrackElement("Projects page"); setTrackElement("Projects page");
commandPaletteStore.toggleCreateProjectModal(true); commandPaletteStore.toggleCreateProjectModal(true);
}} }}
className="items-center" className="items-center gap-1"
> >
<div className="hidden sm:block">Add</div> Project <span className="hidden sm:inline-block">Add</span> Project
</Button> </Button>
)} )}
</div> </div>

View File

@ -5,6 +5,7 @@ import { useRouter } from "next/router";
// icons // icons
import { CalendarDays } from "lucide-react"; import { CalendarDays } from "lucide-react";
// hooks // hooks
import { usePlatformOS } from "hooks/use-platform-os";
// ui // ui
import { Tooltip, PriorityIcon } from "@plane/ui"; import { Tooltip, PriorityIcon } from "@plane/ui";
// helpers // helpers
@ -33,7 +34,7 @@ export const InboxIssueListItem: FC<TInboxIssueListItem> = observer((props) => {
const { const {
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail();
const { isMobile } = usePlatformOS();
const inboxIssueDetail = getInboxIssueByIssueId(inboxId, issueId); const inboxIssueDetail = getInboxIssueByIssueId(inboxId, issueId);
const issue = getIssueById(issueId); const issue = getIssueById(issueId);
@ -83,10 +84,10 @@ export const InboxIssueListItem: FC<TInboxIssueListItem> = observer((props) => {
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Tooltip tooltipHeading="Priority" tooltipContent={`${issue.priority ?? "None"}`}> <Tooltip tooltipHeading="Priority" tooltipContent={`${issue.priority ?? "None"}`} isMobile={isMobile}>
<PriorityIcon priority={issue.priority ?? null} className="h-3.5 w-3.5" /> <PriorityIcon priority={issue.priority ?? null} className="h-3.5 w-3.5" />
</Tooltip> </Tooltip>
<Tooltip tooltipHeading="Created on" tooltipContent={`${renderFormattedDate(issue.created_at ?? "")}`}> <Tooltip tooltipHeading="Created on" tooltipContent={`${renderFormattedDate(issue.created_at ?? "")}`} isMobile={isMobile}>
<div className="flex items-center gap-1 rounded border border-custom-border-200 px-2 py-[0.19rem] text-xs text-custom-text-200 shadow-sm"> <div className="flex items-center gap-1 rounded border border-custom-border-200 px-2 py-[0.19rem] text-xs text-custom-text-200 shadow-sm">
<CalendarDays size={12} strokeWidth={1.5} /> <CalendarDays size={12} strokeWidth={1.5} />
<span>{renderFormattedDate(issue.created_at ?? "")}</span> <span>{renderFormattedDate(issue.created_at ?? "")}</span>

View File

@ -9,6 +9,7 @@ import { Menu, Transition } from "@headlessui/react";
// icons // icons
import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react"; import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react";
// hooks // hooks
import { usePlatformOS } from "hooks/use-platform-os";
import { Avatar, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; import { Avatar, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
import { useApplication, useUser } from "hooks/store"; import { useApplication, useUser } from "hooks/store";
// ui // ui
@ -34,7 +35,7 @@ export const InstanceSidebarDropdown = observer(() => {
const { signOut, currentUser, currentUserSettings } = useUser(); const { signOut, currentUser, currentUserSettings } = useUser();
// hooks // hooks
const { setTheme } = useTheme(); const { setTheme } = useTheme();
const { isMobile } = usePlatformOS();
// redirect url for normal mode // redirect url for normal mode
const redirectWorkspaceSlug = const redirectWorkspaceSlug =
workspaceSlug || workspaceSlug ||
@ -73,7 +74,7 @@ export const InstanceSidebarDropdown = observer(() => {
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<div className="flex w-full gap-2"> <div className="flex w-full gap-2">
<h4 className="grow truncate text-base font-medium text-custom-text-200">Instance admin</h4> <h4 className="grow truncate text-base font-medium text-custom-text-200">Instance admin</h4>
<Tooltip position="bottom-left" tooltipContent="Exit God Mode"> <Tooltip position="bottom-left" tooltipContent="Exit God Mode" isMobile={isMobile}>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Link href={`/${redirectWorkspaceSlug}`}> <Link href={`/${redirectWorkspaceSlug}`}>
<span> <span>

View File

@ -2,6 +2,7 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react"; import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
// hooks // hooks
import { usePlatformOS } from "hooks/use-platform-os";
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
import { useApplication } from "hooks/store"; import { useApplication } from "hooks/store";
// ui // ui
@ -46,6 +47,7 @@ export const InstanceAdminSidebarMenu = () => {
} = useApplication(); } = useApplication();
// router // router
const router = useRouter(); const router = useRouter();
const { isMobile } = usePlatformOS();
return ( return (
<div className="flex h-full w-full flex-col gap-2.5 overflow-y-auto px-4 py-6"> <div className="flex h-full w-full flex-col gap-2.5 overflow-y-auto px-4 py-6">
@ -55,7 +57,7 @@ export const InstanceAdminSidebarMenu = () => {
return ( return (
<Link key={index} href={item.href}> <Link key={index} href={item.href}>
<div> <div>
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!sidebarCollapsed}> <Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!sidebarCollapsed} isMobile={isMobile}>
<div <div
className={`group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none ${ className={`group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none ${
isActive isActive

View File

@ -11,6 +11,7 @@ import { WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
// hooks // hooks
import { useApplication, useUser } from "hooks/store"; import { useApplication, useUser } from "hooks/store";
import useIntegrationPopup from "hooks/use-integration-popup"; import useIntegrationPopup from "hooks/use-integration-popup";
import { usePlatformOS } from "hooks/use-platform-os";
// services // services
import { IntegrationService } from "services/integrations"; import { IntegrationService } from "services/integrations";
// icons // icons
@ -55,7 +56,7 @@ export const SingleIntegrationCard: React.FC<Props> = observer(({ integration })
} = useUser(); } = useUser();
const isUserAdmin = currentWorkspaceRole === 20; const isUserAdmin = currentWorkspaceRole === 20;
const { isMobile } = usePlatformOS();
const { startAuth, isConnecting: isInstalling } = useIntegrationPopup({ const { startAuth, isConnecting: isInstalling } = useIntegrationPopup({
provider: integration.provider, provider: integration.provider,
github_app_name: envConfig?.github_app_name || "", github_app_name: envConfig?.github_app_name || "",
@ -129,6 +130,7 @@ export const SingleIntegrationCard: React.FC<Props> = observer(({ integration })
{workspaceIntegrations ? ( {workspaceIntegrations ? (
isInstalled ? ( isInstalled ? (
<Tooltip <Tooltip
isMobile={isMobile}
disabled={isUserAdmin} disabled={isUserAdmin}
tooltipContent={!isUserAdmin ? "You don't have permission to perform this" : null} tooltipContent={!isUserAdmin ? "You don't have permission to perform this" : null}
> >
@ -147,6 +149,7 @@ export const SingleIntegrationCard: React.FC<Props> = observer(({ integration })
</Tooltip> </Tooltip>
) : ( ) : (
<Tooltip <Tooltip
isMobile={isMobile}
disabled={isUserAdmin} disabled={isUserAdmin}
tooltipContent={!isUserAdmin ? "You don't have permission to perform this" : null} tooltipContent={!isUserAdmin ? "You don't have permission to perform this" : null}
> >

View File

@ -2,6 +2,7 @@ import { FC, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { AlertCircle, X } from "lucide-react"; import { AlertCircle, X } from "lucide-react";
// hooks // hooks
import { usePlatformOS } from "hooks/use-platform-os";
// ui // ui
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// components // components
@ -34,7 +35,7 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = (props) => {
} = useIssueDetail(); } = useIssueDetail();
// states // states
const [attachmentDeleteModal, setAttachmentDeleteModal] = useState<boolean>(false); const [attachmentDeleteModal, setAttachmentDeleteModal] = useState<boolean>(false);
const { isMobile } = usePlatformOS();
const attachment = attachmentId && getAttachmentById(attachmentId); const attachment = attachmentId && getAttachmentById(attachmentId);
if (!attachment) return <></>; if (!attachment) return <></>;
@ -56,10 +57,11 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = (props) => {
<div className="h-7 w-7">{getFileIcon(getFileExtension(attachment.asset))}</div> <div className="h-7 w-7">{getFileIcon(getFileExtension(attachment.asset))}</div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Tooltip tooltipContent={getFileName(attachment.attributes.name)}> <Tooltip tooltipContent={getFileName(attachment.attributes.name)} isMobile={isMobile}>
<span className="text-sm">{truncateText(`${getFileName(attachment.attributes.name)}`, 10)}</span> <span className="text-sm">{truncateText(`${getFileName(attachment.attributes.name)}`, 10)}</span>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
isMobile={isMobile}
tooltipContent={`${ tooltipContent={`${
getUserDetails(attachment.updated_by)?.display_name ?? "" getUserDetails(attachment.updated_by)?.display_name ?? ""
} uploaded on ${renderFormattedDate(attachment.updated_at)}`} } uploaded on ${renderFormattedDate(attachment.updated_at)}`}

View File

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

View File

@ -2,6 +2,7 @@ import { FC } from "react";
// hooks // hooks
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
import { useIssueDetail } from "hooks/store"; import { useIssueDetail } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// ui // ui
type TIssueLink = { type TIssueLink = {
@ -14,12 +15,12 @@ export const IssueLink: FC<TIssueLink> = (props) => {
const { const {
activity: { getActivityById }, activity: { getActivityById },
} = useIssueDetail(); } = useIssueDetail();
const { isMobile } = usePlatformOS();
const activity = getActivityById(activityId); const activity = getActivityById(activityId);
if (!activity) return <></>; if (!activity) return <></>;
return ( return (
<Tooltip tooltipContent={activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"}> <Tooltip tooltipContent={activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"} isMobile={isMobile}>
<a <a
aria-disabled={activity.issue === null} aria-disabled={activity.issue === null}
href={`${ href={`${

View File

@ -1,4 +1,5 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react";
// components // components
import { useIssueDetail } from "hooks/store"; import { useIssueDetail } from "hooks/store";
import { LabelListItem } from "./label-list-item"; import { LabelListItem } from "./label-list-item";
@ -14,7 +15,7 @@ type TLabelList = {
disabled: boolean; disabled: boolean;
}; };
export const LabelList: FC<TLabelList> = (props) => { export const LabelList: FC<TLabelList> = observer((props) => {
const { workspaceSlug, projectId, issueId, labelOperations, disabled } = props; const { workspaceSlug, projectId, issueId, labelOperations, disabled } = props;
// hooks // hooks
const { const {
@ -40,4 +41,4 @@ export const LabelList: FC<TLabelList> = (props) => {
))} ))}
</> </>
); );
}; });

View File

@ -9,6 +9,7 @@ import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
import { calculateTimeAgo } from "helpers/date-time.helper"; import { calculateTimeAgo } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
import { useIssueDetail, useMember } from "hooks/store"; import { useIssueDetail, useMember } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal"; import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal";
export type TIssueLinkDetail = { export type TIssueLinkDetail = {
@ -33,7 +34,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
toggleIssueLinkModalStore(modalToggle); toggleIssueLinkModalStore(modalToggle);
setIsIssueLinkModalOpen(modalToggle); setIsIssueLinkModalOpen(modalToggle);
}; };
const { isMobile } = usePlatformOS();
const linkDetail = getLinkById(linkId); const linkDetail = getLinkById(linkId);
if (!linkDetail) return <></>; if (!linkDetail) return <></>;
@ -64,7 +65,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
<span className="py-1"> <span className="py-1">
<LinkIcon className="h-3 w-3 flex-shrink-0" /> <LinkIcon className="h-3 w-3 flex-shrink-0" />
</span> </span>
<Tooltip tooltipContent={linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url}> <Tooltip tooltipContent={linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url} isMobile={isMobile}>
<span className="truncate text-xs"> <span className="truncate text-xs">
{linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url} {linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url}
</span> </span>

View File

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

View File

@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
import Link from "next/link"; import Link from "next/link";
import { Pencil, X } from "lucide-react"; import { Pencil, X } from "lucide-react";
// hooks // hooks
import { usePlatformOS } from "hooks/use-platform-os";
// components // components
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
import { ParentIssuesListModal } from "components/issues"; import { ParentIssuesListModal } from "components/issues";
@ -35,7 +36,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
const parentIssue = issue?.parent_id ? getIssueById(issue.parent_id) : undefined; const parentIssue = issue?.parent_id ? getIssueById(issue.parent_id) : undefined;
const parentIssueProjectDetails = const parentIssueProjectDetails =
parentIssue && parentIssue.project_id ? getProjectById(parentIssue.project_id) : undefined; parentIssue && parentIssue.project_id ? getProjectById(parentIssue.project_id) : undefined;
const { isMobile } = usePlatformOS();
const handleParentIssue = async (_issueId: string | null = null) => { const handleParentIssue = async (_issueId: string | null = null) => {
try { try {
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId }); await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId });
@ -73,7 +74,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
> >
{issue.parent_id && parentIssue ? ( {issue.parent_id && parentIssue ? (
<div className="flex items-center gap-1 bg-green-500/20 text-green-700 rounded px-1.5 py-1"> <div className="flex items-center gap-1 bg-green-500/20 text-green-700 rounded px-1.5 py-1">
<Tooltip tooltipHeading="Title" tooltipContent={parentIssue.name}> <Tooltip tooltipHeading="Title" tooltipContent={parentIssue.name} isMobile={isMobile}>
<Link <Link
href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`} href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`}
target="_blank" target="_blank"
@ -86,7 +87,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
</Tooltip> </Tooltip>
{!disabled && ( {!disabled && (
<Tooltip tooltipContent="Remove" position="bottom"> <Tooltip tooltipContent="Remove" position="bottom" isMobile={isMobile}>
<span <span
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();

View File

@ -1,10 +1,11 @@
import { FC, useMemo } from "react"; import { FC, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components // components
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
import { useIssueDetail } from "hooks/store"; import { useIssueDetail, useMember } from "hooks/store";
// ui // helper
import { formatTextList } from "helpers/issue.helper";
// types // types
import { IUser } from "@plane/types"; import { IUser } from "@plane/types";
import { ReactionSelector } from "./reaction-selector"; import { ReactionSelector } from "./reaction-selector";
@ -21,10 +22,11 @@ export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props)
// hooks // hooks
const { const {
commentReaction: { getCommentReactionsByCommentId, commentReactionsByUser }, commentReaction: { getCommentReactionsByCommentId, commentReactionsByUser, getCommentReactionById },
createCommentReaction, createCommentReaction,
removeCommentReaction, removeCommentReaction,
} = useIssueDetail(); } = useIssueDetail();
const { getUserDetails } = useMember();
const reactionIds = getCommentReactionsByCommentId(commentId); const reactionIds = getCommentReactionsByCommentId(commentId);
const userReactions = commentReactionsByUser(commentId, currentUser.id).map((r) => r.reaction); const userReactions = commentReactionsByUser(commentId, currentUser.id).map((r) => r.reaction);
@ -73,6 +75,17 @@ export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props)
[workspaceSlug, projectId, commentId, currentUser, createCommentReaction, removeCommentReaction, userReactions] [workspaceSlug, projectId, commentId, currentUser, createCommentReaction, removeCommentReaction, userReactions]
); );
const getReactionUsers = (reaction: string): string => {
const reactionUsers = (reactionIds?.[reaction] || [])
.map((reactionId) => {
const reactionDetails = getCommentReactionById(reactionId);
return reactionDetails ? getUserDetails(reactionDetails.actor)?.display_name : null;
})
.filter((displayName): displayName is string => !!displayName);
const formattedUsers = formatTextList(reactionUsers);
return formattedUsers;
};
return ( return (
<div className="mt-4 relative flex items-center gap-1.5"> <div className="mt-4 relative flex items-center gap-1.5">
<ReactionSelector <ReactionSelector
@ -87,6 +100,7 @@ export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props)
(reaction) => (reaction) =>
reactionIds[reaction]?.length > 0 && ( reactionIds[reaction]?.length > 0 && (
<> <>
<Tooltip tooltipContent={getReactionUsers(reaction)}>
<button <button
type="button" type="button"
onClick={() => issueCommentReactionOperations.react(reaction)} onClick={() => issueCommentReactionOperations.react(reaction)}
@ -100,6 +114,7 @@ export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props)
{(reactionIds || {})[reaction].length}{" "} {(reactionIds || {})[reaction].length}{" "}
</span> </span>
</button> </button>
</Tooltip>
</> </>
) )
)} )}

View File

@ -1,10 +1,12 @@
import { FC, useMemo } from "react"; import { FC, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components // hooks
import { TOAST_TYPE, setToast } from "@plane/ui"; import { useIssueDetail, useMember } from "hooks/store";
import { renderEmoji } from "helpers/emoji.helper";
import { useIssueDetail } from "hooks/store";
// ui // ui
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
import { formatTextList } from "helpers/issue.helper";
// types // types
import { IUser } from "@plane/types"; import { IUser } from "@plane/types";
import { ReactionSelector } from "./reaction-selector"; import { ReactionSelector } from "./reaction-selector";
@ -20,10 +22,11 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
const { workspaceSlug, projectId, issueId, currentUser } = props; const { workspaceSlug, projectId, issueId, currentUser } = props;
// hooks // hooks
const { const {
reaction: { getReactionsByIssueId, reactionsByUser }, reaction: { getReactionsByIssueId, reactionsByUser, getReactionById },
createReaction, createReaction,
removeReaction, removeReaction,
} = useIssueDetail(); } = useIssueDetail();
const { getUserDetails } = useMember();
const reactionIds = getReactionsByIssueId(issueId); const reactionIds = getReactionsByIssueId(issueId);
const userReactions = reactionsByUser(issueId, currentUser.id).map((r) => r.reaction); const userReactions = reactionsByUser(issueId, currentUser.id).map((r) => r.reaction);
@ -72,6 +75,18 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
[workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, userReactions] [workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, userReactions]
); );
const getReactionUsers = (reaction: string): string => {
const reactionUsers = (reactionIds?.[reaction] || [])
.map((reactionId) => {
const reactionDetails = getReactionById(reactionId);
return reactionDetails ? getUserDetails(reactionDetails.actor_id)?.display_name : null;
})
.filter((displayName): displayName is string => !!displayName);
const formattedUsers = formatTextList(reactionUsers);
return formattedUsers;
};
return ( return (
<div className="mt-4 relative flex items-center gap-1.5"> <div className="mt-4 relative flex items-center gap-1.5">
<ReactionSelector size="md" position="top" value={userReactions} onSelect={issueReactionOperations.react} /> <ReactionSelector size="md" position="top" value={userReactions} onSelect={issueReactionOperations.react} />
@ -81,6 +96,7 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
(reaction) => (reaction) =>
reactionIds[reaction]?.length > 0 && ( reactionIds[reaction]?.length > 0 && (
<> <>
<Tooltip tooltipContent={getReactionUsers(reaction)}>
<button <button
type="button" type="button"
onClick={() => issueReactionOperations.react(reaction)} onClick={() => issueReactionOperations.react(reaction)}
@ -94,6 +110,7 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
{(reactionIds || {})[reaction].length}{" "} {(reactionIds || {})[reaction].length}{" "}
</span> </span>
</button> </button>
</Tooltip>
</> </>
) )
)} )}

View File

@ -7,6 +7,7 @@ import { RelatedIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
import { ExistingIssuesListModal } from "components/core"; import { ExistingIssuesListModal } from "components/core";
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
import { useIssueDetail, useIssues, useProject } from "hooks/store"; import { useIssueDetail, useIssues, useProject } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// components // components
// ui // ui
// helpers // helpers
@ -59,7 +60,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
toggleRelationModal, toggleRelationModal,
} = useIssueDetail(); } = useIssueDetail();
const { issueMap } = useIssues(); const { issueMap } = useIssues();
const { isMobile } = usePlatformOS();
const relationIssueIds = getRelationByIssueIdRelationType(issueId, relationKey); const relationIssueIds = getRelationByIssueIdRelationType(issueId, relationKey);
const onSubmit = async (data: ISearchIssueResponse[]) => { const onSubmit = async (data: ISearchIssueResponse[]) => {
@ -124,7 +125,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
key={relationIssueId} key={relationIssueId}
className={`group flex items-center gap-1 rounded px-1.5 pb-1 pt-1 leading-3 hover:bg-custom-background-90 ${issueRelationObject[relationKey].className}`} className={`group flex items-center gap-1 rounded px-1.5 pb-1 pt-1 leading-3 hover:bg-custom-background-90 ${issueRelationObject[relationKey].className}`}
> >
<Tooltip tooltipHeading="Title" tooltipContent={currentIssue.name}> <Tooltip tooltipHeading="Title" tooltipContent={currentIssue.name} isMobile={isMobile}>
<Link <Link
href={`/${workspaceSlug}/projects/${projectDetails?.id}/issues/${currentIssue.id}`} href={`/${workspaceSlug}/projects/${projectDetails?.id}/issues/${currentIssue.id}`}
target="_blank" target="_blank"
@ -136,7 +137,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
</Link> </Link>
</Tooltip> </Tooltip>
{!disabled && ( {!disabled && (
<Tooltip tooltipContent="Remove" position="bottom"> <Tooltip tooltipContent="Remove" position="bottom" isMobile={isMobile}>
<span <span
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();

View File

@ -376,7 +376,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
/> />
</div> </div>
<div <div
className="fixed right-0 z-[5] h-full w-full min-w-[300px] space-y-5 overflow-hidden border-l border-custom-border-200 bg-custom-sidebar-background-100 py-5 sm:w-1/2 md:relative md:w-1/3 lg:min-w-80 xl:min-w-96" className="fixed right-0 z-[5] h-full w-full min-w-[300px] overflow-hidden border-l border-custom-border-200 bg-custom-sidebar-background-100 py-5 sm:w-1/2 md:relative md:w-1/3 lg:min-w-80 xl:min-w-96"
style={themeStore.issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}} style={themeStore.issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
> >
<IssueDetailsSidebar <IssueDetailsSidebar

View File

@ -15,6 +15,7 @@ import {
CalendarCheck2, CalendarCheck2,
} from "lucide-react"; } from "lucide-react";
// hooks // hooks
import { usePlatformOS } from "hooks/use-platform-os";
// components // components
import { import {
ArchiveIcon, ArchiveIcon,
@ -78,7 +79,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail();
const { getStateById } = useProjectState(); const { getStateById } = useProjectState();
const { isMobile } = usePlatformOS();
const issue = getIssueById(issueId); const issue = getIssueById(issueId);
if (!issue) return <></>; if (!issue) return <></>;
@ -138,7 +139,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} /> <IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
)} )}
<div className="flex flex-wrap items-center gap-2.5 text-custom-text-300"> <div className="flex flex-wrap items-center gap-2.5 text-custom-text-300">
<Tooltip tooltipContent="Copy link"> <Tooltip tooltipContent="Copy link" isMobile={isMobile}>
<button <button
type="button" type="button"
className="grid h-5 w-5 place-items-center rounded hover:text-custom-text-200 focus:outline-none focus:ring-2 focus:ring-custom-primary" className="grid h-5 w-5 place-items-center rounded hover:text-custom-text-200 focus:outline-none focus:ring-2 focus:ring-custom-primary"
@ -149,6 +150,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
</Tooltip> </Tooltip>
{isArchivingAllowed && ( {isArchivingAllowed && (
<Tooltip <Tooltip
isMobile={isMobile}
tooltipContent={isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"} tooltipContent={isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"}
> >
<button <button
@ -170,7 +172,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
</Tooltip> </Tooltip>
)} )}
{is_editable && ( {is_editable && (
<Tooltip tooltipContent="Delete"> <Tooltip tooltipContent="Delete" isMobile={isMobile}>
<button <button
type="button" type="button"
className="grid h-5 w-5 place-items-center rounded hover:text-custom-text-200 focus:outline-none focus:ring-2 focus:ring-custom-primary" className="grid h-5 w-5 place-items-center rounded hover:text-custom-text-200 focus:outline-none focus:ring-2 focus:ring-custom-primary"

View File

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

View File

@ -1,7 +1,8 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import isEmpty from "lodash/isEmpty";
// hooks // hooks
import { useApplication, useEventTracker, useIssues } from "hooks/store"; import { useApplication, useCycle, useEventTracker, useIssues } from "hooks/store";
// ui // ui
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
import { ExistingIssuesListModal } from "components/core"; import { ExistingIssuesListModal } from "components/core";
@ -27,12 +28,15 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
// states // states
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
// store hooks // store hooks
const { getCycleById } = useCycle();
const { issues } = useIssues(EIssuesStoreType.CYCLE); const { issues } = useIssues(EIssuesStoreType.CYCLE);
const { const {
commandPalette: { toggleCreateIssueModal }, commandPalette: { toggleCreateIssueModal },
} = useApplication(); } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId || !cycleId) return;
@ -56,8 +60,14 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
); );
}; };
const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_CYCLE_NO_ISSUES; const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {});
const additionalPath = activeLayout ?? "list";
const emptyStateType = isCompletedCycleSnapshotAvailable
? EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES
: isEmptyFilters
? EmptyStateType.PROJECT_EMPTY_FILTER
: EmptyStateType.PROJECT_CYCLE_NO_ISSUES;
const additionalPath = isCompletedCycleSnapshotAvailable ? undefined : activeLayout ?? "list";
const emptyStateSize = isEmptyFilters ? "lg" : "sm"; const emptyStateSize = isEmptyFilters ? "lg" : "sm";
return ( return (
@ -76,14 +86,18 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
additionalPath={additionalPath} additionalPath={additionalPath}
size={emptyStateSize} size={emptyStateSize}
primaryButtonOnClick={ primaryButtonOnClick={
isEmptyFilters !isCompletedCycleSnapshotAvailable && !isEmptyFilters
? undefined ? () => {
: () => {
setTrackElement("Cycle issue empty state"); setTrackElement("Cycle issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
} }
: undefined
}
secondaryButtonOnClick={
!isCompletedCycleSnapshotAvailable && isEmptyFilters
? handleClearAllFilters
: () => setCycleIssuesListModal(true)
} }
secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setCycleIssuesListModal(true)}
/> />
</div> </div>
</> </>

View File

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

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
import { DateFilterModal } from "components/core"; import { DateFilterModal } from "components/core";
import { FilterHeader, FilterOption } from "components/issues"; import { FilterHeader, FilterOption } from "components/issues";
// constants // constants
import { DATE_FILTER_OPTIONS } from "constants/filters"; import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
type Props = { type Props = {
appliedFilters: string[] | null; appliedFilters: string[] | null;
@ -21,7 +21,9 @@ export const FilterStartDate: React.FC<Props> = observer((props) => {
const appliedFiltersCount = appliedFilters?.length ?? 0; const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase())); const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) =>
d.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return ( return (
<> <>

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
import { DateFilterModal } from "components/core"; import { DateFilterModal } from "components/core";
import { FilterHeader, FilterOption } from "components/issues"; import { FilterHeader, FilterOption } from "components/issues";
// constants // constants
import { DATE_FILTER_OPTIONS } from "constants/filters"; import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
type Props = { type Props = {
appliedFilters: string[] | null; appliedFilters: string[] | null;
@ -21,7 +21,9 @@ export const FilterTargetDate: React.FC<Props> = observer((props) => {
const appliedFiltersCount = appliedFilters?.length ?? 0; const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase())); const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) =>
d.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return ( return (
<> <>

View File

@ -5,7 +5,8 @@ import { Tooltip } from "@plane/ui";
// types // types
import { ISSUE_LAYOUTS } from "constants/issue"; import { ISSUE_LAYOUTS } from "constants/issue";
import { TIssueLayouts } from "@plane/types"; import { TIssueLayouts } from "@plane/types";
// constants // hooks
import { usePlatformOS } from "hooks/use-platform-os";
type Props = { type Props = {
layouts: TIssueLayouts[]; layouts: TIssueLayouts[];
@ -15,11 +16,12 @@ type Props = {
export const LayoutSelection: React.FC<Props> = (props) => { export const LayoutSelection: React.FC<Props> = (props) => {
const { layouts, onChange, selectedLayout } = props; const { layouts, onChange, selectedLayout } = props;
const { isMobile } = usePlatformOS();
return ( return (
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1"> <div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => ( {ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => (
<Tooltip key={layout.key} tooltipContent={layout.title}> <Tooltip key={layout.key} tooltipContent={layout.title} isMobile={isMobile}>
<button <button
type="button" type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${ className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${

View File

@ -1,5 +1,6 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// hooks // hooks
import { usePlatformOS } from "hooks/use-platform-os";
// ui // ui
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui"; import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
// helpers // helpers
@ -31,6 +32,7 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
issueDetails && issueDetails &&
!issueDetails.tempId && !issueDetails.tempId &&
setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id }); setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id });
const { isMobile } = usePlatformOS();
return ( return (
<div <div
@ -42,6 +44,7 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
> >
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" /> <div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
<Tooltip <Tooltip
isMobile={isMobile}
tooltipContent={ tooltipContent={
<div className="space-y-1"> <div className="space-y-1">
<h5>{issueDetails?.name}</h5> <h5>{issueDetails?.name}</h5>
@ -83,6 +86,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
workspaceSlug && workspaceSlug &&
issueDetails && issueDetails &&
setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id }); setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id });
const { isMobile } = usePlatformOS();
return ( return (
<ControlLink <ControlLink
@ -97,7 +101,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
<div className="flex-shrink-0 text-xs text-custom-text-300"> <div className="flex-shrink-0 text-xs text-custom-text-300">
{projectIdentifier} {issueDetails?.sequence_id} {projectIdentifier} {issueDetails?.sequence_id}
</div> </div>
<Tooltip tooltipContent={issueDetails?.name}> <Tooltip tooltipContent={issueDetails?.name} isMobile={isMobile}>
<span className="flex-grow truncate text-sm font-medium">{issueDetails?.name}</span> <span className="flex-grow truncate text-sm font-medium">{issueDetails?.name}</span>
</Tooltip> </Tooltip>
</div> </div>

View File

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

View File

@ -1,11 +1,12 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components // components
// hooks // hooks
import { usePlatformOS } from "hooks/use-platform-os";
import { useApplication, useIssueDetail, useProject } from "hooks/store";
// ui // ui
import { Spinner, Tooltip, ControlLink } from "@plane/ui"; import { Spinner, Tooltip, ControlLink } from "@plane/ui";
// helper // helper
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
import { useApplication, useIssueDetail, useProject } from "hooks/store";
// types // types
import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types"; import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types";
import { IssueProperties } from "../properties/all-properties"; import { IssueProperties } from "../properties/all-properties";
@ -36,7 +37,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id }); setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
const issue = issuesMap[issueId]; const issue = issuesMap[issueId];
const { isMobile } = usePlatformOS();
if (!issue) return null; if (!issue) return null;
const canEditIssueProperties = canEditProperties(issue.project_id); const canEditIssueProperties = canEditProperties(issue.project_id);
@ -44,11 +45,16 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
return ( return (
<div <div
className={cn("min-h-12 relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm", { className={cn(
"min-h-12 relative flex flex-col md:flex-row md:items-center gap-3 bg-custom-background-100 p-3 text-sm",
{
"border border-custom-primary-70 hover:border-custom-primary-70": peekIssue && peekIssue.issueId === issue.id, "border border-custom-primary-70 hover:border-custom-primary-70": peekIssue && peekIssue.issueId === issue.id,
"last:border-b-transparent": peekIssue?.issueId !== issue.id, "last:border-b-transparent": peekIssue?.issueId !== issue.id,
})} }
)}
> >
<div className="flex">
<div className="flex flex-grow items-center gap-3">
{displayProperties && displayProperties?.key && ( {displayProperties && displayProperties?.key && (
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300"> <div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
{projectIdentifier}-{issue.sequence_id} {projectIdentifier}-{issue.sequence_id}
@ -60,37 +66,41 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
)} )}
{issue?.is_draft ? ( {issue?.is_draft ? (
<Tooltip tooltipContent={issue.name}> <Tooltip tooltipContent={issue.name} isMobile={isMobile}>
<span>{issue.name}</span> <span>{issue.name}</span>
</Tooltip> </Tooltip>
) : ( ) : (
<ControlLink <ControlLink
href={`/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archived-issues" : "issues"}/${ href={`/${workspaceSlug}/projects/${issue.project_id}/${
issue.id issue.archived_at ? "archived-issues" : "issues"
}`} }/${issue.id}`}
target="_blank" target="_blank"
onClick={() => handleIssuePeekOverview(issue)} onClick={() => handleIssuePeekOverview(issue)}
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
disabled={!!issue?.tempId} disabled={!!issue?.tempId}
> >
<Tooltip tooltipContent={issue.name}> <Tooltip tooltipContent={issue.name} isMobile={isMobile}>
<span>{issue.name}</span> <span>{issue.name}</span>
</Tooltip> </Tooltip>
</ControlLink> </ControlLink>
)} )}
</div>
<div className="ml-auto flex flex-shrink-0 items-center gap-2"> {!issue?.tempId && (
<div className="block md:hidden border border-custom-border-300 rounded ">{quickActions(issue)}</div>
)}
</div>
<div className="ml-0 md:ml-auto flex flex-wrap md:flex-shrink-0 items-center gap-2">
{!issue?.tempId ? ( {!issue?.tempId ? (
<> <>
<IssueProperties <IssueProperties
className="relative flex items-center gap-2 whitespace-nowrap" className="relative flex flex-wrap md:flex-grow md:flex-shrink-0 items-center gap-2 whitespace-nowrap"
issue={issue} issue={issue}
isReadOnly={!canEditIssueProperties} isReadOnly={!canEditIssueProperties}
updateIssue={updateIssue} updateIssue={updateIssue}
displayProperties={displayProperties} displayProperties={displayProperties}
activeLayout="List" activeLayout="List"
/> />
{quickActions(issue)} <div className="hidden md:block">{quickActions(issue)}</div>
</> </>
) : ( ) : (
<div className="h-4 w-4"> <div className="h-4 w-4">

View File

@ -123,7 +123,10 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
const isGroupByCreatedBy = group_by === "created_by"; const isGroupByCreatedBy = group_by === "created_by";
return ( return (
<div ref={containerRef} className="vertical-scrollbar scrollbar-lg relative h-full w-full overflow-auto"> <div
ref={containerRef}
className="vertical-scrollbar scrollbar-lg relative h-full w-full overflow-auto vertical-scrollbar-margin-top-md"
>
{groups && {groups &&
groups.length > 0 && groups.length > 0 &&
groups.map( groups.map(

View File

@ -20,6 +20,7 @@ import { cn } from "helpers/common.helper";
import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store"; import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
// components // components
import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types";
import { IssuePropertyLabels } from "../properties/labels"; import { IssuePropertyLabels } from "../properties/labels";
@ -50,6 +51,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
} = useIssues(EIssuesStoreType.CYCLE); } = useIssues(EIssuesStoreType.CYCLE);
const { areEstimatesEnabledForCurrentProject } = useEstimate(); const { areEstimatesEnabledForCurrentProject } = useEstimate();
const { getStateById } = useProjectState(); const { getStateById } = useProjectState();
const { isMobile } = usePlatformOS();
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -391,7 +393,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
displayPropertyKey="sub_issue_count" displayPropertyKey="sub_issue_count"
shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!issue.sub_issues_count} shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!issue.sub_issues_count}
> >
<Tooltip tooltipHeading="Sub-issues" tooltipContent={`${issue.sub_issues_count}`}> <Tooltip tooltipHeading="Sub-issues" tooltipContent={`${issue.sub_issues_count}`} isMobile={isMobile}>
<div <div
onClick={issue.sub_issues_count ? redirectToIssueDetail : () => {}} onClick={issue.sub_issues_count ? redirectToIssueDetail : () => {}}
className={cn( className={cn(
@ -413,7 +415,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
displayPropertyKey="attachment_count" displayPropertyKey="attachment_count"
shouldRenderProperty={(properties) => !!properties.attachment_count && !!issue.attachment_count} shouldRenderProperty={(properties) => !!properties.attachment_count && !!issue.attachment_count}
> >
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}> <Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`} isMobile={isMobile}>
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1"> <div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1">
<Paperclip className="h-3 w-3 flex-shrink-0" strokeWidth={2} /> <Paperclip className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<div className="text-xs">{issue.attachment_count}</div> <div className="text-xs">{issue.attachment_count}</div>
@ -427,7 +429,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
displayPropertyKey="link" displayPropertyKey="link"
shouldRenderProperty={(properties) => !!properties.link && !!issue.link_count} shouldRenderProperty={(properties) => !!properties.link && !!issue.link_count}
> >
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}> <Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`} isMobile={isMobile}>
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1"> <div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1">
<Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} /> <Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<div className="text-xs">{issue.link_count}</div> <div className="text-xs">{issue.link_count}</div>

View File

@ -9,6 +9,7 @@ import { Tooltip } from "@plane/ui";
import { useApplication, useLabel } from "hooks/store"; import { useApplication, useLabel } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { usePlatformOS } from "hooks/use-platform-os";
// components // components
// types // types
import { IIssueLabel } from "@plane/types"; import { IIssueLabel } from "@plane/types";
@ -62,7 +63,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
router: { workspaceSlug }, router: { workspaceSlug },
} = useApplication(); } = useApplication();
const { fetchProjectLabels, getProjectLabels } = useLabel(); const { fetchProjectLabels, getProjectLabels } = useLabel();
const { isMobile } = usePlatformOS();
const storeLabels = getProjectLabels(projectId); const storeLabels = getProjectLabels(projectId);
const onOpen = () => { const onOpen = () => {
@ -149,7 +150,13 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
{projectLabels {projectLabels
?.filter((l) => value.includes(l?.id)) ?.filter((l) => value.includes(l?.id))
.map((label) => ( .map((label) => (
<Tooltip key={label.id} position="top" tooltipHeading="Labels" tooltipContent={label?.name ?? ""}> <Tooltip
key={label.id}
position="top"
tooltipHeading="Labels"
tooltipContent={label?.name ?? ""}
isMobile={isMobile}
>
<div <div
key={label?.id} key={label?.id}
className={`flex overflow-hidden hover:bg-custom-background-80 ${ className={`flex overflow-hidden hover:bg-custom-background-80 ${
@ -176,6 +183,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
}`} }`}
> >
<Tooltip <Tooltip
isMobile={isMobile}
position="top" position="top"
tooltipHeading="Labels" tooltipHeading="Labels"
tooltipContent={projectLabels tooltipContent={projectLabels
@ -191,7 +199,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
</div> </div>
) )
) : ( ) : (
<Tooltip position="top" tooltipHeading="Labels" tooltipContent="None"> <Tooltip position="top" tooltipHeading="Labels" tooltipContent="None" isMobile={isMobile}>
<div <div
className={`flex h-full items-center justify-center gap-2 rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${ className={`flex h-full items-center justify-center gap-2 rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${
noLabelBorder ? "" : "border-[0.5px] border-custom-border-300" noLabelBorder ? "" : "border-[0.5px] border-custom-border-300"

View File

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

View File

@ -1,14 +1,15 @@
import React from "react"; import React from "react";
// components // components
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
import { usePlatformOS } from "hooks/use-platform-os";
type Props = { type Props = {
labelDetails: any[]; labelDetails: any[];
maxRender?: number; maxRender?: number;
}; };
export const ViewIssueLabel: React.FC<Props> = ({ labelDetails, maxRender = 1 }) => ( export const ViewIssueLabel: React.FC<Props> = ({ labelDetails, maxRender = 1 }) => {
<> const { isMobile } = usePlatformOS();
return (<>
{labelDetails?.length > 0 ? ( {labelDetails?.length > 0 ? (
labelDetails.length <= maxRender ? ( labelDetails.length <= maxRender ? (
<> <>
@ -17,7 +18,7 @@ export const ViewIssueLabel: React.FC<Props> = ({ labelDetails, maxRender = 1 })
key={label.id} key={label.id}
className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm" className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
> >
<Tooltip position="top" tooltipHeading="Label" tooltipContent={label.name}> <Tooltip position="top" tooltipHeading="Label" tooltipContent={label.name} isMobile={isMobile}>
<div className="flex items-center gap-1.5 text-custom-text-200"> <div className="flex items-center gap-1.5 text-custom-text-200">
<span <span
className="h-2 w-2 flex-shrink-0 rounded-full" className="h-2 w-2 flex-shrink-0 rounded-full"
@ -33,7 +34,7 @@ export const ViewIssueLabel: React.FC<Props> = ({ labelDetails, maxRender = 1 })
</> </>
) : ( ) : (
<div className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"> <div className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm">
<Tooltip position="top" tooltipHeading="Labels" tooltipContent={labelDetails.map((l) => l.name).join(", ")}> <Tooltip position="top" tooltipHeading="Labels" tooltipContent={labelDetails.map((l) => l.name).join(", ")} isMobile={isMobile}>
<div className="flex items-center gap-1.5 text-custom-text-200"> <div className="flex items-center gap-1.5 text-custom-text-200">
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" /> <span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
{`${labelDetails.length} Labels`} {`${labelDetails.length} Labels`}
@ -44,5 +45,5 @@ export const ViewIssueLabel: React.FC<Props> = ({ labelDetails, maxRender = 1 })
) : ( ) : (
"" ""
)} )}
</> </>)
); };

View File

@ -8,6 +8,7 @@ import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
import useDebounce from "hooks/use-debounce"; import useDebounce from "hooks/use-debounce";
import { ProjectService } from "services/project"; import { ProjectService } from "services/project";
// hooks // hooks
import { usePlatformOS } from "hooks/use-platform-os";
// ui // ui
// icons // icons
// types // types
@ -37,7 +38,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]); const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
const { isMobile } = usePlatformOS();
const debouncedSearchTerm: string = useDebounce(searchTerm, 500); const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
const router = useRouter(); const router = useRouter();
@ -113,7 +114,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
/> />
</div> </div>
<div className="flex p-2 sm:justify-end"> <div className="flex p-2 sm:justify-end">
<Tooltip tooltipContent="Toggle workspace level search"> <Tooltip tooltipContent="Toggle workspace level search" isMobile={isMobile}>
<div <div
className={`flex flex-shrink-0 cursor-pointer items-center gap-1 text-xs ${ className={`flex flex-shrink-0 cursor-pointer items-center gap-1 text-xs ${
isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200" isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200"

View File

@ -21,9 +21,8 @@ import { cn } from "helpers/common.helper";
import { copyUrlToClipboard } from "helpers/string.helper"; import { copyUrlToClipboard } from "helpers/string.helper";
// store hooks // store hooks
import { useIssueDetail, useProjectState, useUser } from "hooks/store"; import { useIssueDetail, useProjectState, useUser } from "hooks/store";
// helpers // hooks
// components import { usePlatformOS } from "hooks/use-platform-os";
// helpers
export type TPeekModes = "side-peek" | "modal" | "full-screen"; export type TPeekModes = "side-peek" | "modal" | "full-screen";
@ -83,6 +82,7 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail();
const { getStateById } = useProjectState(); const { getStateById } = useProjectState();
const { isMobile } = usePlatformOS();
// derived values // derived values
const issueDetails = getIssueById(issueId); const issueDetails = getIssueById(issueId);
const stateDetails = issueDetails ? getStateById(issueDetails?.state_id) : undefined; const stateDetails = issueDetails ? getStateById(issueDetails?.state_id) : undefined;
@ -160,13 +160,14 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
{currentUser && !isArchived && ( {currentUser && !isArchived && (
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} /> <IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
)} )}
<Tooltip tooltipContent="Copy link"> <Tooltip tooltipContent="Copy link" isMobile={isMobile}>
<button type="button" onClick={handleCopyText}> <button type="button" onClick={handleCopyText}>
<Link2 className="h-4 w-4 -rotate-45 text-custom-text-300 hover:text-custom-text-200" /> <Link2 className="h-4 w-4 -rotate-45 text-custom-text-300 hover:text-custom-text-200" />
</button> </button>
</Tooltip> </Tooltip>
{isArchivingAllowed && ( {isArchivingAllowed && (
<Tooltip <Tooltip
isMobile={isMobile}
tooltipContent={isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"} tooltipContent={isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"}
> >
<button <button
@ -185,14 +186,14 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
</Tooltip> </Tooltip>
)} )}
{isRestoringAllowed && ( {isRestoringAllowed && (
<Tooltip tooltipContent="Restore"> <Tooltip tooltipContent="Restore" isMobile={isMobile}>
<button type="button" onClick={handleRestoreIssue}> <button type="button" onClick={handleRestoreIssue}>
<RotateCcw className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" /> <RotateCcw className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
</button> </button>
</Tooltip> </Tooltip>
)} )}
{!disabled && ( {!disabled && (
<Tooltip tooltipContent="Delete"> <Tooltip tooltipContent="Delete" isMobile={isMobile}>
<button type="button" onClick={() => toggleDeleteIssueModal(true)}> <button type="button" onClick={() => toggleDeleteIssueModal(true)}>
<Trash2 className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" /> <Trash2 className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
</button> </button>

View File

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

View File

@ -4,6 +4,7 @@ import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader }
// components // components
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
import { useIssueDetail, useProject, useProjectState } from "hooks/store"; import { useIssueDetail, useProject, useProjectState } from "hooks/store";
import { usePlatformOS } from "hooks/use-platform-os";
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
import { IssueList } from "./issues-list"; import { IssueList } from "./issues-list";
import { IssueProperty } from "./properties"; import { IssueProperty } from "./properties";
@ -46,7 +47,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
} = useIssueDetail(); } = useIssueDetail();
const project = useProject(); const project = useProject();
const { getProjectStates } = useProjectState(); const { getProjectStates } = useProjectState();
const { isMobile } = usePlatformOS();
const issue = getIssueById(issueId); const issue = getIssueById(issueId);
const projectDetail = (issue && issue.project_id && project.getProjectById(issue.project_id)) || undefined; const projectDetail = (issue && issue.project_id && project.getProjectById(issue.project_id)) || undefined;
const currentIssueStateDetail = const currentIssueStateDetail =
@ -117,7 +118,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
onClick={() => handleIssuePeekOverview(issue)} onClick={() => handleIssuePeekOverview(issue)}
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
> >
<Tooltip tooltipContent={issue.name}> <Tooltip tooltipContent={issue.name} isMobile={isMobile}>
<span>{issue.name}</span> <span>{issue.name}</span>
</Tooltip> </Tooltip>
</ControlLink> </ControlLink>

View File

@ -0,0 +1,56 @@
import { observer } from "mobx-react-lite";
// icons
import { X } from "lucide-react";
// helpers
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
import { renderFormattedDate } from "helpers/date-time.helper";
import { capitalizeFirstLetter } from "helpers/string.helper";
// constants
type Props = {
editable: boolean | undefined;
handleRemove: (val: string) => void;
values: string[];
};
export const AppliedDateFilters: React.FC<Props> = observer((props) => {
const { editable, handleRemove, values } = props;
const getDateLabel = (value: string): string => {
let dateLabel = "";
const dateDetails = DATE_AFTER_FILTER_OPTIONS.find((d) => d.value === value);
if (dateDetails) dateLabel = dateDetails.name;
else {
const dateParts = value.split(";");
if (dateParts.length === 2) {
const [date, time] = dateParts;
dateLabel = `${capitalizeFirstLetter(time)} ${renderFormattedDate(date)}`;
}
}
return dateLabel;
};
return (
<>
{values.map((date) => (
<div key={date} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<span className="normal-case">{getDateLabel(date)}</span>
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(date)}
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
))}
</>
);
});

View File

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

View File

@ -0,0 +1,46 @@
import { observer } from "mobx-react-lite";
import { X } from "lucide-react";
// ui
import { Avatar } from "@plane/ui";
// types
import { useMember } from "hooks/store";
type Props = {
handleRemove: (val: string) => void;
values: string[];
editable: boolean | undefined;
};
export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
const { handleRemove, values, editable } = props;
// store hooks
const {
workspace: { getWorkspaceMemberDetails },
} = useMember();
return (
<>
{values.map((memberId) => {
const memberDetails = getWorkspaceMemberDetails(memberId)?.member;
if (!memberDetails) return null;
return (
<div key={memberId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<Avatar name={memberDetails.display_name} src={memberDetails.avatar} showTooltip={false} />
<span className="normal-case">{memberDetails.display_name}</span>
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(memberId)}
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
);
})}
</>
);
});

View File

@ -0,0 +1,88 @@
import { X } from "lucide-react";
// components
import { AppliedDateFilters, AppliedMembersFilters, AppliedStatusFilters } from "components/modules";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { TModuleFilters } from "@plane/types";
type Props = {
appliedFilters: TModuleFilters;
handleClearAllFilters: () => void;
handleRemoveFilter: (key: keyof TModuleFilters, value: string | null) => void;
alwaysAllowEditing?: boolean;
};
const MEMBERS_FILTERS = ["lead", "members"];
const DATE_FILTERS = ["start_date", "target_date"];
export const ModuleAppliedFiltersList: React.FC<Props> = (props) => {
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, alwaysAllowEditing } = props;
if (!appliedFilters) return null;
if (Object.keys(appliedFilters).length === 0) return null;
const isEditingAllowed = alwaysAllowEditing;
return (
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
{Object.entries(appliedFilters).map(([key, value]) => {
const filterKey = key as keyof TModuleFilters;
if (!value) return;
if (Array.isArray(value) && value.length === 0) return;
return (
<div
key={filterKey}
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize"
>
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span>
{filterKey === "status" && (
<AppliedStatusFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("status", val)}
values={value}
/>
)}
{DATE_FILTERS.includes(filterKey) && (
<AppliedDateFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
values={value}
/>
)}
{MEMBERS_FILTERS.includes(filterKey) && (
<AppliedMembersFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
values={value}
/>
)}
{isEditingAllowed && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemoveFilter(filterKey, null)}
>
<X size={12} strokeWidth={2} />
</button>
)}
</div>
</div>
);
})}
{isEditingAllowed && (
<button
type="button"
onClick={handleClearAllFilters}
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
>
Clear all
<X size={12} strokeWidth={2} />
</button>
)}
</div>
);
};

View File

@ -0,0 +1,41 @@
import { observer } from "mobx-react-lite";
import { X } from "lucide-react";
// ui
import { ModuleStatusIcon } from "@plane/ui";
// constants
import { MODULE_STATUS } from "constants/module";
type Props = {
handleRemove: (val: string) => void;
values: string[];
editable: boolean | undefined;
};
export const AppliedStatusFilters: React.FC<Props> = observer((props) => {
const { handleRemove, values, editable } = props;
return (
<>
{values.map((status) => {
const statusDetails = MODULE_STATUS?.find((s) => s.value === status);
if (!statusDetails) return null;
return (
<div key={status} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<ModuleStatusIcon status={statusDetails.value} height="12px" width="12px" />
{statusDetails.label}
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(status)}
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
);
})}
</>
);
});

View File

@ -0,0 +1,6 @@
export * from "./lead";
export * from "./members";
export * from "./root";
export * from "./start-date";
export * from "./status";
export * from "./target-date";

View File

@ -0,0 +1,96 @@
import { useMemo, useState } from "react";
import { observer } from "mobx-react-lite";
import sortBy from "lodash/sortBy";
// hooks
import { useMember } from "hooks/store";
// components
import { FilterHeader, FilterOption } from "components/issues";
// ui
import { Avatar, Loader } from "@plane/ui";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
memberIds: string[] | undefined;
searchQuery: string;
};
export const FilterLead: React.FC<Props> = observer((props: Props) => {
const { appliedFilters, handleUpdate, memberIds, searchQuery } = props;
// states
const [itemsToRender, setItemsToRender] = useState(5);
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const appliedFiltersCount = appliedFilters?.length ?? 0;
const sortedOptions = useMemo(() => {
const filteredOptions = (memberIds || []).filter((memberId) =>
getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
);
return sortBy(filteredOptions, [
(memberId) => !(appliedFilters ?? []).includes(memberId),
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
]);
}, [appliedFilters, getUserDetails, memberIds, , searchQuery]);
const handleViewToggle = () => {
if (!sortedOptions) return;
if (itemsToRender === sortedOptions.length) setItemsToRender(5);
else setItemsToRender(sortedOptions.length);
};
return (
<>
<FilterHeader
title={`Lead${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{sortedOptions ? (
sortedOptions.length > 0 ? (
<>
{sortedOptions.slice(0, itemsToRender).map((memberId) => {
const member = getUserDetails(memberId);
if (!member) return null;
return (
<FilterOption
key={`lead-${member.id}`}
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
title={member.display_name}
/>
);
})}
{sortedOptions.length > 5 && (
<button
type="button"
className="ml-8 text-xs font-medium text-custom-primary-100"
onClick={handleViewToggle}
>
{itemsToRender === sortedOptions.length ? "View less" : "View all"}
</button>
)}
</>
) : (
<p className="text-xs italic text-custom-text-400">No matches found</p>
)
) : (
<Loader className="space-y-2">
<Loader.Item height="20px" />
<Loader.Item height="20px" />
<Loader.Item height="20px" />
</Loader>
)}
</div>
)}
</>
);
});

View File

@ -0,0 +1,97 @@
import { useMemo, useState } from "react";
import { observer } from "mobx-react-lite";
import sortBy from "lodash/sortBy";
// hooks
import { useMember } from "hooks/store";
// components
import { FilterHeader, FilterOption } from "components/issues";
// ui
import { Avatar, Loader } from "@plane/ui";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
memberIds: string[] | undefined;
searchQuery: string;
};
export const FilterMembers: React.FC<Props> = observer((props: Props) => {
const { appliedFilters, handleUpdate, memberIds, searchQuery } = props;
// states
const [itemsToRender, setItemsToRender] = useState(5);
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const appliedFiltersCount = appliedFilters?.length ?? 0;
const sortedOptions = useMemo(() => {
const filteredOptions = (memberIds || []).filter((memberId) =>
getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
);
return sortBy(filteredOptions, [
(memberId) => !(appliedFilters ?? []).includes(memberId),
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchQuery]);
const handleViewToggle = () => {
if (!sortedOptions) return;
if (itemsToRender === sortedOptions.length) setItemsToRender(5);
else setItemsToRender(sortedOptions.length);
};
return (
<>
<FilterHeader
title={`Members${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{sortedOptions ? (
sortedOptions.length > 0 ? (
<>
{sortedOptions.slice(0, itemsToRender).map((memberId) => {
const member = getUserDetails(memberId);
if (!member) return null;
return (
<FilterOption
key={`member-${member.id}`}
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
title={member.display_name}
/>
);
})}
{sortedOptions.length > 5 && (
<button
type="button"
className="ml-8 text-xs font-medium text-custom-primary-100"
onClick={handleViewToggle}
>
{itemsToRender === sortedOptions.length ? "View less" : "View all"}
</button>
)}
</>
) : (
<p className="text-xs italic text-custom-text-400">No matches found</p>
)
) : (
<Loader className="space-y-2">
<Loader.Item height="20px" />
<Loader.Item height="20px" />
<Loader.Item height="20px" />
</Loader>
)}
</div>
)}
</>
);
});

View File

@ -0,0 +1,106 @@
import { useState } from "react";
import { observer } from "mobx-react-lite";
import { Search, X } from "lucide-react";
// components
import { FilterLead, FilterMembers, FilterStartDate, FilterStatus, FilterTargetDate } from "components/modules";
import { FilterOption } from "components/issues";
// types
import { TModuleDisplayFilters, TModuleFilters } from "@plane/types";
import { TModuleStatus } from "@plane/ui";
type Props = {
displayFilters: TModuleDisplayFilters;
filters: TModuleFilters;
handleDisplayFiltersUpdate: (updatedDisplayProperties: Partial<TModuleDisplayFilters>) => void;
handleFiltersUpdate: (key: keyof TModuleFilters, value: string | string[]) => void;
memberIds?: string[] | undefined;
};
export const ModuleFiltersSelection: React.FC<Props> = observer((props) => {
const { displayFilters, filters, handleDisplayFiltersUpdate, handleFiltersUpdate, memberIds } = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
return (
<div className="flex h-full w-full flex-col overflow-hidden">
<div className="bg-custom-background-100 p-2.5 pb-0">
<div className="flex items-center gap-1.5 rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-1.5 py-1 text-xs">
<Search className="text-custom-text-400" size={12} strokeWidth={2} />
<input
type="text"
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
placeholder="Search"
value={filtersSearchQuery}
onChange={(e) => setFiltersSearchQuery(e.target.value)}
autoFocus
/>
{filtersSearchQuery !== "" && (
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>
<X className="text-custom-text-300" size={12} strokeWidth={2} />
</button>
)}
</div>
</div>
<div className="h-full w-full divide-y divide-custom-border-200 overflow-y-auto px-2.5 vertical-scrollbar scrollbar-sm">
<div className="py-2">
<FilterOption
isChecked={!!displayFilters.favorites}
onClick={() =>
handleDisplayFiltersUpdate({
favorites: !displayFilters.favorites,
})
}
title="Favorites"
/>
</div>
{/* status */}
<div className="py-2">
<FilterStatus
appliedFilters={(filters.status as TModuleStatus[]) ?? null}
handleUpdate={(val) => handleFiltersUpdate("status", val)}
searchQuery={filtersSearchQuery}
/>
</div>
{/* lead */}
<div className="py-2">
<FilterLead
appliedFilters={filters.lead ?? null}
handleUpdate={(val) => handleFiltersUpdate("lead", val)}
searchQuery={filtersSearchQuery}
memberIds={memberIds}
/>
</div>
{/* members */}
<div className="py-2">
<FilterMembers
appliedFilters={filters.members ?? null}
handleUpdate={(val) => handleFiltersUpdate("members", val)}
searchQuery={filtersSearchQuery}
memberIds={memberIds}
/>
</div>
{/* start date */}
<div className="py-2">
<FilterStartDate
appliedFilters={filters.start_date ?? null}
handleUpdate={(val) => handleFiltersUpdate("start_date", val)}
searchQuery={filtersSearchQuery}
/>
</div>
{/* target date */}
<div className="py-2">
<FilterTargetDate
appliedFilters={filters.target_date ?? null}
handleUpdate={(val) => handleFiltersUpdate("target_date", val)}
searchQuery={filtersSearchQuery}
/>
</div>
</div>
</div>
);
});

View File

@ -0,0 +1,65 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { DateFilterModal } from "components/core";
import { FilterHeader, FilterOption } from "components/issues";
// constants
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string | string[]) => void;
searchQuery: string;
};
export const FilterStartDate: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, searchQuery } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) =>
d.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<>
{isDateFilterModalOpen && (
<DateFilterModal
handleClose={() => setIsDateFilterModalOpen(false)}
isOpen={isDateFilterModalOpen}
onSelect={(val) => handleUpdate(val)}
title="Start date"
/>
)}
<FilterHeader
title={`Start date${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{filteredOptions.length > 0 ? (
<>
{filteredOptions.map((option) => (
<FilterOption
key={option.value}
isChecked={appliedFilters?.includes(option.value) ? true : false}
onClick={() => handleUpdate(option.value)}
title={option.name}
multiple
/>
))}
<FilterOption isChecked={false} onClick={() => setIsDateFilterModalOpen(true)} title="Custom" multiple />
</>
) : (
<p className="text-xs italic text-custom-text-400">No matches found</p>
)}
</div>
)}
</>
);
});

View File

@ -0,0 +1,52 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { FilterHeader, FilterOption } from "components/issues";
// ui
import { ModuleStatusIcon } from "@plane/ui";
// types
import { TModuleStatus } from "@plane/types";
// constants
import { MODULE_STATUS } from "constants/module";
type Props = {
appliedFilters: TModuleStatus[] | null;
handleUpdate: (val: string) => void;
searchQuery: string;
};
export const FilterStatus: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, searchQuery } = props;
// states
const [previewEnabled, setPreviewEnabled] = useState(true);
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = MODULE_STATUS.filter((p) => p.value.includes(searchQuery.toLowerCase()));
return (
<>
<FilterHeader
title={`Status${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{filteredOptions.length > 0 ? (
filteredOptions.map((status) => (
<FilterOption
key={status.value}
isChecked={appliedFilters?.includes(status.value) ? true : false}
onClick={() => handleUpdate(status.value)}
icon={<ModuleStatusIcon status={status.value} />}
title={status.label}
/>
))
) : (
<p className="text-xs italic text-custom-text-400">No matches found</p>
)}
</div>
)}
</>
);
});

View File

@ -0,0 +1,65 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { DateFilterModal } from "components/core";
import { FilterHeader, FilterOption } from "components/issues";
// constants
import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string | string[]) => void;
searchQuery: string;
};
export const FilterTargetDate: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, searchQuery } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) =>
d.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<>
{isDateFilterModalOpen && (
<DateFilterModal
handleClose={() => setIsDateFilterModalOpen(false)}
isOpen={isDateFilterModalOpen}
onSelect={(val) => handleUpdate(val)}
title="Due date"
/>
)}
<FilterHeader
title={`Due date${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{filteredOptions.length > 0 ? (
<>
{filteredOptions.map((option) => (
<FilterOption
key={option.value}
isChecked={appliedFilters?.includes(option.value) ? true : false}
onClick={() => handleUpdate(option.value)}
title={option.name}
multiple
/>
))}
<FilterOption isChecked={false} onClick={() => setIsDateFilterModalOpen(true)} title="Custom" multiple />
</>
) : (
<p className="text-xs italic text-custom-text-400">No matches found</p>
)}
</div>
)}
</>
);
});

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