Compare commits

...
Author SHA1 Message Date
Anmol Singh Bhatia 4a5cbd4215 feat: search icon migration 2025-12-01 12:52:29 +05:30
50 changed files with 177 additions and 625 deletions
@@ -1,31 +1,25 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
// types
import type { IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue";
// local imports
import { FilterPriority } from "./priority";
import { FilterState } from "./state";
type Props = {
filters: IIssueFilterOptions;
handleFilters: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
layoutDisplayFiltersOptions: TIssueFilterKeys[];
};
export const FilterSelection = observer(function FilterSelection(props: Props) {
const { filters, handleFilters, layoutDisplayFiltersOptions } = props;
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions.includes(filter);
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} />
<SearchIcon className="text-custom-text-400" width="12" height="12" strokeWidth={2} />
<input
type="text"
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
@@ -1,11 +1,11 @@
import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { Plus, Search } from "lucide-react";
import { Plus } from "lucide-react";
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { SearchIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip";
import { copyUrlToClipboard, orderJoinedProjects } from "@plane/utils";
@@ -18,7 +18,6 @@ import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
import type { TProject } from "@/plane-web/types";
import { ExtendedSidebarWrapper } from "./extended-sidebar-wrapper";
export const ExtendedProjectSidebar = observer(function ExtendedProjectSidebar() {
// refs
const extendedProjectSidebarRef = useRef<HTMLDivElement | null>(null);
@@ -32,7 +31,6 @@ export const ExtendedProjectSidebar = observer(function ExtendedProjectSidebar()
const { isExtendedProjectSidebarOpened, toggleExtendedProjectSidebar } = useAppTheme();
const { getPartialProjectById, joinedProjectIds: joinedProjects, updateProjectView } = useProject();
const { allowPermissions } = useUserPermissions();
const handleOnProjectDrop = (
sourceId: string | undefined,
destinationId: string | undefined,
@@ -40,18 +38,14 @@ export const ExtendedProjectSidebar = observer(function ExtendedProjectSidebar()
) => {
if (!sourceId || !destinationId || !workspaceSlug) return;
if (sourceId === destinationId) return;
const joinedProjectsList: TProject[] = [];
joinedProjects.map((projectId) => {
const projectDetails = getPartialProjectById(projectId);
if (projectDetails) joinedProjectsList.push(projectDetails);
});
const sourceIndex = joinedProjects.indexOf(sourceId);
const destinationIndex = shouldDropAtEnd ? joinedProjects.length : joinedProjects.indexOf(destinationId);
if (joinedProjectsList.length <= 0) return;
const updatedSortOrder = orderJoinedProjects(sourceIndex, destinationIndex, sourceId, joinedProjectsList);
if (updatedSortOrder != undefined)
updateProjectView(workspaceSlug.toString(), sourceId, { sort_order: updatedSortOrder }).catch(() => {
@@ -62,22 +56,18 @@ export const ExtendedProjectSidebar = observer(function ExtendedProjectSidebar()
});
});
};
// filter projects based on search query
const filteredProjects = joinedProjects.filter((projectId) => {
const project = getPartialProjectById(projectId);
if (!project) return false;
return project.name.toLowerCase().includes(searchQuery.toLowerCase()) || project.identifier.includes(searchQuery);
});
// auth
const isAuthorizedUser = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
const handleClose = useCallback(() => toggleExtendedProjectSidebar(false), [toggleExtendedProjectSidebar]);
const handleCopyText = (projectId: string) => {
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => {
setToast({
@@ -123,7 +113,7 @@ export const ExtendedProjectSidebar = observer(function ExtendedProjectSidebar()
)}
</div>
<div className="ml-auto flex items-center gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1 w-full">
<Search className="h-3.5 w-3.5 text-custom-text-400" />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-400" />
<input
className="w-full max-w-[234px] border-none bg-transparent text-sm outline-none placeholder:text-custom-text-400"
placeholder={t("search")}
@@ -1,9 +1,9 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
// plane imports
import { DEFAULT_GLOBAL_VIEWS_LIST } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { SearchIcon } from "@plane/propel/icons";
import { Input } from "@plane/ui";
// components
import { PageHead } from "@/components/core/page-title";
@@ -11,7 +11,6 @@ import { GlobalDefaultViewListItem } from "@/components/workspace/views/default-
import { GlobalViewsList } from "@/components/workspace/views/views-list";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
function WorkspaceViewsPage() {
const [query, setQuery] = useState("");
// store
@@ -19,13 +18,12 @@ function WorkspaceViewsPage() {
const { t } = useTranslation();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - All Views` : undefined;
return (
<>
<PageHead title={pageTitle} />
<div className="flex flex-col h-full w-full overflow-hidden">
<div className="flex h-11 w-full items-center gap-2.5 px-5 py-3 overflow-hidden border-b border-custom-border-200">
<Search className="text-custom-text-200" size={14} strokeWidth={2} />
<SearchIcon className="text-custom-text-200" width="14" height="14" strokeWidth={2} />
<Input
className="w-full bg-transparent !p-0 text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
@@ -46,5 +44,4 @@ function WorkspaceViewsPage() {
</>
);
}
export default observer(WorkspaceViewsPage);
@@ -1,6 +1,5 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
// types
import {
EUserPermissions,
@@ -10,6 +9,7 @@ import {
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { SearchIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IWorkspaceBulkInviteFormData } from "@plane/types";
import { cn } from "@plane/utils";
@@ -30,7 +30,6 @@ import { useUserPermissions } from "@/hooks/store/user";
import { BillingActionsButton } from "@/plane-web/components/workspace/billing/billing-actions-button";
import { SendWorkspaceInvitationModal } from "@/plane-web/components/workspace/members/invite-modal";
import type { Route } from "./+types/page";
function WorkspaceMembersSettingsPage({ params }: Route.ComponentProps) {
// states
const [inviteModal, setInviteModal] = useState(false);
@@ -44,27 +43,22 @@ function WorkspaceMembersSettingsPage({ params }: Route.ComponentProps) {
} = useMember();
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
// derived values
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const canPerformWorkspaceMemberActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
const handleWorkspaceInvite = async (data: IWorkspaceBulkInviteFormData) => {
try {
await inviteMembersToWorkspace(workspaceSlug, data);
setInviteModal(false);
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.invite,
payload: {
emails: data.emails.map((email) => email.email),
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
@@ -79,39 +73,32 @@ function WorkspaceMembersSettingsPage({ params }: Route.ComponentProps) {
},
error: err,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: `${err.error ?? t("something_went_wrong_please_try_again")}`,
});
throw err;
}
};
// Handler for role filter updates
const handleRoleFilterUpdate = (role: string) => {
const currentFilters = filtersStore.filters;
const currentRoles = currentFilters?.roles || [];
const updatedRoles = currentRoles.includes(role) ? currentRoles.filter((r) => r !== role) : [...currentRoles, role];
filtersStore.updateFilters({
roles: updatedRoles.length > 0 ? updatedRoles : undefined,
});
};
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined;
const appliedRoleFilters = filtersStore.filters?.roles || [];
// if user is not authorized to view this page
if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
return <NotAuthorizedView section="settings" className="h-auto" />;
}
return (
<SettingsContentWrapper size="lg">
<SettingsContentWrapper width="lg" height="lg">
<PageHead title={pageTitle} />
<SendWorkspaceInvitationModal
isOpen={inviteModal}
@@ -132,7 +119,7 @@ function WorkspaceMembersSettingsPage({ params }: Route.ComponentProps) {
</h4>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5">
<Search className="h-3.5 w-3.5 text-custom-text-400" />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-400" />
<input
className="w-full max-w-[234px] border-none bg-transparent text-sm outline-none placeholder:text-custom-text-400"
placeholder={`${t("search")}...`}
@@ -149,7 +136,8 @@ function WorkspaceMembersSettingsPage({ params }: Route.ComponentProps) {
{canPerformWorkspaceAdminActions && (
<Button
variant="primary"
size="sm"
width="sm"
height="sm"
onClick={() => setInviteModal(true)}
data-ph-element={MEMBER_TRACKER_ELEMENTS.HEADER_ADD_BUTTON}
>
@@ -164,5 +152,4 @@ function WorkspaceMembersSettingsPage({ params }: Route.ComponentProps) {
</SettingsContentWrapper>
);
}
export default observer(WorkspaceMembersSettingsPage);
@@ -1,9 +1,10 @@
import { observer } from "mobx-react";
import { AlertOctagon, BarChart4, CircleDashed, Folder, Microscope, Search } from "lucide-react";
import { AlertOctagon, BarChart4, CircleDashed, Folder, Microscope } from "lucide-react";
// plane imports
import { MARKETING_PRICING_PAGE_LINK } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { getButtonStyling } from "@plane/propel/button";
import { SearchIcon } from "@plane/propel/icons";
import { ContentWrapper } from "@plane/ui";
import { cn } from "@plane/utils";
// assets
@@ -17,7 +18,6 @@ import ctaR2Light from "@/app/assets/workspace-active-cycles/cta-r-2-light.webp?
import { ProIcon } from "@/components/common/pro-icon";
// hooks
import { useUser } from "@/hooks/store/user";
export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [
{
key: "10000_feet_view",
@@ -50,7 +50,7 @@ export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [
key: "zoom_into_cycles_that_need_attention",
title: "Zoom into cycles that need attention. ",
description: "Investigate the state of any cycle that doesn’t conform to expectations in one click.",
icon: Search,
icon: SearchIcon,
},
{
key: "stay_ahead_of_blockers",
@@ -60,16 +60,13 @@ export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [
icon: Microscope,
},
];
export const WorkspaceActiveCyclesUpgrade = observer(function WorkspaceActiveCyclesUpgrade() {
const { t } = useTranslation();
// store hooks
const {
userProfile: { data: userProfile },
} = useUser();
const isDarkMode = userProfile?.theme.theme === "dark";
return (
<ContentWrapper className="gap-10">
<div
@@ -1,27 +1,24 @@
import { Command } from "cmdk";
import { Search } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
// components
import { SearchIcon } from "@plane/propel/icons";
import type { TPowerKContext } from "@/components/power-k/core/types";
// plane web imports
import { PowerKModalCommandItem } from "@/components/power-k/ui/modal/command-item";
export type TPowerKModalNoSearchResultsCommandProps = {
context: TPowerKContext;
searchTerm: string;
updateSearchTerm: (value: string) => void;
};
export function PowerKModalNoSearchResultsCommand(props: TPowerKModalNoSearchResultsCommandProps) {
const { updateSearchTerm } = props;
// translation
const { t } = useTranslation();
return (
<Command.Group>
<PowerKModalCommandItem
icon={Search}
icon={SearchIcon}
value="no-results"
label={
<p className="flex items-center gap-2">
@@ -15,22 +15,19 @@ import {
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Search } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
// plane package imports
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@plane/propel/table";
import { cn } from "@plane/utils";
// plane web components
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
searchPlaceholder: string;
actions?: (table: TanstackTable<TData>) => React.ReactNode;
}
export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, actions }: DataTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = React.useState({});
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
@@ -39,7 +36,6 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
const { t } = useTranslation();
const inputRef = React.useRef<HTMLInputElement>(null);
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
const table = useReactTable({
data,
columns,
@@ -57,7 +53,6 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
});
return (
<div className="space-y-4">
<div className="flex w-full items-center justify-between">
@@ -76,7 +71,7 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon className="h-3.5 w-3.5" />
</button>
)}
<div
@@ -87,7 +82,7 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
}
)}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon 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"
@@ -4,11 +4,11 @@ import { useParams } from "next/navigation";
import { useTheme } from "next-themes";
import type { SubmitHandler } from "react-hook-form";
import { useForm } from "react-hook-form";
import { Search } from "lucide-react";
import { Combobox, Dialog, Transition } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { SearchIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { ISearchIssueResponse, IUser } from "@plane/types";
import { EIssuesStoreType } from "@plane/types";
@@ -27,19 +27,15 @@ import useDebounce from "@/hooks/use-debounce";
import { ProjectService } from "@/services/project";
// local components
import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item";
type FormInput = {
delete_issue_ids: string[];
};
type Props = {
isOpen: boolean;
onClose: () => void;
user: IUser | undefined;
};
const projectService = new ProjectService();
export const BulkDeleteIssuesModal = observer(function BulkDeleteIssuesModal(props: Props) {
const { isOpen, onClose } = props;
// router params
@@ -59,10 +55,8 @@ export const BulkDeleteIssuesModal = observer(function BulkDeleteIssuesModal(pro
const debouncedSearchTerm: string = useDebounce(query, 500);
const searchResolvedPath = resolvedTheme === "light" ? lightSearchAsset : darkSearchAsset;
const issuesResolvedPath = resolvedTheme === "light" ? lightIssuesAsset : darkIssuesAsset;
useEffect(() => {
if (!isOpen || !workspaceSlug || !projectId) return;
setIsSearching(true);
projectService
.projectIssuesSearch(workspaceSlug.toString(), projectId.toString(), {
@@ -72,7 +66,6 @@ export const BulkDeleteIssuesModal = observer(function BulkDeleteIssuesModal(pro
.then((res: ISearchIssueResponse[]) => setIssues(res))
.finally(() => setIsSearching(false));
}, [debouncedSearchTerm, isOpen, projectId, workspaceSlug]);
const {
handleSubmit,
watch,
@@ -84,16 +77,13 @@ export const BulkDeleteIssuesModal = observer(function BulkDeleteIssuesModal(pro
delete_issue_ids: [],
},
});
const handleClose = () => {
setQuery("");
reset();
onClose();
};
const handleDelete: SubmitHandler<FormInput> = async (data) => {
if (!workspaceSlug || !projectId) return;
if (!data.delete_issue_ids || data.delete_issue_ids.length === 0) {
setToast({
type: TOAST_TYPE.ERROR,
@@ -102,9 +92,7 @@ export const BulkDeleteIssuesModal = observer(function BulkDeleteIssuesModal(pro
});
return;
}
if (!Array.isArray(data.delete_issue_ids)) data.delete_issue_ids = [data.delete_issue_ids];
await removeBulkIssues(workspaceSlug as string, projectId as string, data.delete_issue_ids)
.then(() => {
setToast({
@@ -122,7 +110,6 @@ export const BulkDeleteIssuesModal = observer(function BulkDeleteIssuesModal(pro
})
);
};
const issueList =
issues.length > 0 ? (
<li className="p-2">
@@ -148,7 +135,6 @@ export const BulkDeleteIssuesModal = observer(function BulkDeleteIssuesModal(pro
)}
</div>
);
return (
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
@@ -177,7 +163,7 @@ export const BulkDeleteIssuesModal = observer(function BulkDeleteIssuesModal(pro
}}
>
<div className="relative m-1">
<Search
<SearchIcon
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-custom-text-100 text-opacity-40"
aria-hidden="true"
/>
@@ -208,10 +194,16 @@ export const BulkDeleteIssuesModal = observer(function BulkDeleteIssuesModal(pro
{issues.length > 0 && (
<div className="flex items-center justify-end gap-2 p-3">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
<Button variant="neutral-primary" width="sm" height="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="danger" size="sm" onClick={handleSubmit(handleDelete)} loading={isSubmitting}>
<Button
variant="danger"
width="sm"
height="sm"
onClick={handleSubmit(handleDelete)}
loading={isSubmitting}
>
{isSubmitting ? "Deleting..." : "Delete selected work items"}
</Button>
</div>
@@ -1,11 +1,11 @@
import React, { useEffect, useState, useRef } from "react";
import { Rocket, Search } from "lucide-react";
import { Rocket } from "lucide-react";
import { Combobox, Dialog, Transition } from "@headlessui/react";
// i18n
import { useTranslation } from "@plane/i18n";
// types
import { Button } from "@plane/propel/button";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip";
import type { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
@@ -22,7 +22,6 @@ import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/iss
import { ProjectService } from "@/services/project";
// components
import { IssueSearchModalEmptyState } from "./issue-search-modal-empty-state";
type Props = {
workspaceSlug: string | undefined;
projectId?: string;
@@ -35,12 +34,9 @@ type Props = {
selectedWorkItemIds?: string[];
workItemSearchServiceCallback?: (params: TProjectIssuesSearchParams) => Promise<ISearchIssueResponse[]>;
};
const projectService = new ProjectService();
export function ExistingIssuesListModal(props: Props) {
const { t } = useTranslation();
const {
workspaceSlug,
projectId,
@@ -65,7 +61,6 @@ export function ExistingIssuesListModal(props: Props) {
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
const { baseTabIndex } = getTabIndex(undefined, isMobile);
const hasInitializedSelection = useRef(false);
const handleClose = () => {
onClose();
setSearchTerm("");
@@ -73,7 +68,6 @@ export function ExistingIssuesListModal(props: Props) {
setIsWorkspaceLevel(false);
hasInitializedSelection.current = false;
};
const onSubmit = async () => {
if (selectedIssues.length === 0) {
setToast({
@@ -81,17 +75,12 @@ export function ExistingIssuesListModal(props: Props) {
title: t("toast.error"),
message: t("issue.select.error"),
});
return;
}
setIsSubmitting(true);
await handleOnSubmit(selectedIssues).finally(() => setIsSubmitting(false));
handleClose();
};
const handleSearch = () => {
if (!isOpen || !workspaceSlug) return;
setIsLoading(true);
@@ -112,24 +101,19 @@ export function ExistingIssuesListModal(props: Props) {
setIsLoading(false);
});
};
const handleSelectIssues = () => {
setSelectedIssues((prevData) => (prevData.length === filteredIssues.length ? [] : [...filteredIssues]));
};
useEffect(() => {
if (isOpen && !hasInitializedSelection.current && selectedWorkItemIds && issues.length > 0) {
setSelectedIssues(issues.filter((issue) => selectedWorkItemIds.includes(issue.id)));
hasInitializedSelection.current = true;
}
}, [isOpen, issues, selectedWorkItemIds]);
useEffect(() => {
handleSearch();
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, workspaceSlug]);
const filteredIssues = issues.filter((issue) => !shouldHideIssue?.(issue));
return (
<>
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setSearchTerm("")} appear>
@@ -166,7 +150,7 @@ export function ExistingIssuesListModal(props: Props) {
}}
>
<div className="relative m-1">
<Search
<SearchIcon
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-custom-text-100 text-opacity-40"
aria-hidden="true"
/>
@@ -212,9 +196,7 @@ export function ExistingIssuesListModal(props: Props) {
{workspaceLevelToggle && (
<Tooltip tooltipContent="Toggle workspace level search" isMobile={isMobile}>
<div
className={`flex flex-shrink-0 cursor-pointer items-center gap-1 text-xs ${
isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200"
}`}
className={`flex flex-shrink-0 cursor-pointer items-center gap-1 text-xs ${isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200"}`}
>
<ToggleSwitch
value={isWorkspaceLevel}
@@ -269,7 +251,6 @@ export function ExistingIssuesListModal(props: Props) {
<ul className={`text-sm text-custom-text-100 ${filteredIssues.length > 0 ? "p-2" : ""}`}>
{filteredIssues.map((issue) => {
const selected = selectedIssues.some((i) => i.id === issue.id);
return (
<Combobox.Option
key={issue.id}
@@ -277,9 +258,7 @@ export function ExistingIssuesListModal(props: Props) {
htmlFor={`issue-${issue.id}`}
value={issue}
className={({ active }) =>
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 my-0.5 text-custom-text-200 ${
active ? "bg-custom-background-80 text-custom-text-100" : ""
} ${selected ? "text-custom-text-100" : ""}`
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 my-0.5 text-custom-text-200 ${active ? "bg-custom-background-80 text-custom-text-100" : ""} ${selected ? "text-custom-text-100" : ""}`
}
>
<div className="flex items-center gap-2 truncate">
@@ -328,7 +307,8 @@ export function ExistingIssuesListModal(props: Props) {
<div className="flex justify-between items-center p-3">
<Button
variant="link-primary"
size="sm"
width="sm"
height="sm"
onClick={handleSelectIssues}
disabled={filteredIssues.length === 0}
className={filteredIssues.length === 0 ? "p-0" : ""}
@@ -338,12 +318,13 @@ export function ExistingIssuesListModal(props: Props) {
: t("issue.select.select_all")}
</Button>
<div className="flex items-center justify-end gap-2">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
<Button variant="neutral-primary" width="sm" height="sm" onClick={handleClose}>
{t("common.cancel")}
</Button>
<Button
variant="primary"
size="sm"
width="sm"
height="sm"
onClick={onSubmit}
loading={isSubmitting}
disabled={isSubmitting || selectedIssues.length === 0}
@@ -2,10 +2,9 @@ import type { FC } from "react";
import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { ListFilter, Search } from "lucide-react";
import { ListFilter } from "lucide-react";
import { useOutsideClickDetector } from "@plane/hooks";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
// plane helpers
// types
import type { TCycleFilters } from "@plane/types";
@@ -17,7 +16,6 @@ import { FiltersDropdown } from "@/components/issues/issue-layouts/filters";
import { useCycleFilter } from "@/hooks/store/use-cycle-filter";
// local imports
import { CycleFiltersSelection } from "../dropdowns";
export const ArchivedCyclesHeader = observer(function ArchivedCyclesHeader() {
// router
const { projectId } = useParams();
@@ -32,12 +30,10 @@ export const ArchivedCyclesHeader = observer(function ArchivedCyclesHeader() {
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && archivedCyclesSearchQuery.trim() === "") setIsSearchOpen(false);
});
const handleFilters = useCallback(
(key: keyof TCycleFilters, value: string | string[]) => {
if (!projectId) return;
const newValues = currentProjectArchivedFilters?.[key] ?? [];
if (Array.isArray(value))
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
@@ -47,12 +43,10 @@ export const ArchivedCyclesHeader = observer(function ArchivedCyclesHeader() {
if (currentProjectArchivedFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(projectId.toString(), { [key]: newValues }, "archived");
},
[currentProjectArchivedFilters, projectId, updateFilters]
);
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (archivedCyclesSearchQuery && archivedCyclesSearchQuery.trim() !== "") updateArchivedCyclesSearchQuery("");
@@ -62,9 +56,7 @@ export const ArchivedCyclesHeader = observer(function ArchivedCyclesHeader() {
}
}
};
const isFiltersApplied = calculateTotalFilters(currentProjectArchivedFilters ?? {}) !== 0;
return (
<div className="group relative flex border-b border-custom-border-200">
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
@@ -81,7 +73,7 @@ export const ArchivedCyclesHeader = observer(function ArchivedCyclesHeader() {
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon className="h-3.5 w-3.5" />
</button>
)}
<div
@@ -92,7 +84,7 @@ export const ArchivedCyclesHeader = observer(function ArchivedCyclesHeader() {
}
)}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon 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"
@@ -1,12 +1,11 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
// icons
import { ListFilter, Search } from "lucide-react";
import { ListFilter } from "lucide-react";
// plane helpers
import { useOutsideClickDetector } from "@plane/hooks";
// types
import { useTranslation } from "@plane/i18n";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
import type { TCycleFilters } from "@plane/types";
import { cn, calculateTotalFilters } from "@plane/utils";
// components
@@ -15,11 +14,9 @@ import { FiltersDropdown } from "@/components/issues/issue-layouts/filters";
import { useCycleFilter } from "@/hooks/store/use-cycle-filter";
// local imports
import { CycleFiltersSelection } from "./dropdowns";
type Props = {
projectId: string;
};
export const CyclesViewHeader = observer(function CyclesViewHeader(props: Props) {
const { projectId } = props;
// refs
@@ -33,12 +30,10 @@ export const CyclesViewHeader = observer(function CyclesViewHeader(props: Props)
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
});
const handleFilters = useCallback(
(key: keyof TCycleFilters, value: string | string[]) => {
if (!projectId) return;
const newValues = currentProjectFilters?.[key] ?? [];
if (Array.isArray(value))
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
@@ -48,12 +43,10 @@ export const CyclesViewHeader = observer(function CyclesViewHeader(props: Props)
if (currentProjectFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(projectId, { [key]: newValues });
},
[currentProjectFilters, projectId, updateFilters]
);
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
@@ -63,13 +56,10 @@ export const CyclesViewHeader = observer(function CyclesViewHeader(props: Props)
}
}
};
const isFiltersApplied = calculateTotalFilters(currentProjectFilters ?? {}) !== 0;
useEffect(() => {
if (searchQuery.trim() !== "") setIsSearchOpen(true);
}, [searchQuery]);
return (
<div className="flex items-center gap-3">
{!isSearchOpen && (
@@ -81,7 +71,7 @@ export const CyclesViewHeader = observer(function CyclesViewHeader(props: Props)
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon className="h-3.5 w-3.5" />
</button>
)}
<div
@@ -92,7 +82,7 @@ export const CyclesViewHeader = observer(function CyclesViewHeader(props: Props)
}
)}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon 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"
@@ -1,7 +1,6 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
// plane imports
import type { TCycleFilters, TCycleGroups } from "@plane/types";
// hooks
@@ -10,25 +9,22 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
import { FilterEndDate } from "./end-date";
import { FilterStartDate } from "./start-date";
import { FilterStatus } from "./status";
type Props = {
filters: TCycleFilters;
handleFiltersUpdate: (key: keyof TCycleFilters, value: string | string[]) => void;
isArchived?: boolean;
};
export const CycleFiltersSelection = observer(function CycleFiltersSelection(props: Props) {
const { filters, handleFiltersUpdate, isArchived = false } = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
// hooks
const { isMobile } = usePlatformOS();
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} />
<SearchIcon className="text-custom-text-400" width="12" height="12" strokeWidth={2} />
<input
type="text"
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
@@ -1,38 +1,32 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { AlertCircle, Search } from "lucide-react";
import { AlertCircle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
import { CycleIcon, TransferIcon, CloseIcon } from "@plane/propel/icons";
import { CloseIcon, CycleIcon, SearchIcon, TransferIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EIssuesStoreType } from "@plane/types";
import { useCycle } from "@/hooks/store/use-cycle";
import { useIssues } from "@/hooks/store/use-issues";
//icons
// constants
type Props = {
isOpen: boolean;
handleClose: () => void;
cycleId: string;
};
export const TransferIssuesModal = observer(function TransferIssuesModal(props: Props) {
const { isOpen, handleClose, cycleId } = props;
// states
const [query, setQuery] = useState("");
// store hooks
const { currentProjectIncompleteCycleIds, getCycleById, fetchActiveCycleProgress } = useCycle();
const {
issues: { transferIssuesFromCycle },
} = useIssues(EIssuesStoreType.CYCLE);
const { workspaceSlug, projectId } = useParams();
const transferIssue = async (payload: { new_cycle_id: string }) => {
if (!workspaceSlug || !projectId || !cycleId) return;
await transferIssuesFromCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), payload)
.then(async () => {
setToast({
@@ -50,7 +44,6 @@ export const TransferIssuesModal = observer(function TransferIssuesModal(props:
});
});
};
/**To update issue counts in target cycle and current cycle */
const getCycleDetails = async (newCycleId: string) => {
const cyclesFetch = [
@@ -65,13 +58,10 @@ export const TransferIssuesModal = observer(function TransferIssuesModal(props:
});
});
};
const filteredOptions = currentProjectIncompleteCycleIds?.filter((optionId) => {
const cycleDetails = getCycleById(optionId);
return cycleDetails?.name?.toLowerCase().includes(query?.toLowerCase());
});
// useEffect(() => {
// const handleKeyDown = (e: KeyboardEvent) => {
// if (e.key === "Escape") {
@@ -79,7 +69,6 @@ export const TransferIssuesModal = observer(function TransferIssuesModal(props:
// }
// };
// }, [handleClose]);
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
@@ -118,7 +107,7 @@ export const TransferIssuesModal = observer(function TransferIssuesModal(props:
</button>
</div>
<div className="flex items-center gap-2 border-b border-custom-border-200 px-5 pb-3">
<Search className="h-4 w-4 text-custom-text-200" />
<SearchIcon className="h-4 w-4 text-custom-text-200" />
<input
className="outline-none text-sm"
placeholder="Search for a cycle..."
@@ -131,9 +120,7 @@ export const TransferIssuesModal = observer(function TransferIssuesModal(props:
filteredOptions.length > 0 ? (
filteredOptions.map((optionId) => {
const cycleDetails = getCycleById(optionId);
if (!cycleDetails) return;
return (
<button
key={optionId}
@@ -4,20 +4,17 @@ import type { Placement } from "@popperjs/core";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { usePopper } from "react-popper";
// components
import { Check, Search } from "lucide-react";
import { Check } from "lucide-react";
import { Combobox } from "@headlessui/react";
// i18n
import { useTranslation } from "@plane/i18n";
// icon
import { CycleGroupIcon, CycleIcon } from "@plane/propel/icons";
import { CycleGroupIcon, CycleIcon, SearchIcon } from "@plane/propel/icons";
import type { TCycleGroups } from "@plane/types";
// ui
// store hooks
import { useCycle } from "@/hooks/store/use-cycle";
import { usePlatformOS } from "@/hooks/use-platform-os";
// types
type DropdownOptions =
| {
value: string | null;
@@ -25,7 +22,6 @@ type DropdownOptions =
content: React.ReactNode;
}[]
| undefined;
type CycleOptionsProps = {
projectId: string;
referenceElement: HTMLButtonElement | null;
@@ -34,7 +30,6 @@ type CycleOptionsProps = {
canRemoveCycle: boolean;
currentCycleId?: string;
};
export const CycleOptions = observer(function CycleOptions(props: CycleOptionsProps) {
const { projectId, isOpen, referenceElement, placement, canRemoveCycle, currentCycleId } = props;
// i18n
@@ -47,7 +42,6 @@ export const CycleOptions = observer(function CycleOptions(props: CycleOptionsPr
const { workspaceSlug } = useParams();
const { getProjectCycleIds, fetchAllCycles, getCycleById } = useCycle();
const { isMobile } = usePlatformOS();
useEffect(() => {
if (isOpen) {
onOpen();
@@ -56,7 +50,6 @@ export const CycleOptions = observer(function CycleOptions(props: CycleOptionsPr
}
}
}, [isOpen, isMobile]);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
@@ -69,28 +62,23 @@ export const CycleOptions = observer(function CycleOptions(props: CycleOptionsPr
},
],
});
const cycleIds = (getProjectCycleIds(projectId) ?? [])?.filter((cycleId) => {
const cycleDetails = getCycleById(cycleId);
if (currentCycleId && currentCycleId === cycleId) return false;
return cycleDetails?.status ? (cycleDetails?.status.toLowerCase() != "completed" ? true : false) : true;
});
const onOpen = () => {
if (workspaceSlug && !cycleIds) fetchAllCycles(workspaceSlug.toString(), projectId);
};
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
const options: DropdownOptions = cycleIds?.map((cycleId) => {
const cycleDetails = getCycleById(cycleId);
const cycleStatus = cycleDetails?.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
return {
value: cycleId,
query: `${cycleDetails?.name}`,
@@ -102,7 +90,6 @@ export const CycleOptions = observer(function CycleOptions(props: CycleOptionsPr
),
};
});
if (canRemoveCycle) {
options?.unshift({
value: null,
@@ -115,10 +102,8 @@ export const CycleOptions = observer(function CycleOptions(props: CycleOptionsPr
),
});
}
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
return (
<Combobox.Options className="fixed z-10" static>
<div
@@ -128,7 +113,7 @@ export const CycleOptions = observer(function CycleOptions(props: CycleOptionsPr
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
@@ -148,9 +133,7 @@ export const CycleOptions = observer(function CycleOptions(props: CycleOptionsPr
key={option.value}
value={option.value}
className={({ active, selected }) =>
`flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
`flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${active ? "bg-custom-background-80" : ""} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
@@ -3,11 +3,11 @@ import { useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { usePopper } from "react-popper";
import { Check, Search } from "lucide-react";
import { Check } from "lucide-react";
import { Combobox } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { EstimatePropertyIcon, ChevronDownIcon } from "@plane/propel/icons";
import { ChevronDownIcon, EstimatePropertyIcon, SearchIcon } from "@plane/propel/icons";
import { EEstimateSystem } from "@plane/types";
import { ComboDropDown } from "@plane/ui";
import { convertMinutesToHoursMinutesString, cn } from "@plane/utils";
@@ -20,7 +20,6 @@ import { DropdownButton } from "./buttons";
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
// types
import type { TDropdownProps } from "./types";
type Props = TDropdownProps & {
button?: ReactNode;
dropdownArrow?: boolean;
@@ -31,7 +30,6 @@ type Props = TDropdownProps & {
value: string | undefined | null;
renderByDefault?: boolean;
};
type DropdownOptions =
| {
value: string | null;
@@ -39,7 +37,6 @@ type DropdownOptions =
content: React.ReactNode;
}[]
| undefined;
export const EstimateDropdown = observer(function EstimateDropdown(props: Props) {
const {
button,
@@ -91,11 +88,8 @@ export const EstimateDropdown = observer(function EstimateDropdown(props: Props)
const { estimatePointIds, estimatePointById } = useEstimate(
projectId ? currentActiveEstimateIdByProjectId(projectId) : undefined
);
const currentActiveEstimateId = projectId ? currentActiveEstimateIdByProjectId(projectId) : undefined;
const currentActiveEstimate = currentActiveEstimateId ? getEstimateById(currentActiveEstimateId) : undefined;
const options: DropdownOptions = (estimatePointIds ?? [])
?.map((estimatePoint) => {
const currentEstimatePoint = estimatePointById(estimatePoint);
@@ -127,17 +121,13 @@ export const EstimateDropdown = observer(function EstimateDropdown(props: Props)
</div>
),
});
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
const selectedEstimate = value && estimatePointById ? estimatePointById(value) : undefined;
const onOpen = async () => {
if (!currentActiveEstimateId && workspaceSlug && projectId)
await getProjectEstimates(workspaceSlug.toString(), projectId);
};
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
dropdownRef,
inputRef,
@@ -148,12 +138,10 @@ export const EstimateDropdown = observer(function EstimateDropdown(props: Props)
setIsOpen,
setQuery,
});
const dropdownOnChange = (val: string | undefined) => {
onChange(val);
handleClose();
};
const comboButton = (
<>
{button ? (
@@ -208,7 +196,6 @@ export const EstimateDropdown = observer(function EstimateDropdown(props: Props)
)}
</>
);
return (
<ComboDropDown
as="div"
@@ -231,7 +218,7 @@ export const EstimateDropdown = observer(function EstimateDropdown(props: Props)
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
@@ -1,14 +1,12 @@
"use client";
import type { ReactNode } from "react";
import { useRef, useState } from "react";
import { observer } from "mobx-react";
import { usePopper } from "react-popper";
import { Search } from "lucide-react";
import { Combobox } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { IntakeStateGroupIcon, ChevronDownIcon } from "@plane/propel/icons";
import { ChevronDownIcon, IntakeStateGroupIcon, SearchIcon } from "@plane/propel/icons";
import type { IIntakeState } from "@plane/types";
import { ComboDropDown, Spinner } from "@plane/ui";
import { cn } from "@plane/utils";
@@ -20,7 +18,6 @@ import type { TDropdownProps } from "@/components/dropdowns/types";
import { useDropdown } from "@/hooks/use-dropdown";
// plane web imports
import { StateOption } from "@/plane-web/components/workflow";
export type TWorkItemStateDropdownBaseProps = TDropdownProps & {
alwaysAllowStateChange?: boolean;
button?: ReactNode;
@@ -40,7 +37,6 @@ export type TWorkItemStateDropdownBaseProps = TDropdownProps & {
stateIds: string[];
value: string | undefined | null;
};
export const WorkItemStateDropdownBase: React.FC<TWorkItemStateDropdownBaseProps> = observer((props) => {
const {
button,
@@ -103,7 +99,6 @@ export const WorkItemStateDropdownBase: React.FC<TWorkItemStateDropdownBaseProps
setIsOpen,
setQuery,
});
// derived values
const options = statesList?.map((state) => ({
value: state?.id,
@@ -119,17 +114,13 @@ export const WorkItemStateDropdownBase: React.FC<TWorkItemStateDropdownBaseProps
</div>
),
}));
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
const selectedState = stateValue ? getStateById(stateValue) : undefined;
const dropdownOnChange = (val: string) => {
onChange(val);
handleClose();
};
const comboButton = (
<>
{button ? (
@@ -195,7 +186,6 @@ export const WorkItemStateDropdownBase: React.FC<TWorkItemStateDropdownBaseProps
)}
</>
);
return (
<ComboDropDown
as="div"
@@ -217,7 +207,7 @@ export const WorkItemStateDropdownBase: React.FC<TWorkItemStateDropdownBaseProps
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
@@ -4,11 +4,11 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { createPortal } from "react-dom";
import { usePopper } from "react-popper";
import { Check, Search } from "lucide-react";
import { Check } from "lucide-react";
import { Combobox } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { SuspendedUserIcon } from "@plane/propel/icons";
import { SearchIcon, SuspendedUserIcon } from "@plane/propel/icons";
import { EPillSize, EPillVariant, Pill } from "@plane/propel/pill";
import type { IUserLite } from "@plane/types";
import { Avatar } from "@plane/ui";
@@ -17,7 +17,6 @@ import { cn, getFileURL } from "@plane/utils";
import { useMember } from "@/hooks/store/use-member";
import { useUser } from "@/hooks/store/user";
import { usePlatformOS } from "@/hooks/use-platform-os";
interface Props {
className?: string;
getUserDetails: (userId: string) => IUserLite | undefined;
@@ -28,7 +27,6 @@ interface Props {
placement: Placement | undefined;
referenceElement: HTMLButtonElement | null;
}
export const MemberOptions = observer(function MemberOptions(props: Props) {
const {
getUserDetails,
@@ -66,7 +64,6 @@ export const MemberOptions = observer(function MemberOptions(props: Props) {
},
],
});
useEffect(() => {
if (isOpen) {
onDropdownOpen?.();
@@ -75,14 +72,12 @@ export const MemberOptions = observer(function MemberOptions(props: Props) {
}
}
}, [isOpen, isMobile]);
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
const options = memberIds
?.map((userId) => {
const userDetails = getUserDetails(userId);
@@ -111,10 +106,8 @@ export const MemberOptions = observer(function MemberOptions(props: Props) {
};
})
.filter((o) => !!o);
const filteredOptions =
query === "" ? options : options?.filter((o) => o?.query.toLowerCase().includes(query.toLowerCase()));
return createPortal(
<Combobox.Options data-prevent-outside-click static>
<div
@@ -129,7 +122,7 @@ export const MemberOptions = observer(function MemberOptions(props: Props) {
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
@@ -167,7 +160,7 @@ export const MemberOptions = observer(function MemberOptions(props: Props) {
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
{isUserSuspended(option.value, workspaceSlug?.toString()) && (
<Pill variant={EPillVariant.DEFAULT} size={EPillSize.XS} className="border-none">
<Pill variant={EPillVariant.DEFAULT} className="border-none">
Suspended
</Pill>
)}
@@ -2,16 +2,15 @@ import { useEffect, useRef, useState } from "react";
import type { Placement } from "@popperjs/core";
import { observer } from "mobx-react";
import { usePopper } from "react-popper";
import { Check, Search } from "lucide-react";
import { Check } from "lucide-react";
import { Combobox } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { ModuleIcon } from "@plane/propel/icons";
import { ModuleIcon, SearchIcon } from "@plane/propel/icons";
import type { IModule } from "@plane/types";
import { cn } from "@plane/utils";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
type DropdownOptions =
| {
value: string | null;
@@ -19,7 +18,6 @@ type DropdownOptions =
content: React.ReactNode;
}[]
| undefined;
interface Props {
getModuleById: (moduleId: string) => IModule | null;
isOpen: boolean;
@@ -29,7 +27,6 @@ interface Props {
placement: Placement | undefined;
referenceElement: HTMLButtonElement | null;
}
export const ModuleOptions = observer(function ModuleOptions(props: Props) {
const { getModuleById, isOpen, moduleIds, multiple, onDropdownOpen, placement, referenceElement } = props;
// refs
@@ -41,7 +38,6 @@ export const ModuleOptions = observer(function ModuleOptions(props: Props) {
const { t } = useTranslation();
// store hooks
const { isMobile } = usePlatformOS();
useEffect(() => {
if (isOpen) {
onOpen();
@@ -51,7 +47,6 @@ export const ModuleOptions = observer(function ModuleOptions(props: Props) {
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, isMobile]);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
@@ -64,18 +59,15 @@ export const ModuleOptions = observer(function ModuleOptions(props: Props) {
},
],
});
const onOpen = () => {
onDropdownOpen?.();
};
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
const options: DropdownOptions = moduleIds?.map((moduleId) => {
const moduleDetails = getModuleById(moduleId);
return {
@@ -100,10 +92,8 @@ export const ModuleOptions = observer(function ModuleOptions(props: Props) {
</div>
),
});
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
return (
<Combobox.Options className="fixed z-10" static>
<div
@@ -113,7 +103,7 @@ export const ModuleOptions = observer(function ModuleOptions(props: Props) {
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
+11 -37
View File
@@ -2,12 +2,11 @@ import type { ReactNode } from "react";
import { Fragment, useRef, useState } from "react";
import { useTheme } from "next-themes";
import { usePopper } from "react-popper";
import { Check, Search, SignalHigh } from "lucide-react";
import { Check, SignalHigh } from "lucide-react";
import { Combobox } from "@headlessui/react";
import { ISSUE_PRIORITIES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import { PriorityIcon, ChevronDownIcon } from "@plane/propel/icons";
import { ChevronDownIcon, PriorityIcon, SearchIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import type { TIssuePriorities } from "@plane/types";
// ui
@@ -21,7 +20,6 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants";
// types
import type { TDropdownProps } from "./types";
type Props = TDropdownProps & {
button?: ReactNode;
dropdownArrow?: boolean;
@@ -32,7 +30,6 @@ type Props = TDropdownProps & {
value: TIssuePriorities | undefined | null;
renderByDefault?: boolean;
};
type ButtonProps = {
className?: string;
dropdownArrow: boolean;
@@ -46,7 +43,6 @@ type ButtonProps = {
showTooltip: boolean;
renderToolTipByDefault?: boolean;
};
function BorderButton(props: ButtonProps) {
const {
className,
@@ -60,9 +56,7 @@ function BorderButton(props: ButtonProps) {
showTooltip,
renderToolTipByDefault = true,
} = props;
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
const priorityClasses = {
urgent: "bg-red-600/10 text-red-600 border-red-600 px-1",
high: "bg-orange-500/20 text-orange-950 border-orange-500",
@@ -70,10 +64,8 @@ function BorderButton(props: ButtonProps) {
low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100",
none: "hover:bg-custom-background-80 border-custom-border-300",
};
const { isMobile } = usePlatformOS();
const { t } = useTranslation();
return (
<Tooltip
tooltipHeading={t("priority")}
@@ -106,7 +98,8 @@ function BorderButton(props: ButtonProps) {
>
<PriorityIcon
priority={priority}
size={12}
width="12"
height="12"
className={cn("flex-shrink-0", {
// increase the icon size if text is hidden
"h-3.5 w-3.5": hideText,
@@ -129,7 +122,6 @@ function BorderButton(props: ButtonProps) {
</Tooltip>
);
}
function BackgroundButton(props: ButtonProps) {
const {
className,
@@ -143,9 +135,7 @@ function BackgroundButton(props: ButtonProps) {
showTooltip,
renderToolTipByDefault = true,
} = props;
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
const priorityClasses = {
urgent: "bg-red-600/20 text-red-600",
high: "bg-orange-500/20 text-orange-950",
@@ -153,10 +143,8 @@ function BackgroundButton(props: ButtonProps) {
low: "bg-blue-500/20 text-blue-950",
none: "bg-custom-background-80",
};
const { isMobile } = usePlatformOS();
const { t } = useTranslation();
return (
<Tooltip
tooltipHeading={t("priority")}
@@ -189,7 +177,8 @@ function BackgroundButton(props: ButtonProps) {
>
<PriorityIcon
priority={priority}
size={12}
width="12"
height="12"
className={cn("flex-shrink-0", {
// increase the icon size if text is hidden
"h-3.5 w-3.5": hideText,
@@ -214,7 +203,6 @@ function BackgroundButton(props: ButtonProps) {
</Tooltip>
);
}
function TransparentButton(props: ButtonProps) {
const {
className,
@@ -229,9 +217,7 @@ function TransparentButton(props: ButtonProps) {
showTooltip,
renderToolTipByDefault = true,
} = props;
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
const priorityClasses = {
urgent: "text-red-950",
high: "text-orange-950",
@@ -239,10 +225,8 @@ function TransparentButton(props: ButtonProps) {
low: "text-blue-950",
none: "hover:text-custom-text-300",
};
const { isMobile } = usePlatformOS();
const { t } = useTranslation();
return (
<Tooltip
tooltipHeading={t("priority")}
@@ -276,7 +260,8 @@ function TransparentButton(props: ButtonProps) {
>
<PriorityIcon
priority={priority}
size={12}
width="12"
height="12"
className={cn("flex-shrink-0", {
// increase the icon size if text is hidden
"h-3.5 w-3.5": hideText,
@@ -301,7 +286,6 @@ function TransparentButton(props: ButtonProps) {
</Tooltip>
);
}
export function PriorityDropdown(props: Props) {
//hooks
const { t } = useTranslation();
@@ -346,30 +330,25 @@ export function PriorityDropdown(props: Props) {
},
],
});
// next-themes
// TODO: remove this after new theming implementation
const { resolvedTheme } = useTheme();
const options = ISSUE_PRIORITIES.map((priority) => ({
value: priority.key,
query: priority.key,
content: (
<div className="flex items-center gap-2">
<PriorityIcon priority={priority.key} size={14} withContainer />
<PriorityIcon priority={priority.key} width="14" height="14" withContainer />
<span className="flex-grow truncate">{priority.title}</span>
</div>
),
}));
const filteredOptions =
query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
const dropdownOnChange = (val: TIssuePriorities) => {
onChange(val);
handleClose();
};
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
dropdownRef,
inputRef,
@@ -379,13 +358,11 @@ export function PriorityDropdown(props: Props) {
setIsOpen,
setQuery,
});
const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant)
? BorderButton
: BACKGROUND_BUTTON_VARIANTS.includes(buttonVariant)
? BackgroundButton
: TransparentButton;
const comboButton = (
<>
{button ? (
@@ -433,7 +410,6 @@ export function PriorityDropdown(props: Props) {
)}
</>
);
return (
<ComboDropDown
as="div"
@@ -461,7 +437,7 @@ export function PriorityDropdown(props: Props) {
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
@@ -480,9 +456,7 @@ export function PriorityDropdown(props: Props) {
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${active ? "bg-custom-background-80" : ""} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
@@ -2,12 +2,12 @@ import type { ReactNode } from "react";
import { useRef, useState } from "react";
import { observer } from "mobx-react";
import { usePopper } from "react-popper";
import { Check, Search } from "lucide-react";
import { Check } from "lucide-react";
import { Combobox } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Logo } from "@plane/propel/emoji-icon-picker";
import { ProjectIcon, ChevronDownIcon } from "@plane/propel/icons";
import { ChevronDownIcon, ProjectIcon, SearchIcon } from "@plane/propel/icons";
import { ComboDropDown } from "@plane/ui";
import { cn } from "@plane/utils";
// components
@@ -19,7 +19,6 @@ import type { TProject } from "@/plane-web/types";
import { DropdownButton } from "../buttons";
import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
import type { TDropdownProps } from "../types";
type Props = TDropdownProps & {
button?: ReactNode;
currentProjectId?: string;
@@ -42,7 +41,6 @@ type Props = TDropdownProps & {
value: string[];
}
);
export const ProjectDropdownBase = observer(function ProjectDropdownBase(props: Props) {
const {
button,
@@ -102,7 +100,7 @@ export const ProjectDropdownBase = observer(function ProjectDropdownBase(props:
<div className="flex items-center gap-2">
{projectDetails?.logo_props && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={projectDetails?.logo_props} size={12} />
<Logo logo={projectDetails?.logo_props} width="12" height="12" />
</span>
)}
<span className="flex-grow truncate">{projectDetails?.name}</span>
@@ -110,12 +108,10 @@ export const ProjectDropdownBase = observer(function ProjectDropdownBase(props:
),
};
});
const filteredOptions =
query === ""
? options?.filter((o) => o?.value !== currentProjectId)
: options?.filter((o) => o?.value !== currentProjectId && o?.query.toLowerCase().includes(query.toLowerCase()));
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
dropdownRef,
inputRef,
@@ -125,12 +121,10 @@ export const ProjectDropdownBase = observer(function ProjectDropdownBase(props:
setIsOpen,
setQuery,
});
const dropdownOnChange = (val: string & string[]) => {
onChange(val);
if (!multiple) handleClose();
};
const getDisplayName = (value: string | string[] | null, placeholder: string = "") => {
if (Array.isArray(value)) {
const firstProject = getProjectById(value[0]);
@@ -139,14 +133,12 @@ export const ProjectDropdownBase = observer(function ProjectDropdownBase(props:
return value ? (getProjectById(value)?.name ?? placeholder) : placeholder;
}
};
const getProjectIcon = (value: string | string[] | null) => {
const renderIcon = (logoProps: TProject["logo_props"]) => (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={logoProps} size={14} />
<Logo logo={logoProps} width="14" height="14" />
</span>
);
if (Array.isArray(value)) {
return (
<div className="flex items-center gap-0.5">
@@ -165,7 +157,6 @@ export const ProjectDropdownBase = observer(function ProjectDropdownBase(props:
return projectDetails?.logo_props ? renderIcon(projectDetails.logo_props) : null;
}
};
const comboButton = (
<>
{button ? (
@@ -214,7 +205,6 @@ export const ProjectDropdownBase = observer(function ProjectDropdownBase(props:
)}
</>
);
return (
<ComboDropDown
as="div"
@@ -238,7 +228,7 @@ export const ProjectDropdownBase = observer(function ProjectDropdownBase(props:
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
@@ -260,9 +250,7 @@ export const ProjectDropdownBase = observer(function ProjectDropdownBase(props:
key={option.value}
value={option.value}
className={({ active, selected }) =>
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
`w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${active ? "bg-custom-background-80" : ""} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
@@ -2,11 +2,10 @@ import type { ReactNode } from "react";
import { useRef, useState } from "react";
import { observer } from "mobx-react";
import { usePopper } from "react-popper";
import { Search } from "lucide-react";
import { Combobox } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { StateGroupIcon, ChevronDownIcon } from "@plane/propel/icons";
import { ChevronDownIcon, SearchIcon, StateGroupIcon } from "@plane/propel/icons";
import type { IState } from "@plane/types";
import { ComboDropDown, Spinner } from "@plane/ui";
import { cn } from "@plane/utils";
@@ -18,7 +17,6 @@ import type { TDropdownProps } from "@/components/dropdowns/types";
import { useDropdown } from "@/hooks/use-dropdown";
// plane web imports
import { StateOption } from "@/plane-web/components/workflow";
export type TWorkItemStateDropdownBaseProps = TDropdownProps & {
alwaysAllowStateChange?: boolean;
button?: ReactNode;
@@ -38,7 +36,6 @@ export type TWorkItemStateDropdownBaseProps = TDropdownProps & {
stateIds: string[];
value: string | undefined | null;
};
export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdownBase(
props: TWorkItemStateDropdownBaseProps
) {
@@ -103,7 +100,6 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
setIsOpen,
setQuery,
});
// derived values
const options = statesList?.map((state) => ({
value: state?.id,
@@ -120,17 +116,13 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
</div>
),
}));
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
const selectedState = stateValue ? getStateById(stateValue) : undefined;
const dropdownOnChange = (val: string) => {
onChange(val);
handleClose();
};
const comboButton = (
<>
{button ? (
@@ -197,7 +189,6 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
)}
</>
);
return (
<ComboDropDown
as="div"
@@ -219,7 +210,7 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
@@ -1,8 +1,7 @@
import type { FC } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
// hooks
import { useLabel } from "@/hooks/store/use-label";
import { useMember } from "@/hooks/store/use-member";
@@ -15,7 +14,6 @@ import { FilterMember } from "./members";
import { FilterPriority } from "./priority";
import { FilterState } from "./state";
import { FilterStatus } from "./status";
export const InboxIssueFilterSelection = observer(function InboxIssueFilterSelection() {
// hooks
const { isMobile } = usePlatformOS();
@@ -26,12 +24,11 @@ export const InboxIssueFilterSelection = observer(function InboxIssueFilterSelec
const { projectStates } = useProjectState();
// 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} />
<SearchIcon className="text-custom-text-400" width="12" height="12" strokeWidth={2} />
<input
type="text"
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
@@ -1,10 +1,10 @@
import React, { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { useTheme } from "next-themes";
import { Search } from "lucide-react";
import { Combobox, Dialog, Transition } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { SearchIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { ISearchIssueResponse } from "@plane/types";
import { Loader } from "@plane/ui";
@@ -20,16 +20,13 @@ import { useProject } from "@/hooks/store/use-project";
import useDebounce from "@/hooks/use-debounce";
// services
import { ProjectService } from "@/services/project";
type Props = {
isOpen: boolean;
value?: string | null;
onClose: () => void;
onSubmit: (issueId: string) => void;
};
const projectService = new ProjectService();
export function SelectDuplicateInboxIssueModal(props: Props) {
const { isOpen, onClose, onSubmit, value } = props;
// router
@@ -47,10 +44,8 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
const debouncedSearchTerm: string = useDebounce(query, 500);
const searchResolvedPath = resolvedTheme === "light" ? lightSearchAsset : darkSearchAsset;
const issuesResolvedPath = resolvedTheme === "light" ? lightIssuesAsset : darkIssuesAsset;
useEffect(() => {
if (!isOpen || !workspaceSlug || !projectId) return;
setIsSearching(true);
projectService
.projectIssuesSearch(workspaceSlug.toString(), projectId.toString(), {
@@ -60,13 +55,10 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
.then((res: ISearchIssueResponse[]) => setIssues(res))
.finally(() => setIsSearching(false));
}, [debouncedSearchTerm, isOpen, projectId, workspaceSlug]);
const filteredIssues = issues.filter((issue) => issue.id !== issueId);
const handleClose = () => {
onClose();
};
const handleSubmit = (selectedItem: string) => {
if (!selectedItem || selectedItem.length === 0)
return setToast({
@@ -76,7 +68,6 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
onSubmit(selectedItem);
handleClose();
};
const issueList =
filteredIssues.length > 0 ? (
<li className="p-2">
@@ -86,16 +77,13 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
<ul className="text-sm text-custom-text-100">
{filteredIssues.map((issue) => {
const stateColor = issue.state__color || "";
return (
<Combobox.Option
key={issue.id}
as="div"
value={issue.id}
className={({ active, selected }) =>
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active || selected ? "bg-custom-background-80 text-custom-text-100" : ""
} `
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${active || selected ? "bg-custom-background-80 text-custom-text-100" : ""} `
}
>
<div className="flex items-center gap-2">
@@ -124,7 +112,6 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
)}
</div>
);
return (
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
<div className="flex flex-wrap items-start">
@@ -155,7 +142,7 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-lg bg-custom-background-100 shadow-custom-shadow-md transition-all">
<Combobox value={value} onChange={handleSubmit}>
<div className="relative m-1">
<Search
<SearchIcon
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-custom-text-100 text-opacity-40"
aria-hidden="true"
/>
@@ -1,9 +1,9 @@
import type { FC } from "react";
import { useMemo, useState } from "react";
import { observer } from "mobx-react";
import { ListFilter, Search } from "lucide-react";
import { ListFilter } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
import type { IIssueFilterOptions, IState } from "@plane/types";
import { cn } from "@plane/utils";
import {
@@ -25,18 +25,14 @@ type TSubIssueFiltersProps = {
states?: IState[];
availableFilters: (keyof IIssueFilterOptions)[];
};
export const SubIssueFilters = observer(function SubIssueFilters(props: TSubIssueFiltersProps) {
const { handleFiltersUpdate, filters, memberIds, states, availableFilters } = props;
// plane hooks
const { t } = useTranslation();
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
const isFilterEnabled = (filter: keyof IIssueFilterOptions) => !!availableFilters.includes(filter);
const isFilterApplied = useMemo(() => isFiltersApplied(filters), [filters]);
return (
<>
<FiltersDropdown
@@ -56,7 +52,7 @@ export const SubIssueFilters = observer(function SubIssueFilters(props: TSubIssu
<div className="flex max-h-[350px] 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} />
<SearchIcon className="text-custom-text-400" width="12" height="12" strokeWidth={2} />
<input
type="text"
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
@@ -1,12 +1,12 @@
import { Fragment, useState } from "react";
import { observer } from "mobx-react";
import { usePopper } from "react-popper";
import { Check, Loader, Search } from "lucide-react";
import { Check, Loader } from "lucide-react";
import { Combobox } from "@headlessui/react";
// plane imports
import { EUserPermissionsLevel, getRandomLabelColor } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { LabelPropertyIcon } from "@plane/propel/icons";
import { LabelPropertyIcon, SearchIcon } from "@plane/propel/icons";
import type { IIssueLabel } from "@plane/types";
import { EUserProjectRoles } from "@plane/types";
// helpers
@@ -24,7 +24,6 @@ export interface IIssueLabelSelect {
onSelect: (_labelIds: string[]) => void;
onAddLabel: (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => Promise<any>;
}
export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssueLabelSelect) {
const { workspaceSlug, projectId, issueId, values, onSelect, onAddLabel } = props;
const { t } = useTranslation();
@@ -38,20 +37,15 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue
const [isLoading, setIsLoading] = useState<boolean>(false);
const [query, setQuery] = useState("");
const [submitting, setSubmitting] = useState<boolean>(false);
const canCreateLabel =
projectId && allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId);
const projectLabels = getProjectLabels(projectId);
const { baseTabIndex } = getTabIndex(undefined, isMobile);
const fetchLabels = () => {
setIsLoading(true);
if (!projectLabels && workspaceSlug && projectId)
fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
};
const options = (projectLabels ?? []).map((label) => ({
value: label.id,
query: label.name,
@@ -67,10 +61,8 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue
</div>
),
}));
const filteredOptions =
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "bottom-end",
modifiers: [
@@ -82,9 +74,7 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue
},
],
});
const issueLabels = values ?? [];
const label = (
<div
className={`relative flex flex-shrink-0 cursor-pointer items-center gap-1 rounded-full border border-custom-border-100 p-0.5 px-2 py-0.5 text-xs text-custom-text-300 transition-all hover:bg-custom-background-90 hover:text-custom-text-200`}
@@ -95,20 +85,17 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue
<div className="flex-shrink-0">{t("label.select")}</div>
</div>
);
const searchInputKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
if (query !== "" && e.key === "Enter" && !e.nativeEvent.isComposing && canCreateLabel) {
e.stopPropagation();
e.preventDefault();
await handleAddLabel(query);
}
};
const handleAddLabel = async (labelName: string) => {
setSubmitting(true);
const label = await onAddLabel(workspaceSlug, projectId, { name: labelName, color: getRandomLabelColor() });
@@ -116,9 +103,7 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue
setQuery("");
setSubmitting(false);
};
if (!issueId || !values) return <></>;
return (
<>
<Combobox
@@ -148,7 +133,7 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue
>
<div className="px-2">
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-300" />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-300" />
<Combobox.Input
className="w-full bg-transparent px-2 py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
@@ -169,9 +154,7 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue
key={option.value}
value={option.value}
className={({ selected }) =>
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 hover:bg-custom-background-80 ${
selected ? "text-custom-text-100" : "text-custom-text-200"
}`
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 hover:bg-custom-background-80 ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
@@ -2,13 +2,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { Placement } from "@popperjs/core";
import { useParams } from "next/navigation";
import { usePopper } from "react-popper";
import { Check, Loader, Search } from "lucide-react";
import { Check, Loader } from "lucide-react";
import { Combobox } from "@headlessui/react";
// plane imports
import { EUserPermissionsLevel, getRandomLabelColor } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { ChevronDownIcon } from "@plane/propel/icons";
import { ChevronDownIcon, SearchIcon } from "@plane/propel/icons";
// types
import type { IIssueLabel } from "@plane/types";
import { EUserProjectRoles } from "@plane/types";
@@ -19,7 +19,6 @@ import { useLabel } from "@/hooks/store/use-label";
import { useUserPermissions } from "@/hooks/store/user";
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
import { usePlatformOS } from "@/hooks/use-platform-os";
export interface ILabelDropdownProps {
projectId: string | null;
value: string[];
@@ -38,7 +37,6 @@ export interface ILabelDropdownProps {
fullHeight?: boolean;
label: React.ReactNode;
}
export function LabelDropdown(props: ILabelDropdownProps) {
const {
projectId,
@@ -59,37 +57,29 @@ export function LabelDropdown(props: ILabelDropdownProps) {
label,
} = props;
const { t } = useTranslation();
//router
const { workspaceSlug: routerWorkspaceSlug } = useParams();
const workspaceSlug = routerWorkspaceSlug?.toString();
//states
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [query, setQuery] = useState<string>("");
const [submitting, setSubmitting] = useState<boolean>(false);
//refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
//hooks
const { fetchProjectLabels, getProjectLabels, createLabel } = useLabel();
const { isMobile } = usePlatformOS();
const storeLabels = getProjectLabels(projectId);
const { allowPermissions } = useUserPermissions();
const canCreateLabel =
projectId && allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId);
let projectLabels: IIssueLabel[] = defaultOptions;
if (storeLabels && storeLabels.length > 0) projectLabels = storeLabels;
const options = useMemo(
() =>
projectLabels.map((label) => ({
@@ -109,13 +99,11 @@ export function LabelDropdown(props: ILabelDropdownProps) {
})),
[projectLabels]
);
const filteredOptions = useMemo(
() =>
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())),
[options, query]
);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
@@ -127,25 +115,21 @@ export function LabelDropdown(props: ILabelDropdownProps) {
},
],
});
const onOpen = useCallback(() => {
if (!storeLabels && workspaceSlug && projectId)
fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
}, [storeLabels, workspaceSlug, projectId, fetchProjectLabels]);
const toggleDropdown = useCallback(() => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
if (isOpen && onClose) onClose();
}, [onOpen, onClose, isOpen]);
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
setQuery("");
if (onClose) onClose();
};
const handleAddLabel = async (labelName: string) => {
if (!projectId) return;
setSubmitting(true);
@@ -154,20 +138,17 @@ export function LabelDropdown(props: ILabelDropdownProps) {
setQuery("");
setSubmitting(false);
};
const searchInputKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation();
if (query !== "" && e.key === "Escape") {
setQuery("");
}
if (query !== "" && e.key === "Enter" && !e.nativeEvent.isComposing && canCreateLabel) {
e.preventDefault();
await handleAddLabel(query);
}
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
@@ -176,15 +157,12 @@ export function LabelDropdown(props: ILabelDropdownProps) {
},
[toggleDropdown]
);
useEffect(() => {
if (isOpen && inputRef.current && !isMobile) {
inputRef.current.focus();
}
}, [isOpen, isMobile]);
useOutsideClickDetector(dropdownRef, handleClose);
const comboButton = useMemo(
() => (
<button
@@ -206,12 +184,10 @@ export function LabelDropdown(props: ILabelDropdownProps) {
),
[buttonClassName, disabled, fullWidth, handleOnClick, hideDropdownArrow, label, maxRender, value.length]
);
const preventPropagation = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
};
return (
<div className={`${fullHeight ? "h-full" : "h-5"}`} onClick={preventPropagation}>
<ComboDropDown
@@ -235,7 +211,7 @@ export function LabelDropdown(props: ILabelDropdownProps) {
{...attributes.popper}
>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-300" />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-300" />
<Combobox.Input
ref={inputRef}
className="w-full bg-transparent px-2 py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
@@ -261,9 +237,7 @@ export function LabelDropdown(props: ILabelDropdownProps) {
}
}}
className={({ active, selected }) =>
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 hover:bg-custom-background-80 ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 hover:bg-custom-background-80 ${active ? "bg-custom-background-80" : ""} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
@@ -1,12 +1,12 @@
import React, { useEffect, useState } from "react";
import { useParams } from "next/navigation";
// icons
import { Rocket, Search } from "lucide-react";
import { Rocket } from "lucide-react";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
// i18n
import { useTranslation } from "@plane/i18n";
// types
import { SearchIcon } from "@plane/propel/icons";
import type { ISearchIssueResponse } from "@plane/types";
// ui
import { Loader } from "@plane/ui";
@@ -21,7 +21,6 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
// services
import { ProjectService } from "@/services/project";
type Props = {
isOpen: boolean;
handleClose: () => void;
@@ -31,10 +30,8 @@ type Props = {
issueId?: string;
searchEpic?: boolean;
};
// services
const projectService = new ProjectService();
export function ParentIssuesListModal({
isOpen,
handleClose: onClose,
@@ -46,29 +43,22 @@ export function ParentIssuesListModal({
}: Props) {
// i18n
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
const [isSearching, setIsSearching] = useState(false);
const { isMobile } = usePlatformOS();
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
const { workspaceSlug } = useParams();
const { baseTabIndex } = getTabIndex(undefined, isMobile);
const handleClose = () => {
onClose();
setSearchTerm("");
};
useEffect(() => {
if (!isOpen || !workspaceSlug || !projectId) return;
setIsSearching(true);
setIsLoading(true);
projectService
.projectIssuesSearch(workspaceSlug as string, projectId as string, {
search: debouncedSearchTerm,
@@ -83,7 +73,6 @@ export function ParentIssuesListModal({
setIsLoading(false);
});
}, [debouncedSearchTerm, isOpen, issueId, projectId, workspaceSlug]);
return (
<>
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setSearchTerm("")} appear>
@@ -119,7 +108,7 @@ export function ParentIssuesListModal({
}}
>
<div className="relative m-1">
<Search
<SearchIcon
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-custom-text-100 text-opacity-40"
aria-hidden="true"
/>
@@ -171,9 +160,7 @@ export function ParentIssuesListModal({
key={issue.id}
value={issue}
className={({ active, selected }) =>
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 my-0.5 text-custom-text-200 ${
active ? "bg-custom-background-80 text-custom-text-100" : ""
} ${selected ? "text-custom-text-100" : ""}`
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 my-0.5 text-custom-text-200 ${active ? "bg-custom-background-80 text-custom-text-100" : ""} ${selected ? "text-custom-text-100" : ""}`
}
>
<div className="flex flex-grow items-center gap-2 truncate">
@@ -2,13 +2,13 @@ import React, { useEffect, useRef, useState } from "react";
import type { Placement } from "@popperjs/core";
import { observer } from "mobx-react";
import { usePopper } from "react-popper";
import { Check, Component, Loader, Search } from "lucide-react";
import { Check, Component, Loader } from "lucide-react";
import { Combobox } from "@headlessui/react";
import { getRandomLabelColor } from "@plane/constants";
// plane imports
import { useOutsideClickDetector } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { LabelPropertyIcon } from "@plane/propel/icons";
import { LabelPropertyIcon, SearchIcon } from "@plane/propel/icons";
import type { IIssueLabel } from "@plane/types";
import { cn } from "@plane/utils";
// components
@@ -16,7 +16,6 @@ import { IssueLabelsList } from "@/components/ui/labels-list";
// hooks
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
import { usePlatformOS } from "@/hooks/use-platform-os";
export type TWorkItemLabelSelectBaseProps = {
buttonClassName?: string;
buttonContainerClassName?: string;
@@ -32,7 +31,6 @@ export type TWorkItemLabelSelectBaseProps = {
tabIndex?: number;
value: string[];
};
export const WorkItemLabelSelectBase = observer(function WorkItemLabelSelectBase(props: TWorkItemLabelSelectBaseProps) {
const {
buttonClassName,
@@ -70,27 +68,22 @@ export const WorkItemLabelSelectBase = observer(function WorkItemLabelSelectBase
const labelsList = labelIds.map((labelId) => getLabelById(labelId)).filter((label) => !!label);
const filteredOptions =
query === "" ? labelsList : labelsList?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
const onOpen = () => {
if (referenceElement) referenceElement.focus();
onDropdownOpen?.();
};
const handleClose = () => {
if (isDropdownOpen) setIsDropdownOpen(false);
if (referenceElement) referenceElement.blur();
setQuery("");
};
const toggleDropdown = () => {
if (!isDropdownOpen) onOpen();
setIsDropdownOpen((prevIsOpen) => !prevIsOpen);
};
const dropdownOnChange = (val: string[]) => {
onChange(val);
};
const searchInputKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
const q = query.trim();
if (q !== "" && e.key === "Escape") {
@@ -112,21 +105,17 @@ export const WorkItemLabelSelectBase = observer(function WorkItemLabelSelectBase
}
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => {
if (isDropdownOpen && inputRef.current && !isMobile) {
inputRef.current.focus();
}
}, [isDropdownOpen, isMobile]);
const handleAddLabel = async (labelName: string) => {
if (!createLabel || submitting) return;
const name = labelName.trim();
@@ -143,7 +132,6 @@ export const WorkItemLabelSelectBase = observer(function WorkItemLabelSelectBase
setSubmitting(false);
}
};
return (
<Combobox
as="div"
@@ -197,7 +185,7 @@ export const WorkItemLabelSelectBase = observer(function WorkItemLabelSelectBase
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
@@ -213,16 +201,13 @@ export const WorkItemLabelSelectBase = observer(function WorkItemLabelSelectBase
filteredOptions.length > 0 ? (
filteredOptions.map((label) => {
const children = labelsList?.filter((l) => l.parent === label.id);
if (children.length === 0) {
if (!label.parent)
return (
<Combobox.Option
key={label.id}
className={({ active }) =>
`${
active ? "bg-custom-background-80" : ""
} group flex w-full cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200`
`${active ? "bg-custom-background-80" : ""} group flex w-full cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200`
}
value={label.id}
>
@@ -255,9 +240,7 @@ export const WorkItemLabelSelectBase = observer(function WorkItemLabelSelectBase
<Combobox.Option
key={child.id}
className={({ active }) =>
`${
active ? "bg-custom-background-80" : ""
} group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200`
`${active ? "bg-custom-background-80" : ""} group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200`
}
value={child.id}
>
@@ -2,10 +2,9 @@ import type { FC } from "react";
import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { ListFilter, Search } from "lucide-react";
import { ListFilter } from "lucide-react";
import { useOutsideClickDetector } from "@plane/hooks";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
// plane helpers
// types
import type { TModuleFilters } from "@plane/types";
@@ -18,7 +17,6 @@ import { ModuleFiltersSelection, ModuleOrderByDropdown } from "@/components/modu
// hooks
import { useMember } from "@/hooks/store/use-member";
import { useModuleFilter } from "@/hooks/store/use-module-filter";
export const ArchivedModulesHeader = observer(function ArchivedModulesHeader() {
// router
const { projectId } = useParams();
@@ -42,12 +40,10 @@ export const ArchivedModulesHeader = observer(function ArchivedModulesHeader() {
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && archivedModulesSearchQuery.trim() === "") setIsSearchOpen(false);
});
const handleFilters = useCallback(
(key: keyof TModuleFilters, value: string | string[]) => {
if (!projectId) return;
const newValues = currentProjectArchivedFilters?.[key] ?? [];
if (Array.isArray(value))
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
@@ -57,12 +53,10 @@ export const ArchivedModulesHeader = observer(function ArchivedModulesHeader() {
if (currentProjectArchivedFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(projectId.toString(), { [key]: newValues }, "archived");
},
[currentProjectArchivedFilters, projectId, updateFilters]
);
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (archivedModulesSearchQuery && archivedModulesSearchQuery.trim() !== "") updateArchivedModulesSearchQuery("");
@@ -72,9 +66,7 @@ export const ArchivedModulesHeader = observer(function ArchivedModulesHeader() {
}
}
};
const isFiltersApplied = calculateTotalFilters(currentProjectArchivedFilters ?? {}) !== 0;
return (
<div className="group relative flex border-b border-custom-border-200">
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
@@ -91,7 +83,7 @@ export const ArchivedModulesHeader = observer(function ArchivedModulesHeader() {
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon className="h-3.5 w-3.5" />
</button>
)}
<div
@@ -102,7 +94,7 @@ export const ArchivedModulesHeader = observer(function ArchivedModulesHeader() {
}
)}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon 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"
@@ -1,16 +1,13 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
// plane imports
import type { TModuleStatus } from "@plane/propel/icons";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
import type { TModuleDisplayFilters, TModuleFilters } from "@plane/types";
// components
import { FilterOption } from "@/components/issues/issue-layouts/filters";
import { FilterLead, FilterMembers, FilterStartDate, FilterStatus, FilterTargetDate } from "@/components/modules";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
displayFilters: TModuleDisplayFilters;
filters: TModuleFilters;
@@ -19,7 +16,6 @@ type Props = {
memberIds?: string[] | undefined;
isArchived?: boolean;
};
export const ModuleFiltersSelection = observer(function ModuleFiltersSelection(props: Props) {
const {
displayFilters,
@@ -33,12 +29,11 @@ export const ModuleFiltersSelection = observer(function ModuleFiltersSelection(p
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
// store
const { isMobile } = usePlatformOS();
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} />
<SearchIcon className="text-custom-text-400" width="12" height="12" strokeWidth={2} />
<input
type="text"
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
@@ -2,13 +2,13 @@ import type { FC } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { ListFilter, Search } from "lucide-react";
import { ListFilter } from "lucide-react";
// plane helpers
import { MODULE_VIEW_LAYOUTS } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
// types
import { useTranslation } from "@plane/i18n";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import type { TModuleFilters } from "@plane/types";
// ui
@@ -25,7 +25,6 @@ import { useModuleFilter } from "@/hooks/store/use-module-filter";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { ModuleLayoutIcon } from "./module-layout-icon";
// i18n
export const ModuleViewHeader = observer(function ModuleViewHeader() {
// refs
const inputRef = useRef<HTMLInputElement>(null);
@@ -46,16 +45,13 @@ export const ModuleViewHeader = observer(function ModuleViewHeader() {
updateSearchQuery,
} = useModuleFilter();
const { t } = useTranslation();
// states
const [isSearchOpen, setIsSearchOpen] = useState(searchQuery !== "" ? true : false);
// handlers
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);
@@ -65,12 +61,10 @@ export const ModuleViewHeader = observer(function ModuleViewHeader() {
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("");
@@ -80,18 +74,14 @@ export const ModuleViewHeader = observer(function ModuleViewHeader() {
}
}
};
// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
});
useEffect(() => {
if (searchQuery.trim() !== "") setIsSearchOpen(true);
}, [searchQuery]);
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0 || displayFilters?.favorites;
return (
<div className="hidden h-full sm:flex items-center gap-3 self-end">
<div className="flex items-center">
@@ -104,7 +94,7 @@ export const ModuleViewHeader = observer(function ModuleViewHeader() {
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon className="h-3.5 w-3.5" />
</button>
)}
<div
@@ -115,7 +105,7 @@ export const ModuleViewHeader = observer(function ModuleViewHeader() {
}
)}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon 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"
@@ -1,32 +1,26 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
import type { TPageFilterProps, TPageFilters } from "@plane/types";
// components
import { FilterCreatedDate } from "@/components/common/filters/created-at";
import { FilterCreatedBy } from "@/components/common/filters/created-by";
import { FilterOption } from "@/components/issues/issue-layouts/filters";
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
filters: TPageFilters;
handleFiltersUpdate: <T extends keyof TPageFilters>(filterKey: T, filterValue: TPageFilters[T]) => void;
memberIds?: string[] | undefined;
};
export const PageFiltersSelection = observer(function PageFiltersSelection(props: Props) {
const { filters, handleFiltersUpdate, memberIds } = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
// store
const { isMobile } = usePlatformOS();
const handleFilters = (key: keyof TPageFilterProps, value: boolean | string | string[]) => {
const newValues = filters.filters?.[key] ?? [];
if (typeof newValues === "boolean" && typeof value === "boolean") return;
if (Array.isArray(newValues)) {
if (Array.isArray(value))
value.forEach((val) => {
@@ -38,18 +32,16 @@ export const PageFiltersSelection = observer(function PageFiltersSelection(props
else newValues.push(value);
}
}
handleFiltersUpdate("filters", {
...filters.filters,
[key]: newValues,
});
};
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} />
<SearchIcon className="text-custom-text-400" width="12" height="12" strokeWidth={2} />
<input
type="text"
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
@@ -1,16 +1,13 @@
import { useState, useRef, useEffect } from "react";
import { Search } from "lucide-react";
import { useOutsideClickDetector } from "@plane/hooks";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
// plane helpers
// helpers
import { cn } from "@plane/utils";
type Props = {
searchQuery: string;
updateSearchQuery: (val: string) => void;
};
export function PageSearchInput(props: Props) {
const { searchQuery, updateSearchQuery } = props;
// states
@@ -21,7 +18,6 @@ export function PageSearchInput(props: Props) {
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
});
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
@@ -31,11 +27,9 @@ export function PageSearchInput(props: Props) {
}
}
};
useEffect(() => {
if (searchQuery.trim() !== "") setIsSearchOpen(true);
}, [searchQuery]);
return (
<div className="flex">
{!isSearchOpen && (
@@ -47,7 +41,7 @@ export function PageSearchInput(props: Props) {
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon className="h-3.5 w-3.5" />
</button>
)}
<div
@@ -58,7 +52,7 @@ export function PageSearchInput(props: Props) {
}
)}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon 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"
@@ -1,7 +1,8 @@
import { useCallback } from "react";
import { Link, PanelLeft, Search } from "lucide-react";
import { Link, PanelLeft } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { SearchIcon } from "@plane/propel/icons";
import { setToast, TOAST_TYPE } from "@plane/propel/toast";
import { copyTextToClipboard } from "@plane/utils";
// components
@@ -9,14 +10,12 @@ import type { TPowerKCommandConfig } from "@/components/power-k/core/types";
// hooks
import { useAppTheme } from "@/hooks/store/use-app-theme";
import { usePowerK } from "@/hooks/store/use-power-k";
export const usePowerKMiscellaneousCommands = (): TPowerKCommandConfig[] => {
// store hooks
const { toggleSidebar } = useAppTheme();
const { topNavInputRef, topNavSearchInputRef } = usePowerK();
// translation
const { t } = useTranslation();
const copyCurrentPageUrlToClipboard = useCallback(() => {
const url = new URL(window.location.href);
copyTextToClipboard(url.href)
@@ -34,7 +33,6 @@ export const usePowerKMiscellaneousCommands = (): TPowerKCommandConfig[] => {
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const focusTopNavSearch = useCallback(() => {
// Focus PowerK input if available, otherwise focus regular search input
if (topNavSearchInputRef?.current) {
@@ -43,7 +41,6 @@ export const usePowerKMiscellaneousCommands = (): TPowerKCommandConfig[] => {
topNavInputRef.current.focus();
}
}, [topNavInputRef, topNavSearchInputRef]);
return [
{
id: "toggle_app_sidebar",
@@ -74,7 +71,7 @@ export const usePowerKMiscellaneousCommands = (): TPowerKCommandConfig[] => {
group: "miscellaneous",
type: "action",
i18n_title: "power_k.miscellaneous_actions.focus_top_nav_search",
icon: Search,
icon: SearchIcon,
action: focusTopNavSearch,
modifierShortcut: "cmd+f",
isEnabled: () => true,
@@ -1,20 +1,19 @@
import React from "react";
import { Command } from "cmdk";
import { X, Search } from "lucide-react";
import { X } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
// local imports
import { SearchIcon } from "@plane/propel/icons";
import type { TPowerKContext, TPowerKPageType } from "../../core/types";
import { POWER_K_MODAL_PAGE_DETAILS } from "./constants";
import { PowerKModalContextIndicator } from "./context-indicator";
type Props = {
activePage: TPowerKPageType | null;
context: TPowerKContext;
onSearchChange: (value: string) => void;
searchTerm: string;
};
export function PowerKModalHeader(props: Props) {
const { context, searchTerm, onSearchChange, activePage } = props;
// translation
@@ -23,7 +22,6 @@ export function PowerKModalHeader(props: Props) {
const placeholder = activePage
? t(POWER_K_MODAL_PAGE_DETAILS[activePage].i18n_placeholder)
: t("power_k.page_placeholders.default");
return (
<div className="border-b border-custom-border-200">
{/* Context Indicator */}
@@ -36,7 +34,7 @@ export function PowerKModalHeader(props: Props) {
{/* Search Input */}
<div className="flex items-center gap-2 px-4 py-3">
<Search className="shrink-0 size-4 text-custom-text-400" />
<SearchIcon className="shrink-0 size-4 text-custom-text-400" />
<Command.Input
value={searchTerm}
onValueChange={onSearchChange}
@@ -1,34 +1,27 @@
import { useState, Fragment } from "react";
import { Search } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// plane imports
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
import { Input } from "@plane/ui";
// hooks
import { usePowerK } from "@/hooks/store/use-power-k";
// local imports
import { ShortcutRenderer } from "../renderer/shortcut";
type Props = {
isOpen: boolean;
onClose: () => void;
};
export function ShortcutsModal(props: Props) {
const { isOpen, onClose } = props;
// states
const [query, setQuery] = useState("");
// store hooks
const { commandRegistry } = usePowerK();
// Get all commands from registry
const allCommandsWithShortcuts = commandRegistry.getAllCommandsWithShortcuts();
const handleClose = () => {
onClose();
setQuery("");
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-30" onClose={handleClose}>
@@ -67,7 +60,7 @@ export function ShortcutsModal(props: Props) {
</button>
</Dialog.Title>
<div className="flex w-full items-center rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-200" />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-200" />
<Input
id="search"
name="search"
@@ -1,7 +1,6 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
// plane imports
import type { TProjectDisplayFilters, TProjectFilters } from "@plane/types";
// components
@@ -13,7 +12,6 @@ import { FilterAccess } from "./access";
import { FilterCreatedDate } from "./created-at";
import { FilterLead } from "./lead";
import { FilterMembers } from "./members";
type Props = {
displayFilters: TProjectDisplayFilters;
filters: TProjectFilters;
@@ -21,19 +19,17 @@ type Props = {
handleDisplayFiltersUpdate: (updatedDisplayProperties: Partial<TProjectDisplayFilters>) => void;
memberIds?: string[] | undefined;
};
export const ProjectFiltersSelection = observer(function ProjectFiltersSelection(props: Props) {
const { displayFilters, filters, handleFiltersUpdate, handleDisplayFiltersUpdate, memberIds } = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
// store
const { isMobile } = usePlatformOS();
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} />
<SearchIcon className="text-custom-text-400" width="12" height="12" strokeWidth={2} />
<input
type="text"
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
@@ -1,11 +1,11 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
// components
import { SearchIcon } from "@plane/propel/icons";
import { MembersSettingsLoader } from "@/components/ui/loader/settings/members";
// hooks
import { useMember } from "@/hooks/store/use-member";
@@ -14,12 +14,10 @@ import { useUserPermissions } from "@/hooks/store/user";
import { MemberListFiltersDropdown } from "./dropdowns/filters/member-list";
import { ProjectMemberListItem } from "./member-list-item";
import { SendProjectInvitationModal } from "./send-project-invitation-modal";
type TProjectMemberListProps = {
projectId: string;
workspaceSlug: string;
};
export const ProjectMemberList = observer(function ProjectMemberList(props: TProjectMemberListProps) {
const { projectId, workspaceSlug } = props;
// states
@@ -29,26 +27,18 @@ export const ProjectMemberList = observer(function ProjectMemberList(props: TPro
project: { projectMemberIds, getFilteredProjectMemberDetails, filters },
} = useMember();
const { allowPermissions } = useUserPermissions();
const { t } = useTranslation();
const searchedProjectMembers = (projectMemberIds ?? []).filter((userId) => {
const memberDetails = projectId ? getFilteredProjectMemberDetails(userId, projectId.toString()) : null;
if (!memberDetails?.member || !memberDetails.original_role) return false;
const fullName = `${memberDetails?.member.first_name} ${memberDetails?.member.last_name}`.toLowerCase();
const displayName = memberDetails?.member.display_name.toLowerCase();
return displayName?.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase());
});
const memberDetails = searchedProjectMembers?.map((memberId) =>
projectId ? getFilteredProjectMemberDetails(memberId, projectId.toString()) : null
);
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
// Handler for role filter updates
const handleRoleFilterUpdate = (role: string) => {
if (projectId) {
@@ -57,16 +47,13 @@ export const ProjectMemberList = observer(function ProjectMemberList(props: TPro
const updatedRoles = currentRoles.includes(role)
? currentRoles.filter((r) => r !== role)
: [...currentRoles, role];
filters.updateFilters(projectId, {
roles: updatedRoles.length > 0 ? updatedRoles : undefined,
});
}
};
// Get current role filters
const appliedRoleFilters = projectId ? filters.getFilters(projectId)?.roles || [] : [];
return (
<>
<SendProjectInvitationModal
@@ -79,7 +66,7 @@ export const ProjectMemberList = observer(function ProjectMemberList(props: TPro
<div className="text-base font-semibold">{t("common.members")}</div>
<div className="flex items-center gap-2">
<div className="flex items-center justify-start gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2 py-1">
<Search className="h-3.5 w-3.5" />
<SearchIcon className="h-3.5 w-3.5" />
<input
className="w-full max-w-[234px] border-none bg-transparent text-sm focus:outline-none placeholder:text-custom-text-400"
placeholder="Search"
@@ -96,7 +83,8 @@ export const ProjectMemberList = observer(function ProjectMemberList(props: TPro
{isAdmin && (
<Button
variant="primary"
size="sm"
width="sm"
height="sm"
onClick={() => {
setInviteModal(true);
}}
@@ -2,13 +2,12 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
import { xor } from "lodash-es";
import { observer } from "mobx-react";
import { useTheme } from "next-themes";
import { Search } from "lucide-react";
import { Combobox } from "@headlessui/react";
// plane ui
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { Logo } from "@plane/propel/emoji-icon-picker";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
import { Checkbox, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { cn } from "@plane/utils";
// assets
@@ -18,7 +17,6 @@ import lightProjectAsset from "@/app/assets/empty-state/search/project-light.web
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
// hooks
import { useProject } from "@/hooks/store/use-project";
type Props = {
isOpen: boolean;
onClose: () => void;
@@ -26,7 +24,6 @@ type Props = {
projectIds: string[];
onSubmit: (projectIds: string[]) => Promise<void>;
};
export const ProjectMultiSelectModal = observer(function ProjectMultiSelectModal(props: Props) {
const { isOpen, onClose, selectedProjectIds: selectedProjectIdsProp, projectIds, onSubmit } = props;
// states
@@ -53,11 +50,9 @@ export const ProjectMultiSelectModal = observer(function ProjectMultiSelectModal
return projectQuery.includes(searchTerm.toLowerCase());
});
const filteredProjectResolvedPath = resolvedTheme === "light" ? lightProjectAsset : darkProjectAsset;
useEffect(() => {
if (isOpen) setSelectedProjectIds(selectedProjectIdsProp);
}, [isOpen, selectedProjectIdsProp]);
const handleClose = () => {
onClose();
setTimeout(() => {
@@ -65,27 +60,23 @@ export const ProjectMultiSelectModal = observer(function ProjectMultiSelectModal
setSelectedProjectIds([]);
}, 300);
};
const handleSubmit = async () => {
setIsSubmitting(true);
await onSubmit(selectedProjectIds);
setIsSubmitting(false);
handleClose();
};
const handleSelectedProjectChange = (val: string[]) => {
setSelectedProjectIds(val);
setSearchTerm("");
moveButtonRef.current?.focus();
};
if (!isOpen) return null;
return (
<ModalCore isOpen={isOpen} width={EModalWidth.LG} position={EModalPosition.TOP} handleClose={handleClose}>
<Combobox as="div" multiple value={selectedProjectIds} onChange={handleSelectedProjectChange}>
<div className="flex items-center gap-2 px-4 border-b border-custom-border-100">
<Search className="flex-shrink-0 size-4 text-custom-text-400" aria-hidden="true" />
<SearchIcon className="flex-shrink-0 size-4 text-custom-text-400" aria-hidden="true" />
<Combobox.Input
className="h-12 w-full border-0 bg-transparent text-sm text-custom-text-100 outline-none placeholder:text-custom-text-400 focus:ring-0"
placeholder="Search for projects"
@@ -107,7 +98,7 @@ export const ProjectMultiSelectModal = observer(function ProjectMultiSelectModal
handleSelectedProjectChange(selectedProjectIds.filter((id) => id !== projectDetails.id));
}}
>
<Logo logo={projectDetails.logo_props} size={14} />
<Logo logo={projectDetails.logo_props} width="14" height="14" />
<p className="text-xs truncate text-custom-text-300 group-hover:text-custom-text-200 transition-colors">
{projectDetails.identifier}
</p>
@@ -156,7 +147,7 @@ export const ProjectMultiSelectModal = observer(function ProjectMultiSelectModal
<div className="flex items-center gap-2 truncate">
<span className="flex-shrink-0 flex items-center gap-2.5">
<Checkbox checked={isProjectSelected} />
<Logo logo={projectDetails.logo_props} size={16} />
<Logo logo={projectDetails.logo_props} width="16" height="16" />
</span>
<span className="flex-shrink-0 text-[10px]">{projectDetails.identifier}</span>
<p className="text-sm truncate">{projectDetails.name}</p>
@@ -169,13 +160,14 @@ export const ProjectMultiSelectModal = observer(function ProjectMultiSelectModal
</Combobox.Options>
</Combobox>
<div className="flex items-center justify-end gap-2 p-3 border-t border-custom-border-100">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
<Button variant="neutral-primary" width="sm" height="sm" onClick={handleClose}>
{t("cancel")}
</Button>
<Button
ref={moveButtonRef}
variant="primary"
size="sm"
width="sm"
height="sm"
onClick={handleSubmit}
loading={isSubmitting}
disabled={!areSelectedProjectsChanged}
@@ -1,16 +1,14 @@
import { useRef, useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
// plane hooks
import { useOutsideClickDetector } from "@plane/hooks";
// i18n
import { useTranslation } from "@plane/i18n";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
// helpers
import { cn } from "@plane/utils";
// hooks
import { useProjectFilter } from "@/hooks/store/use-project-filter";
export const ProjectSearch = observer(function ProjectSearch() {
// i18n
const { t } = useTranslation();
@@ -24,14 +22,12 @@ export const ProjectSearch = observer(function ProjectSearch() {
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
});
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
else setIsSearchOpen(false);
}
};
return (
<div className="flex items-center">
{!isSearchOpen && (
@@ -43,7 +39,7 @@ export const ProjectSearch = observer(function ProjectSearch() {
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon className="h-3.5 w-3.5" />
</button>
)}
<div
@@ -54,7 +50,7 @@ export const ProjectSearch = observer(function ProjectSearch() {
}
)}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon 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"
@@ -1,10 +1,8 @@
import { Search } from "lucide-react";
import { SearchIcon } from "@plane/propel/icons";
import { cn } from "@plane/utils";
type Props = {
isActive?: boolean;
};
export function SidebarSearchButton(props: Props) {
const { isActive } = props;
return (
@@ -16,7 +14,7 @@ export function SidebarSearchButton(props: Props) {
}
)}
>
<Search
<SearchIcon
className={cn("size-4 text-custom-sidebar-text-300", {
"text-custom-primary-200": isActive,
})}
@@ -3,15 +3,13 @@ import { useCallback, useRef, useState } from "react";
import { debounce } from "lodash-es";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Search } from "lucide-react";
// plane hooks
import { useOutsideClickDetector } from "@plane/hooks";
// helpers
import { useTranslation } from "@plane/i18n";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
import { cn } from "@plane/utils";
import { useSticky } from "@/hooks/use-stickies";
export const StickySearch = observer(function StickySearch() {
// router
const { workspaceSlug } = useParams();
@@ -22,7 +20,6 @@ export const StickySearch = observer(function StickySearch() {
const inputRef = useRef<HTMLInputElement>(null);
// states
const [isSearchOpen, setIsSearchOpen] = useState(false);
// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
@@ -35,18 +32,15 @@ export const StickySearch = observer(function StickySearch() {
} else setIsSearchOpen(false);
}
};
const fetchStickies = async () => {
await fetchWorkspaceStickies(workspaceSlug.toString());
};
const debouncedSearch = useCallback(
debounce(async () => {
await fetchStickies();
}, 500),
[fetchWorkspaceStickies]
);
return (
<div className="flex items-center mr-2 my-auto">
{!isSearchOpen && (
@@ -58,7 +52,7 @@ export const StickySearch = observer(function StickySearch() {
inputRef.current?.focus();
}}
>
<Search className=" size-4 " />
<SearchIcon className=" size-4 " />
</button>
)}
<div
@@ -69,7 +63,7 @@ export const StickySearch = observer(function StickySearch() {
}
)}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon 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"
@@ -1,7 +1,6 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
import type { TViewFilterProps, TViewFilters } from "@plane/types";
import { EViewAccess } from "@plane/types";
// components
@@ -13,26 +12,21 @@ import { FilterOption } from "@/components/issues/issue-layouts/filters";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { FilterByAccess } from "@/plane-web/components/views/filters/access-filter";
type Props = {
filters: TViewFilters;
handleFiltersUpdate: <T extends keyof TViewFilters>(filterKey: T, filterValue: TViewFilters[T]) => void;
memberIds?: string[] | undefined;
};
export const ViewFiltersSelection = observer(function ViewFiltersSelection(props: Props) {
const { filters, handleFiltersUpdate, memberIds } = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
// store
const { isMobile } = usePlatformOS();
// handles filter update
const handleFilters = (key: keyof TViewFilterProps, value: boolean | string | EViewAccess | string[]) => {
const currValues = (filters.filters?.[key] ?? []) as (string | EViewAccess)[];
if (typeof currValues === "boolean" && typeof value === "boolean") return;
if (Array.isArray(currValues)) {
if (Array.isArray(value)) {
value.forEach((val) => {
@@ -44,18 +38,16 @@ export const ViewFiltersSelection = observer(function ViewFiltersSelection(props
else currValues.push(value);
}
}
handleFiltersUpdate("filters", {
...filters.filters,
[key]: currValues,
});
};
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} />
<SearchIcon className="text-custom-text-400" width="12" height="12" strokeWidth={2} />
<input
type="text"
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
@@ -1,9 +1,8 @@
import React, { useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
// icons
import { ListFilter, Search } from "lucide-react";
import { ListFilter } from "lucide-react";
import { useOutsideClickDetector } from "@plane/hooks";
import { CloseIcon } from "@plane/propel/icons";
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
// plane helpers
// helpers
import { cn } from "@plane/utils";
@@ -13,7 +12,6 @@ import { useProjectView } from "@/hooks/store/use-project-view";
import { FiltersDropdown } from "../issues/issue-layouts/filters";
import { ViewFiltersSelection } from "./filters/filter-selection";
import { ViewOrderByDropdown } from "./filters/order-by";
export const ViewListHeader = observer(function ViewListHeader() {
// states
const [isSearchOpen, setIsSearchOpen] = useState(false);
@@ -24,7 +22,6 @@ export const ViewListHeader = observer(function ViewListHeader() {
const {
project: { projectMemberIds },
} = useMember();
// handlers
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
@@ -36,16 +33,13 @@ export const ViewListHeader = observer(function ViewListHeader() {
}
}
};
// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && filters?.searchQuery.trim() === "") setIsSearchOpen(false);
});
useEffect(() => {
if (filters?.searchQuery.trim() !== "") setIsSearchOpen(true);
}, [filters?.searchQuery]);
return (
<div className="h-full flex items-center gap-2">
<div className="flex items-center">
@@ -58,7 +52,7 @@ export const ViewListHeader = observer(function ViewListHeader() {
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon className="h-3.5 w-3.5" />
</button>
)}
<div
@@ -69,7 +63,7 @@ export const ViewListHeader = observer(function ViewListHeader() {
}
)}
>
<Search className="h-3.5 w-3.5" />
<SearchIcon 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"
+2 -24
View File
@@ -1,11 +1,9 @@
import * as React from "react";
import { Combobox as BaseCombobox } from "@base-ui-components/react/combobox";
import { Search } from "lucide-react";
import { SearchIcon } from "@plane/propel/icons";
import { cn } from "../utils/classname";
// Type definitions
type TMaxHeight = "lg" | "md" | "rg" | "sm";
export interface ComboboxProps {
value?: string | string[];
defaultValue?: string | string[];
@@ -17,14 +15,12 @@ export interface ComboboxProps {
onOpenChange?: (open: boolean) => void;
children: React.ReactNode;
}
export interface ComboboxButtonProps {
disabled?: boolean;
children?: React.ReactNode;
className?: string;
ref?: React.Ref<HTMLButtonElement>;
}
export interface ComboboxOptionsProps {
searchPlaceholder?: string;
emptyMessage?: string;
@@ -40,14 +36,12 @@ export interface ComboboxOptionsProps {
onSearchQueryKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
dataPreventOutsideClick?: boolean;
}
export interface ComboboxOptionProps {
value: string;
disabled?: boolean;
children?: React.ReactNode;
className?: string;
}
// Constants
const MAX_HEIGHT_CLASSES: Record<TMaxHeight, string> = {
lg: "max-h-60",
@@ -55,7 +49,6 @@ const MAX_HEIGHT_CLASSES: Record<TMaxHeight, string> = {
rg: "max-h-36",
sm: "max-h-28",
} as const;
// Root component
function ComboboxRoot({
value,
@@ -73,7 +66,6 @@ function ComboboxRoot({
},
[onValueChange]
);
return (
<BaseCombobox.Root
value={value}
@@ -88,7 +80,6 @@ function ComboboxRoot({
</BaseCombobox.Root>
);
}
// Trigger button component
const ComboboxButton = React.forwardRef(function ComboboxButton(
{ className, children, disabled = false }: ComboboxButtonProps,
@@ -100,7 +91,6 @@ const ComboboxButton = React.forwardRef(function ComboboxButton(
</BaseCombobox.Trigger>
);
});
// Options popup component
function ComboboxOptions({
children,
@@ -119,9 +109,7 @@ function ComboboxOptions({
}: ComboboxOptionsProps) {
// const [searchQuery, setSearchQuery] = React.useState("");
const [internalSearchQuery, setInternalSearchQuery] = React.useState("");
const searchQuery = controlledSearchQuery !== undefined ? controlledSearchQuery : internalSearchQuery;
const setSearchQuery = React.useCallback(
(query: string) => {
if (onSearchQueryChange) {
@@ -132,17 +120,13 @@ function ComboboxOptions({
},
[onSearchQueryChange]
);
// Filter children based on search query
const filteredChildren = React.useMemo(() => {
if (!showSearch || !searchQuery) return children;
return React.Children.toArray(children).filter((child) => {
if (!React.isValidElement(child)) return true;
// Only filter ComboboxOption components, leave other elements (like additional content) unfiltered
if (child.type !== ComboboxOption) return true;
// Extract text content from child to search against
const getTextContent = (node: React.ReactNode): string => {
if (typeof node === "string") return node;
@@ -155,15 +139,12 @@ function ComboboxOptions({
}
return "";
};
const textContent = getTextContent(child.props.children);
const value = child.props.value || "";
const searchLower = searchQuery.toLowerCase();
return textContent.toLowerCase().includes(searchLower) || String(value).toLowerCase().includes(searchLower);
});
}, [children, searchQuery, showSearch]);
return (
<BaseCombobox.Portal>
<BaseCombobox.Positioner sideOffset={8} className={positionerClassName}>
@@ -174,7 +155,7 @@ function ComboboxOptions({
<div className="flex flex-col gap-1">
{showSearch && (
<div className="relative">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-custom-text-400" />
<SearchIcon className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-custom-text-400" />
<input
type="text"
placeholder={searchPlaceholder}
@@ -206,7 +187,6 @@ function ComboboxOptions({
</BaseCombobox.Portal>
);
}
// Individual option component
function ComboboxOption({ value, children, disabled, className }: ComboboxOptionProps) {
return (
@@ -219,12 +199,10 @@ function ComboboxOption({ value, children, disabled, className }: ComboboxOption
</BaseCombobox.Item>
);
}
// Compound component export
const Combobox = Object.assign(ComboboxRoot, {
Button: ComboboxButton,
Options: ComboboxOptions,
Option: ComboboxOption,
});
export { Combobox };
@@ -1,17 +1,16 @@
import React, { useEffect, useState } from "react";
import { InfoIcon, Search } from "lucide-react";
import { InfoIcon } from "lucide-react";
import { SearchIcon } from "@plane/propel/icons";
import { cn } from "../../utils/classname";
import { adjustColorForContrast, DEFAULT_COLORS } from "../helper";
import { LucideIconsList } from "./lucide-root";
import { MaterialIconList } from "./material-root";
type IconRootProps = {
onChange: (value: { name: string; color: string }) => void;
defaultColor: string;
searchDisabled?: boolean;
iconType: "material" | "lucide";
};
export function IconRoot(props: IconRootProps) {
const { defaultColor, onChange, searchDisabled = false, iconType } = props;
// states
@@ -20,7 +19,6 @@ export function IconRoot(props: IconRootProps) {
const [hexValue, setHexValue] = useState("");
const [isInputFocused, setIsInputFocused] = useState(false);
const [query, setQuery] = useState("");
useEffect(() => {
if (DEFAULT_COLORS.includes(defaultColor.toLowerCase() ?? "")) setShowHexInput(false);
else {
@@ -28,7 +26,6 @@ export function IconRoot(props: IconRootProps) {
setShowHexInput(true);
}
}, [defaultColor]);
return (
<>
<div className="flex flex-col sticky top-0 bg-custom-background-100">
@@ -45,7 +42,7 @@ export function IconRoot(props: IconRootProps) {
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
>
<Search className="absolute left-2.5 bottom-3 h-3.5 w-3.5 text-custom-text-400" />
<SearchIcon className="absolute left-2.5 bottom-3 h-3.5 w-3.5 text-custom-text-400" />
<input
placeholder="Search"
@@ -148,11 +148,10 @@ import {
CircleChevronDown,
UsersRound,
ToggleLeft,
Search,
User,
} from "lucide-react";
import { SearchIcon } from "@plane/propel/icons";
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon } from "../icons";
export const LUCIDE_ICONS_LIST = [
{ name: "Activity", element: Activity },
{ name: "Airplay", element: Airplay },
@@ -305,7 +304,7 @@ export const LUCIDE_ICONS_LIST = [
{ name: "Minus", element: Minus },
{ name: "MinusCircle", element: MinusCircle },
{ name: "MinusSquare", element: MinusSquare },
{ name: "Search", element: Search },
{ name: "Search", element: SearchIcon },
{ name: "ToggleLeft", element: ToggleLeft },
{ name: "User", element: User },
{ name: "UsersRound", element: UsersRound },
+2 -5
View File
@@ -148,11 +148,9 @@ import {
CircleChevronDown,
UsersRound,
ToggleLeft,
Search,
User,
} from "lucide-react";
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon } from "@plane/propel/icons";
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, SearchIcon } from "@plane/propel/icons";
export const MATERIAL_ICONS_LIST = [
{
name: "search",
@@ -758,7 +756,6 @@ export const MATERIAL_ICONS_LIST = [
name: "skull",
},
];
export const LUCIDE_ICONS_LIST = [
{ name: "Activity", element: Activity },
{ name: "Airplay", element: Airplay },
@@ -911,7 +908,7 @@ export const LUCIDE_ICONS_LIST = [
{ name: "Minus", element: Minus },
{ name: "MinusCircle", element: MinusCircle },
{ name: "MinusSquare", element: MinusSquare },
{ name: "Search", element: Search },
{ name: "Search", element: SearchIcon },
{ name: "ToggleLeft", element: ToggleLeft },
{ name: "User", element: User },
{ name: "UsersRound", element: UsersRound },
@@ -1,10 +1,9 @@
import { Combobox } from "@headlessui/react";
import { Search } from "lucide-react";
import type { FC } from "react";
import React, { useEffect, useRef } from "react";
// helpers
import { SearchIcon } from "@plane/propel/icons";
import { cn } from "../../utils";
interface IInputSearch {
isOpen: boolean;
query: string;
@@ -15,27 +14,22 @@ interface IInputSearch {
inputPlaceholder?: string;
isMobile: boolean;
}
export function InputSearch(props: IInputSearch) {
const { isOpen, query, updateQuery, inputIcon, inputContainerClassName, inputClassName, inputPlaceholder, isMobile } =
props;
const inputRef = useRef<HTMLInputElement | null>(null);
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
updateQuery("");
}
};
useEffect(() => {
if (isOpen && !isMobile) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
inputRef.current && inputRef.current.focus();
}
}, [isOpen, isMobile]);
return (
<div
className={cn(
@@ -43,7 +37,7 @@ export function InputSearch(props: IInputSearch) {
inputContainerClassName
)}
>
{inputIcon ? <>{inputIcon}</> : <Search className="h-4 w-4 text-custom-text-300" aria-hidden="true" />}
{inputIcon ? <>{inputIcon}</> : <SearchIcon className="h-4 w-4 text-custom-text-300" aria-hidden="true" />}
<Combobox.Input
as="input"
ref={inputRef}
@@ -1,17 +1,16 @@
import { Combobox } from "@headlessui/react";
import { Check, Info, Search } from "lucide-react";
import { Check, Info } from "lucide-react";
import React, { useRef, useState } from "react";
import { createPortal } from "react-dom";
import { usePopper } from "react-popper";
import { useOutsideClickDetector } from "@plane/hooks";
import { ChevronDownIcon } from "@plane/propel/icons";
import { ChevronDownIcon, SearchIcon } from "@plane/propel/icons";
// plane imports
// local imports
import { Tooltip } from "@plane/propel/tooltip";
import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
import { cn } from "../utils";
import type { ICustomSearchSelectProps } from "./helper";
export function CustomSearchSelect(props: ICustomSearchSelectProps) {
const {
customButtonClassName = "",
@@ -38,47 +37,37 @@ export function CustomSearchSelect(props: ICustomSearchSelectProps) {
defaultOpen = false,
} = props;
const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [isOpen, setIsOpen] = useState(defaultOpen);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
});
const filteredOptions =
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
const comboboxProps: any = {
value,
onChange,
disabled,
};
if (multiple) comboboxProps.multiple = true;
const openDropdown = () => {
setIsOpen(true);
if (referenceElement) referenceElement.focus();
if (onOpen) onOpen();
};
const closeDropdown = () => {
setIsOpen(false);
onClose && onClose();
};
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const toggleDropdown = () => {
if (isOpen) closeDropdown();
else openDropdown();
};
return (
<Combobox
as="div"
@@ -90,7 +79,6 @@ export function CustomSearchSelect(props: ICustomSearchSelectProps) {
>
{({ open }: { open: boolean }) => {
if (open && onOpen) onOpen();
return (
<>
{customButton ? (
@@ -148,7 +136,7 @@ export function CustomSearchSelect(props: ICustomSearchSelectProps) {
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2 mx-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<SearchIcon className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}