Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { NETWORK_CHOICES } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "@/components/issues/issue-layouts/filters";
|
||||
// local imports
|
||||
import { ProjectNetworkIcon } from "../../project-network-icon";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (val: string) => void;
|
||||
searchQuery: string;
|
||||
};
|
||||
|
||||
export const FilterAccess: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||
// states
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
const filteredOptions = NETWORK_CHOICES.filter((a) => a.i18n_label.includes(searchQuery.toLowerCase()));
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterHeader
|
||||
title={`Access${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||
isPreviewEnabled={previewEnabled}
|
||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||
/>
|
||||
{previewEnabled && (
|
||||
<div>
|
||||
{filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((access) => (
|
||||
<FilterOption
|
||||
key={access.key}
|
||||
isChecked={appliedFilters?.includes(`${access.key}`) ? true : false}
|
||||
onClick={() => handleUpdate(`${access.key}`)}
|
||||
icon={<ProjectNetworkIcon iconKey={access.iconKey} />}
|
||||
title={t(access.i18n_label)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane constants
|
||||
import { PROJECT_CREATED_AT_FILTER_OPTIONS } from "@plane/constants";
|
||||
// components
|
||||
import { isInDateFormat } from "@plane/utils";
|
||||
import { DateFilterModal } from "@/components/core/filters/date-filter-modal";
|
||||
import { FilterHeader, FilterOption } from "@/components/issues/issue-layouts/filters";
|
||||
|
||||
// helpers
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (val: string | string[]) => void;
|
||||
searchQuery: string;
|
||||
};
|
||||
|
||||
export const FilterCreatedDate: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||
// state
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
|
||||
// derived values
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
const filteredOptions = PROJECT_CREATED_AT_FILTER_OPTIONS.filter((d) =>
|
||||
d.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const isCustomDateSelected = () => {
|
||||
const isValidDateSelected = appliedFilters?.filter((f) => isInDateFormat(f.split(";")[0])) || [];
|
||||
return isValidDateSelected.length > 0 ? true : false;
|
||||
};
|
||||
const handleCustomDate = () => {
|
||||
if (isCustomDateSelected()) {
|
||||
const updateAppliedFilters = appliedFilters?.filter((f) => f.includes("-")) || [];
|
||||
handleUpdate(updateAppliedFilters);
|
||||
} else setIsDateFilterModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDateFilterModalOpen && (
|
||||
<DateFilterModal
|
||||
handleClose={() => setIsDateFilterModalOpen(false)}
|
||||
isOpen={isDateFilterModalOpen}
|
||||
onSelect={(val) => handleUpdate(val)}
|
||||
title="Created date"
|
||||
/>
|
||||
)}
|
||||
<FilterHeader
|
||||
title={`Created date${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||
isPreviewEnabled={previewEnabled}
|
||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||
/>
|
||||
{previewEnabled && (
|
||||
<div>
|
||||
{filteredOptions.length > 0 ? (
|
||||
<>
|
||||
{filteredOptions.map((option) => (
|
||||
<FilterOption
|
||||
key={option.value}
|
||||
isChecked={appliedFilters?.includes(option.value) ? true : false}
|
||||
onClick={() => handleUpdate(option.value)}
|
||||
title={option.name}
|
||||
multiple={false}
|
||||
/>
|
||||
))}
|
||||
<FilterOption
|
||||
isChecked={isCustomDateSelected()}
|
||||
onClick={handleCustomDate}
|
||||
title="Custom"
|
||||
multiple={false}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
111
apps/web/core/components/project/dropdowns/filters/lead.tsx
Normal file
111
apps/web/core/components/project/dropdowns/filters/lead.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { sortBy } from "lodash-es";
|
||||
import { observer } from "mobx-react";
|
||||
// plane ui
|
||||
import { Avatar, Loader } from "@plane/ui";
|
||||
// components
|
||||
import { getFileURL } from "@plane/utils";
|
||||
import { FilterHeader, FilterOption } from "@/components/issues/issue-layouts/filters";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (val: string) => void;
|
||||
memberIds: string[] | undefined;
|
||||
searchQuery: string;
|
||||
};
|
||||
|
||||
export const FilterLead: React.FC<Props> = observer((props: Props) => {
|
||||
const { appliedFilters, handleUpdate, memberIds, searchQuery } = props;
|
||||
// states
|
||||
const [itemsToRender, setItemsToRender] = useState(5);
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
|
||||
const sortedOptions = useMemo(() => {
|
||||
const filteredOptions = (memberIds || []).filter((memberId) =>
|
||||
getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return sortBy(filteredOptions, [
|
||||
(memberId) => !(appliedFilters ?? []).includes(memberId),
|
||||
(memberId) => memberId !== currentUser?.id,
|
||||
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchQuery]);
|
||||
|
||||
const handleViewToggle = () => {
|
||||
if (!sortedOptions) return;
|
||||
|
||||
if (itemsToRender === sortedOptions.length) setItemsToRender(5);
|
||||
else setItemsToRender(sortedOptions.length);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterHeader
|
||||
title={`Lead${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||
isPreviewEnabled={previewEnabled}
|
||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||
/>
|
||||
{previewEnabled && (
|
||||
<div>
|
||||
{sortedOptions ? (
|
||||
sortedOptions.length > 0 ? (
|
||||
<>
|
||||
{sortedOptions.slice(0, itemsToRender).map((memberId) => {
|
||||
const member = getUserDetails(memberId);
|
||||
|
||||
if (!member) return null;
|
||||
return (
|
||||
<FilterOption
|
||||
key={`lead-${member.id}`}
|
||||
isChecked={appliedFilters?.includes(member.id) ? true : false}
|
||||
onClick={() => handleUpdate(member.id)}
|
||||
icon={
|
||||
<Avatar
|
||||
name={member.display_name}
|
||||
src={getFileURL(member.avatar_url)}
|
||||
showTooltip={false}
|
||||
size="md"
|
||||
/>
|
||||
}
|
||||
title={currentUser?.id === member.id ? "You" : member?.display_name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{sortedOptions.length > 5 && (
|
||||
<button
|
||||
type="button"
|
||||
className="ml-8 text-xs font-medium text-custom-primary-100"
|
||||
onClick={handleViewToggle}
|
||||
>
|
||||
{itemsToRender === sortedOptions.length ? "View less" : "View all"}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-2">
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { ChevronDownIcon } from "@plane/propel/icons";
|
||||
import { EUserProjectRoles, EUserWorkspaceRoles } from "@plane/types";
|
||||
// plane ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "@/components/issues/issue-layouts/filters";
|
||||
|
||||
interface IRoleOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (role: string) => void;
|
||||
memberType: "project" | "workspace";
|
||||
};
|
||||
|
||||
const PROJECT_ROLE_OPTIONS: IRoleOption[] = [
|
||||
{ value: String(EUserProjectRoles.ADMIN), label: "Admin" },
|
||||
{ value: String(EUserProjectRoles.MEMBER), label: "Member" },
|
||||
{ value: String(EUserProjectRoles.GUEST), label: "Guest" },
|
||||
];
|
||||
|
||||
const WORKSPACE_ROLE_OPTIONS: IRoleOption[] = [
|
||||
{ value: String(EUserWorkspaceRoles.ADMIN), label: "Admin" },
|
||||
{ value: String(EUserWorkspaceRoles.MEMBER), label: "Member" },
|
||||
{ value: String(EUserWorkspaceRoles.GUEST), label: "Guest" },
|
||||
{ value: "suspended", label: "Suspended" },
|
||||
];
|
||||
|
||||
// Role filter group component
|
||||
const RoleFilterGroup: React.FC<{
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (role: string) => void;
|
||||
memberType: "project" | "workspace";
|
||||
}> = observer(({ appliedFilters, handleUpdate, memberType }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
const roleOptions = memberType === "project" ? PROJECT_ROLE_OPTIONS : WORKSPACE_ROLE_OPTIONS;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<FilterHeader
|
||||
title={`Roles${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||
isPreviewEnabled={isExpanded}
|
||||
handleIsPreviewEnabled={() => setIsExpanded(!isExpanded)}
|
||||
/>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="space-y-1">
|
||||
{roleOptions.map((role) => {
|
||||
const isSelected = appliedFilters?.includes(role.value) ?? false;
|
||||
return (
|
||||
<FilterOption
|
||||
key={`role-${role.value}`}
|
||||
isChecked={isSelected}
|
||||
title={role.label}
|
||||
onClick={() => handleUpdate(role.value)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const MemberListFilters: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, memberType } = props;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Role Filter Group */}
|
||||
<RoleFilterGroup appliedFilters={appliedFilters} handleUpdate={handleUpdate} memberType={memberType} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// Dropdown component for member list filters
|
||||
export const MemberListFiltersDropdown: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, memberType } = props;
|
||||
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
|
||||
return (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div className="relative">
|
||||
<Button variant="neutral-primary" size="sm" className="flex items-center gap-2">
|
||||
<span>Filters</span>
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
{appliedFiltersCount > 0 && (
|
||||
<div className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-custom-primary-100" />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
placement="bottom-start"
|
||||
>
|
||||
<MemberListFilters appliedFilters={appliedFilters} handleUpdate={handleUpdate} memberType={memberType} />
|
||||
</CustomMenu>
|
||||
);
|
||||
});
|
||||
111
apps/web/core/components/project/dropdowns/filters/members.tsx
Normal file
111
apps/web/core/components/project/dropdowns/filters/members.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { sortBy } from "lodash-es";
|
||||
import { observer } from "mobx-react";
|
||||
// plane ui
|
||||
import { Avatar, Loader } from "@plane/ui";
|
||||
// components
|
||||
import { getFileURL } from "@plane/utils";
|
||||
import { FilterHeader, FilterOption } from "@/components/issues/issue-layouts/filters";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (val: string) => void;
|
||||
memberIds: string[] | undefined;
|
||||
searchQuery: string;
|
||||
};
|
||||
|
||||
export const FilterMembers: React.FC<Props> = observer((props: Props) => {
|
||||
const { appliedFilters, handleUpdate, memberIds, searchQuery } = props;
|
||||
// states
|
||||
const [itemsToRender, setItemsToRender] = useState(5);
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
|
||||
const sortedOptions = useMemo(() => {
|
||||
const filteredOptions = (memberIds || []).filter((memberId) =>
|
||||
getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return sortBy(filteredOptions, [
|
||||
(memberId) => !(appliedFilters ?? []).includes(memberId),
|
||||
(memberId) => memberId !== currentUser?.id,
|
||||
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchQuery]);
|
||||
|
||||
const handleViewToggle = () => {
|
||||
if (!sortedOptions) return;
|
||||
|
||||
if (itemsToRender === sortedOptions.length) setItemsToRender(5);
|
||||
else setItemsToRender(sortedOptions.length);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterHeader
|
||||
title={`Members${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||
isPreviewEnabled={previewEnabled}
|
||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||
/>
|
||||
{previewEnabled && (
|
||||
<div>
|
||||
{sortedOptions ? (
|
||||
sortedOptions.length > 0 ? (
|
||||
<>
|
||||
{sortedOptions.slice(0, itemsToRender).map((memberId) => {
|
||||
const member = getUserDetails(memberId);
|
||||
|
||||
if (!member) return null;
|
||||
return (
|
||||
<FilterOption
|
||||
key={`member-${member.id}`}
|
||||
isChecked={appliedFilters?.includes(member.id) ? true : false}
|
||||
onClick={() => handleUpdate(member.id)}
|
||||
icon={
|
||||
<Avatar
|
||||
name={member.display_name}
|
||||
src={getFileURL(member.avatar_url)}
|
||||
showTooltip={false}
|
||||
size="md"
|
||||
/>
|
||||
}
|
||||
title={currentUser?.id === member.id ? "You" : member?.display_name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{sortedOptions.length > 5 && (
|
||||
<button
|
||||
type="button"
|
||||
className="ml-8 text-xs font-medium text-custom-primary-100"
|
||||
onClick={handleViewToggle}
|
||||
>
|
||||
{itemsToRender === sortedOptions.length ? "View less" : "View all"}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-2">
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
105
apps/web/core/components/project/dropdowns/filters/root.tsx
Normal file
105
apps/web/core/components/project/dropdowns/filters/root.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Search } from "lucide-react";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// plane imports
|
||||
import type { TProjectDisplayFilters, TProjectFilters } from "@plane/types";
|
||||
// components
|
||||
import { FilterOption } from "@/components/issues/issue-layouts/filters";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// local imports
|
||||
import { FilterAccess } from "./access";
|
||||
import { FilterCreatedDate } from "./created-at";
|
||||
import { FilterLead } from "./lead";
|
||||
import { FilterMembers } from "./members";
|
||||
|
||||
type Props = {
|
||||
displayFilters: TProjectDisplayFilters;
|
||||
filters: TProjectFilters;
|
||||
handleFiltersUpdate: (key: keyof TProjectFilters, value: string | string[]) => void;
|
||||
handleDisplayFiltersUpdate: (updatedDisplayProperties: Partial<TProjectDisplayFilters>) => void;
|
||||
memberIds?: string[] | undefined;
|
||||
};
|
||||
|
||||
export const ProjectFiltersSelection: React.FC<Props> = observer((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} />
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
|
||||
placeholder="Search"
|
||||
value={filtersSearchQuery}
|
||||
onChange={(e) => setFiltersSearchQuery(e.target.value)}
|
||||
autoFocus={!isMobile}
|
||||
/>
|
||||
{filtersSearchQuery !== "" && (
|
||||
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>
|
||||
<CloseIcon className="text-custom-text-300" height={12} width={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full divide-y divide-custom-border-200 overflow-y-auto px-2.5 vertical-scrollbar scrollbar-sm">
|
||||
<div className="py-2">
|
||||
<FilterOption
|
||||
isChecked={!!displayFilters.my_projects}
|
||||
onClick={() =>
|
||||
handleDisplayFiltersUpdate({
|
||||
my_projects: !displayFilters.my_projects,
|
||||
})
|
||||
}
|
||||
title="My projects"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* access */}
|
||||
<div className="py-2">
|
||||
<FilterAccess
|
||||
appliedFilters={filters.access ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("access", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* lead */}
|
||||
<div className="py-2">
|
||||
<FilterLead
|
||||
appliedFilters={filters.lead ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("lead", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
memberIds={memberIds}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* members */}
|
||||
<div className="py-2">
|
||||
<FilterMembers
|
||||
appliedFilters={filters.members ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("members", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
memberIds={memberIds}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* created date */}
|
||||
<div className="py-2">
|
||||
<FilterCreatedDate
|
||||
appliedFilters={filters.created_at ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("created_at", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
93
apps/web/core/components/project/dropdowns/order-by.tsx
Normal file
93
apps/web/core/components/project/dropdowns/order-by.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowDownWideNarrow, Check } from "lucide-react";
|
||||
import { PROJECT_ORDER_BY_OPTIONS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import { ChevronDownIcon } from "@plane/propel/icons";
|
||||
import type { TProjectOrderByOptions } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
// types
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
onChange: (value: TProjectOrderByOptions) => void;
|
||||
value: TProjectOrderByOptions | undefined;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
const DISABLED_ORDERING_OPTIONS = ["sort_order"];
|
||||
|
||||
export const ProjectOrderByDropdown: React.FC<Props> = (props) => {
|
||||
const { onChange, value, isMobile = false } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const orderByDetails = PROJECT_ORDER_BY_OPTIONS.find((option) => value?.includes(option.key));
|
||||
|
||||
const isDescending = value?.[0] === "-";
|
||||
const isOrderingDisabled = !!value && DISABLED_ORDERING_OPTIONS.includes(value);
|
||||
|
||||
return (
|
||||
<CustomMenu
|
||||
className={`${isMobile ? "flex w-full justify-center" : ""}`}
|
||||
customButton={
|
||||
<>
|
||||
{isMobile ? (
|
||||
<div className="flex text-sm items-center gap-2 neutral-primary text-custom-text-200">
|
||||
<ArrowDownWideNarrow className="h-3 w-3" />
|
||||
{orderByDetails && t(orderByDetails?.i18n_label)}
|
||||
<ChevronDownIcon className="h-3 w-3" strokeWidth={2} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn(getButtonStyling("neutral-primary", "sm"), "px-2 text-custom-text-200")}>
|
||||
<ArrowDownWideNarrow className="h-3 w-3" />
|
||||
{orderByDetails && t(orderByDetails?.i18n_label)}
|
||||
<ChevronDownIcon className="h-3 w-3" strokeWidth={2} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
placement="bottom-end"
|
||||
closeOnSelect
|
||||
>
|
||||
{PROJECT_ORDER_BY_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
className="flex items-center justify-between gap-2"
|
||||
onClick={() => {
|
||||
if (isDescending)
|
||||
onChange(option.key == "sort_order" ? option.key : (`-${option.key}` as TProjectOrderByOptions));
|
||||
else onChange(option.key);
|
||||
}}
|
||||
>
|
||||
{option && t(option?.i18n_label)}
|
||||
{value?.includes(option.key) && <Check className="h-3 w-3" />}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
<hr className="my-2 border-custom-border-200" />
|
||||
<CustomMenu.MenuItem
|
||||
className="flex items-center justify-between gap-2"
|
||||
onClick={() => {
|
||||
if (isDescending) onChange(value.slice(1) as TProjectOrderByOptions);
|
||||
}}
|
||||
disabled={isOrderingDisabled}
|
||||
>
|
||||
Ascending
|
||||
{!isOrderingDisabled && !isDescending && <Check className="h-3 w-3" />}
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
className="flex items-center justify-between gap-2"
|
||||
onClick={() => {
|
||||
if (!isDescending) onChange(`-${value}` as TProjectOrderByOptions);
|
||||
}}
|
||||
disabled={isOrderingDisabled}
|
||||
>
|
||||
Descending
|
||||
{!isOrderingDisabled && isDescending && <Check className="h-3 w-3" />}
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user