[WEB-2273] Chore: header UI (#5467)
* chore: headers + common containers * fix: filters code splitting * fix: home header * fix: header changes * fix: uncommented filters * fix: used enums * fix: enum changes
This commit is contained in:
parent
747905a96d
commit
22656d0114
64 changed files with 1356 additions and 1119 deletions
|
|
@ -25,7 +25,7 @@ export const BreadcrumbLink: React.FC<Props> = (props) => {
|
|||
href={href}
|
||||
>
|
||||
{icon && (
|
||||
<div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-[1rem]">{icon}</div>
|
||||
<div className="flex h-5 w-5 items-center justify-start overflow-hidden !text-[1rem]">{icon}</div>
|
||||
)}
|
||||
{label && (
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { ReactNode } from "react";
|
||||
// components
|
||||
import { CustomRow } from "@plane/ui";
|
||||
import { SidebarHamburgerToggle } from "@/components/core";
|
||||
|
||||
export interface AppHeaderProps {
|
||||
|
|
@ -13,16 +14,14 @@ export const AppHeader = (props: AppHeaderProps) => {
|
|||
const { header, mobileHeader } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="z-[15]">
|
||||
<div className="z-10 flex w-full items-center border-b border-custom-border-200">
|
||||
<div className="block bg-custom-sidebar-background-100 py-4 pl-5 md:hidden">
|
||||
<SidebarHamburgerToggle />
|
||||
</div>
|
||||
<div className="w-full">{header}</div>
|
||||
<div className="z-[15]">
|
||||
<CustomRow className="h-[3.75rem] z-10 flex gap-2 w-full items-center border-b border-custom-border-200">
|
||||
<div className="block bg-custom-sidebar-background-100 md:hidden">
|
||||
<SidebarHamburgerToggle />
|
||||
</div>
|
||||
{mobileHeader && mobileHeader}
|
||||
</div>
|
||||
</>
|
||||
<div className="w-full">{header}</div>
|
||||
</CustomRow>
|
||||
{mobileHeader && mobileHeader}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
PanelLeft,
|
||||
MoveRight,
|
||||
} from "lucide-react";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { CustomHeader, CustomMenu, EHeaderVariant } from "@plane/ui";
|
||||
// components
|
||||
import { InboxIssueStatus } from "@/components/inbox";
|
||||
import { IssueUpdateStatus } from "@/components/issues";
|
||||
|
|
@ -80,7 +80,7 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
|
|||
if (!issue || !inboxIssue) return null;
|
||||
|
||||
return (
|
||||
<div className="h-12 relative flex border-custom-border-200 w-full items-center gap-2 px-4">
|
||||
<CustomHeader variant={EHeaderVariant.SECONDARY} className="flex">
|
||||
{isNotificationEmbed && (
|
||||
<button onClick={embedRemoveCurrentNotification}>
|
||||
<MoveRight className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||
|
|
@ -89,7 +89,7 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
|
|||
<PanelLeft
|
||||
onClick={() => setIsMobileSidebar(!isMobileSidebar)}
|
||||
className={cn(
|
||||
"w-4 h-4 flex-shrink-0 mr-2",
|
||||
"w-4 h-4 flex-shrink-0 mr-2 my-auto",
|
||||
isMobileSidebar ? "text-custom-primary-100" : "text-custom-text-200"
|
||||
)}
|
||||
/>
|
||||
|
|
@ -181,6 +181,6 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
|
|||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CustomHeader>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||
import {
|
||||
InboxIssueAppliedFiltersStatus,
|
||||
InboxIssueAppliedFiltersPriority,
|
||||
|
|
@ -17,7 +18,7 @@ export const InboxIssueAppliedFilters: FC = observer(() => {
|
|||
|
||||
if (getAppliedFiltersCount === 0) return <></>;
|
||||
return (
|
||||
<div className="p-3 py-2 relative flex flex-wrap items-center gap-1 border-b border-custom-border-300">
|
||||
<CustomHeader variant={EHeaderVariant.TERNARY} className="flex-wrap items-center gap-1 min-h-none">
|
||||
{/* status */}
|
||||
<InboxIssueAppliedFiltersStatus />
|
||||
{/* state */}
|
||||
|
|
@ -34,6 +35,6 @@ export const InboxIssueAppliedFilters: FC = observer(() => {
|
|||
<InboxIssueAppliedFiltersDate filterKey="created_at" label="Created date" />
|
||||
{/* updated_at */}
|
||||
<InboxIssueAppliedFiltersDate filterKey="updated_at" label="Updated date" />
|
||||
</div>
|
||||
</CustomHeader>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,33 @@
|
|||
import { FC } from "react";
|
||||
import { ListFilter } from "lucide-react";
|
||||
import { ChevronDown, ListFilter } from "lucide-react";
|
||||
// components
|
||||
import { cn } from "@plane/editor";
|
||||
import { getButtonStyling } from "@plane/ui";
|
||||
import { InboxIssueFilterSelection, InboxIssueOrderByDropdown } from "@/components/inbox/inbox-filter";
|
||||
import { FiltersDropdown } from "@/components/issues";
|
||||
|
||||
const smallButton = <ListFilter className="h-3 " />;
|
||||
const largeButton = (
|
||||
<div className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}>
|
||||
<ListFilter className="h-3 " />
|
||||
<span className="hidden lg:flex">Filters</span>
|
||||
|
||||
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
export const FiltersRoot: FC = () => (
|
||||
<div className="relative flex items-center gap-2">
|
||||
<div>
|
||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown
|
||||
menuButton={
|
||||
<>
|
||||
<div className="hidden 2xl:flex">{largeButton}</div>
|
||||
<div className="flex 2xl:hidden">{smallButton}</div>
|
||||
</>
|
||||
}
|
||||
title=""
|
||||
placement="bottom-end"
|
||||
>
|
||||
<InboxIssueFilterSelection />
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,19 +16,27 @@ export const InboxIssueOrderByDropdown: FC = observer(() => {
|
|||
const { inboxSorting, handleInboxIssueSorting } = useProjectInbox();
|
||||
const orderByDetails =
|
||||
INBOX_ISSUE_ORDER_BY_OPTIONS.find((option) => inboxSorting?.order_by?.includes(option.key)) || undefined;
|
||||
const smallButton =
|
||||
inboxSorting?.sort_by === "asc" ? <ArrowUpWideNarrow className="h-3 " /> : <ArrowDownWideNarrow className="h-3 " />;
|
||||
const largeButton = (
|
||||
<div className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}>
|
||||
{inboxSorting?.sort_by === "asc" ? (
|
||||
<ArrowUpWideNarrow className="h-3 " />
|
||||
) : (
|
||||
<ArrowDownWideNarrow className="h-3 " />
|
||||
)}
|
||||
{orderByDetails?.label || "Order By"}
|
||||
|
||||
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div className={cn(getButtonStyling("neutral-primary", "sm"), "px-2 text-custom-text-300")}>
|
||||
{inboxSorting?.sort_by === "asc" ? (
|
||||
<ArrowUpWideNarrow className="h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDownWideNarrow className="h-3 w-3" />
|
||||
)}
|
||||
{orderByDetails?.label || "Order By"}
|
||||
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
||||
</div>
|
||||
<>
|
||||
<div className="hidden 2xl:flex">{largeButton}</div>
|
||||
<div className="flex 2xl:hidden">{smallButton}</div>
|
||||
</>
|
||||
}
|
||||
placement="bottom-end"
|
||||
maxHeight="lg"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { FC, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TInboxIssueCurrentTab } from "@plane/types";
|
||||
import { Loader } from "@plane/ui";
|
||||
import { CustomHeader, Loader, EHeaderVariant } from "@plane/ui";
|
||||
// components
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { FiltersRoot, InboxIssueAppliedFilters, InboxIssueList } from "@/components/inbox";
|
||||
|
|
@ -76,7 +76,7 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
|
|||
return (
|
||||
<div className="bg-custom-background-100 flex-shrink-0 w-full h-full border-r border-custom-border-300 ">
|
||||
<div className="relative w-full h-full flex flex-col overflow-hidden">
|
||||
<div className="border-b border-custom-border-300 flex-shrink-0 w-full h-[50px] relative flex items-center gap-2 whitespace-nowrap px-3">
|
||||
<CustomHeader variant={EHeaderVariant.SECONDARY} className="flex">
|
||||
{tabNavigationOptions.map((option) => (
|
||||
<div
|
||||
key={option?.key}
|
||||
|
|
@ -105,11 +105,10 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
|
|||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="ml-auto">
|
||||
<div className="m-auto mr-0">
|
||||
<FiltersRoot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</CustomHeader>
|
||||
<InboxIssueAppliedFilters />
|
||||
|
||||
{loader != undefined && loader === "filter-loading" && !inboxIssuePaginationInfo?.next_page_results ? (
|
||||
|
|
|
|||
143
web/core/components/issues/filters.tsx
Normal file
143
web/core/components/issues/filters.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { TProject } from "ee/types";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// constants
|
||||
import {
|
||||
EIssueFilterType,
|
||||
EIssuesStoreType,
|
||||
EIssueLayoutTypes,
|
||||
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
|
||||
} from "@/constants/issue";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useLabel, useProjectState, useMember, useIssues } from "@/hooks/store";
|
||||
import { ProjectAnalyticsModal } from "../analytics";
|
||||
|
||||
type Props = {
|
||||
currentProjectDetails: TProject | undefined;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
canUserCreateIssue: boolean | undefined;
|
||||
};
|
||||
const HeaderFilters = ({ currentProjectDetails, projectId, workspaceSlug, canUserCreateIssue }: Props) => {
|
||||
// states
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
// store hooks
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// this validation is majorly for the filter start_date, target_date custom
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues });
|
||||
},
|
||||
[workspaceSlug, projectId, issueFilters, updateFilters]
|
||||
);
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property);
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
projectDetails={currentProjectDetails ?? undefined}
|
||||
/>
|
||||
<LayoutSelection
|
||||
layouts={[
|
||||
EIssueLayoutTypes.LIST,
|
||||
EIssueLayoutTypes.KANBAN,
|
||||
EIssueLayoutTypes.CALENDAR,
|
||||
EIssueLayoutTypes.SPREADSHEET,
|
||||
EIssueLayoutTypes.GANTT,
|
||||
]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
{canUserCreateIssue ? (
|
||||
<Button className="hidden md:block" onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderFilters;
|
||||
|
|
@ -59,7 +59,7 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
|||
!disableEditing && (alwaysAllowEditing || (currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER));
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100 truncate">
|
||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100 truncate my-auto">
|
||||
{Object.entries(appliedFilters).map(([key, value]) => {
|
||||
const filterKey = key as keyof IIssueFilterOptions;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
// hooks
|
||||
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||
import { AppliedFiltersList, SaveFilterView } from "@/components/issues";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||
import { useIssues, useLabel, useProjectState } from "@/hooks/store";
|
||||
|
|
@ -76,15 +77,16 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
|
|||
if (Object.keys(appliedFilters).length === 0 || !workspaceSlug || !projectId || !cycleId) return null;
|
||||
|
||||
return (
|
||||
<div className="flex justify-between p-4 gap-2.5">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
/>
|
||||
|
||||
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||
<CustomHeader.LeftItem>
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
/>
|
||||
</CustomHeader.LeftItem>
|
||||
<SaveFilterView
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
|
|
@ -94,6 +96,6 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
|
|||
display_properties: issueFilters?.displayProperties,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CustomHeader>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import isEmpty from "lodash/isEmpty";
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// types
|
||||
import { cn } from "@plane/editor";
|
||||
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
|
||||
//ui
|
||||
// components
|
||||
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||
import { AppliedFiltersList } from "@/components/issues";
|
||||
import { UpdateViewComponent } from "@/components/views/update-view-component";
|
||||
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
|
||||
|
|
@ -133,7 +133,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
|||
if (areAppliedFiltersEmpty && areFiltersEqual) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||
<CreateUpdateWorkspaceViewModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
|
|
@ -144,31 +144,28 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
|||
...viewFilters,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn("flex items-start justify-between gap-4 p-4", {
|
||||
"justify-end": areAppliedFiltersEmpty,
|
||||
})}
|
||||
>
|
||||
<AppliedFiltersList
|
||||
labels={workspaceLabels ?? undefined}
|
||||
appliedFilters={appliedFilters ?? {}}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
disableEditing={isLocked}
|
||||
alwaysAllowEditing
|
||||
/>
|
||||
|
||||
{!isDefaultView && (
|
||||
<UpdateViewComponent
|
||||
isLocked={isLocked}
|
||||
areFiltersEqual={!!areFiltersEqual}
|
||||
isOwner={isOwner}
|
||||
isAuthorizedUser={isAuthorizedUser}
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
handleUpdateView={handleUpdateView}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
<AppliedFiltersList
|
||||
labels={workspaceLabels ?? undefined}
|
||||
appliedFilters={appliedFilters ?? {}}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
disableEditing={isLocked}
|
||||
alwaysAllowEditing
|
||||
/>
|
||||
|
||||
{!isDefaultView ? (
|
||||
<UpdateViewComponent
|
||||
isLocked={isLocked}
|
||||
areFiltersEqual={!!areFiltersEqual}
|
||||
isOwner={isOwner}
|
||||
isAuthorizedUser={isAuthorizedUser}
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
handleUpdateView={handleUpdateView}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</CustomHeader>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
// hooks
|
||||
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||
import { AppliedFiltersList, SaveFilterView } from "@/components/issues";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||
import { useIssues, useLabel, useProjectState } from "@/hooks/store";
|
||||
|
|
@ -75,15 +76,16 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
|
|||
if (!workspaceSlug || !projectId || !moduleId || Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex justify-between p-4 gap-2.5">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
/>
|
||||
|
||||
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||
<CustomHeader.LeftItem>
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
/>
|
||||
</CustomHeader.LeftItem>
|
||||
<SaveFilterView
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
|
|
@ -93,6 +95,6 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
|
|||
display_properties: issueFilters?.displayProperties,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CustomHeader>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useParams } from "next/navigation";
|
|||
import { IIssueFilterOptions } from "@plane/types";
|
||||
// hooks
|
||||
// components
|
||||
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||
import { AppliedFiltersList, SaveFilterView } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||
|
|
@ -67,7 +68,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
|
|||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex justify-between p-4 gap-2.5">
|
||||
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
|
|
@ -86,6 +87,6 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CustomHeader>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useParams } from "next/navigation";
|
|||
// types
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
// components
|
||||
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||
import { AppliedFiltersList } from "@/components/issues";
|
||||
import { CreateUpdateProjectViewModal } from "@/components/views";
|
||||
import { UpdateViewComponent } from "@/components/views/update-view-component";
|
||||
|
|
@ -113,7 +114,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||
const isOwner = viewDetails?.owned_by === data?.id;
|
||||
|
||||
return (
|
||||
<div className="flex justify-between gap-4 p-4">
|
||||
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||
<CreateUpdateProjectViewModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
|
|
@ -127,7 +128,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||
...viewFilters,
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<CustomHeader.LeftItem>
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters ?? {}}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
|
|
@ -136,7 +137,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||
states={projectStates}
|
||||
disableEditing={isLocked}
|
||||
/>
|
||||
</div>
|
||||
</CustomHeader.LeftItem>
|
||||
<UpdateViewComponent
|
||||
isLocked={isLocked}
|
||||
areFiltersEqual={!!areFiltersEqual}
|
||||
|
|
@ -145,6 +146,6 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||
setIsModalOpen={setIsModalOpen}
|
||||
handleUpdateView={handleUpdateView}
|
||||
/>
|
||||
</div>
|
||||
</CustomHeader>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { X } from "lucide-react";
|
||||
import { TModuleDisplayFilters, TModuleFilters } from "@plane/types";
|
||||
// components
|
||||
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||
import { AppliedDateFilters, AppliedMembersFilters, AppliedStatusFilters } from "@/components/modules";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper";
|
||||
|
|
@ -36,91 +37,93 @@ export const ModuleAppliedFiltersList: React.FC<Props> = (props) => {
|
|||
const isEditingAllowed = alwaysAllowEditing;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
|
||||
{Object.entries(appliedFilters).map(([key, value]) => {
|
||||
const filterKey = key as keyof TModuleFilters;
|
||||
<CustomHeader variant={EHeaderVariant.TERNARY} className="flex flex-wrap gap-2">
|
||||
<CustomHeader.LeftItem>
|
||||
{Object.entries(appliedFilters).map(([key, value]) => {
|
||||
const filterKey = key as keyof TModuleFilters;
|
||||
|
||||
if (!value) return;
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
if (!value) return;
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
|
||||
return (
|
||||
return (
|
||||
<div
|
||||
key={filterKey}
|
||||
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 capitalize"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span>
|
||||
{filterKey === "status" && (
|
||||
<AppliedStatusFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("status", val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{DATE_FILTERS.includes(filterKey) && (
|
||||
<AppliedDateFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{MEMBERS_FILTERS.includes(filterKey) && (
|
||||
<AppliedMembersFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemoveFilter(filterKey, null)}
|
||||
>
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!isArchived && isFavoriteFilterApplied && (
|
||||
<div
|
||||
key={filterKey}
|
||||
key="module_display_filters"
|
||||
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span>
|
||||
{filterKey === "status" && (
|
||||
<AppliedStatusFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("status", val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{DATE_FILTERS.includes(filterKey) && (
|
||||
<AppliedDateFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{MEMBERS_FILTERS.includes(filterKey) && (
|
||||
<AppliedMembersFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemoveFilter(filterKey, null)}
|
||||
>
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
<span className="text-xs text-custom-text-300">Modules</span>
|
||||
<div className="flex items-center gap-1 rounded p-1 text-xs bg-custom-background-80">
|
||||
Favorite
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() =>
|
||||
handleDisplayFiltersUpdate &&
|
||||
handleDisplayFiltersUpdate({
|
||||
favorites: !isFavoriteFilterApplied,
|
||||
})
|
||||
}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!isArchived && isFavoriteFilterApplied && (
|
||||
<div
|
||||
key="module_display_filters"
|
||||
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-xs text-custom-text-300">Modules</span>
|
||||
<div className="flex items-center gap-1 rounded p-1 text-xs bg-custom-background-80">
|
||||
Favorite
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() =>
|
||||
handleDisplayFiltersUpdate &&
|
||||
handleDisplayFiltersUpdate({
|
||||
favorites: !isFavoriteFilterApplied,
|
||||
})
|
||||
}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearAllFilters}
|
||||
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
|
||||
>
|
||||
Clear all
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearAllFilters}
|
||||
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
|
||||
>
|
||||
Clear all
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</CustomHeader.LeftItem>
|
||||
</CustomHeader>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-grow items-center justify-end gap-3">
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{is_locked && <LockedComponent />}
|
||||
{archived_at && (
|
||||
<div className="flex h-7 items-center gap-2 rounded-full bg-blue-500/20 px-3 py-0.5 text-xs font-medium text-blue-500">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { EditorReadOnlyRefApi, EditorRefApi, IMarking } from "@plane/editor";
|
||||
// components
|
||||
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||
import { PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages";
|
||||
// hooks
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
|
|
@ -42,8 +43,8 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center border-b border-custom-border-200 px-2 py-1">
|
||||
<div className="flex-shrink-0">
|
||||
<CustomHeader variant={EHeaderVariant.SECONDARY} className="flex justify-between">
|
||||
<div className="flex-shrink-0 my-auto">
|
||||
<PageSummaryPopover
|
||||
editorRef={isContentEditable ? editorRef.current : readOnlyEditorRef.current}
|
||||
isFullWidth={isFullWidth}
|
||||
|
|
@ -59,12 +60,12 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
|
|||
page={page}
|
||||
readOnlyEditorRef={readOnlyEditorRef}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-b border-custom-border-200 py-1 px-2">
|
||||
</CustomHeader>
|
||||
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||
{(editorReady || readOnlyEditorReady) && isContentEditable && editorRef.current && (
|
||||
<PageToolbar editorRef={editorRef?.current} />
|
||||
)}
|
||||
</div>
|
||||
</CustomHeader>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { EditorReadOnlyRefApi, EditorRefApi, IMarking } from "@plane/editor";
|
||||
// components
|
||||
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||
import { PageEditorMobileHeaderRoot, PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
|
@ -44,13 +45,8 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="hidden md:flex items-center border-b border-custom-border-200 px-3 py-2 md:px-5">
|
||||
<div
|
||||
className={cn("flex-shrink-0", {
|
||||
"w-40 lg:w-56": !isFullWidth,
|
||||
"w-[5%]": isFullWidth,
|
||||
})}
|
||||
>
|
||||
<CustomHeader variant={EHeaderVariant.SECONDARY} className="hidden md:flex justify-between">
|
||||
<div className={cn("flex-shrink-0 my-auto")}>
|
||||
<PageSummaryPopover
|
||||
editorRef={isContentEditable ? editorRef.current : readOnlyEditorRef.current}
|
||||
isFullWidth={isFullWidth}
|
||||
|
|
@ -69,7 +65,7 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
|||
page={page}
|
||||
readOnlyEditorRef={readOnlyEditorRef}
|
||||
/>
|
||||
</div>
|
||||
</CustomHeader>
|
||||
<div className="md:hidden">
|
||||
<PageEditorMobileHeaderRoot
|
||||
editorRef={editorRef}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { observer } from "mobx-react";
|
|||
import { ListFilter } from "lucide-react";
|
||||
import { TPageFilterProps, TPageNavigationTabs } from "@plane/types";
|
||||
// components
|
||||
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||
import { FiltersDropdown } from "@/components/issues";
|
||||
import {
|
||||
PageAppliedFiltersList,
|
||||
|
|
@ -49,9 +50,11 @@ export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-shrink-0 h-[50px] w-full border-b border-custom-border-200 px-6 relative flex items-center gap-4 justify-between">
|
||||
<PageTabNavigation workspaceSlug={workspaceSlug} projectId={projectId} pageType={pageType} />
|
||||
<div className="h-full flex items-center gap-2 self-end">
|
||||
<CustomHeader variant={EHeaderVariant.SECONDARY} className="flex">
|
||||
<CustomHeader.LeftItem>
|
||||
<PageTabNavigation workspaceSlug={workspaceSlug} projectId={projectId} pageType={pageType} />
|
||||
</CustomHeader.LeftItem>
|
||||
<CustomHeader.RightItem>
|
||||
<PageSearchInput
|
||||
searchQuery={filters.searchQuery}
|
||||
updateSearchQuery={(val) => updateFilters("searchQuery", val)}
|
||||
|
|
@ -76,17 +79,17 @@ export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
|
|||
memberIds={workspaceMemberIds ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</CustomHeader.RightItem>
|
||||
</CustomHeader>
|
||||
{calculateTotalFilters(filters?.filters ?? {}) !== 0 && (
|
||||
<div className="border-b border-custom-border-200 px-5 py-3">
|
||||
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||
<PageAppliedFiltersList
|
||||
appliedFilters={filters.filters ?? {}}
|
||||
handleClearAllFilters={clearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
alwaysAllowEditing
|
||||
/>
|
||||
</div>
|
||||
</CustomHeader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -36,11 +36,11 @@ export const PageSearchInput: FC<Props> = (props) => {
|
|||
}, [searchQuery]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex">
|
||||
{!isSearchOpen && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex-shrink-0 hover:bg-custom-background-80 rounded text-custom-text-400 relative flex justify-center items-center w-6 h-6"
|
||||
className="flex-shrink-0 hover:bg-custom-background-80 rounded text-custom-text-400 relative flex justify-center items-center w-6 h-6 my-auto"
|
||||
onClick={() => {
|
||||
setIsSearchOpen(true);
|
||||
inputRef.current?.focus();
|
||||
|
|
@ -49,10 +49,9 @@ export const PageSearchInput: FC<Props> = (props) => {
|
|||
<Search className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
|
||||
"flex items-center justify-start rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
|
||||
{
|
||||
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
|
||||
}
|
||||
|
|
@ -80,6 +79,6 @@ export const PageSearchInput: FC<Props> = (props) => {
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export const ProjectAppliedFiltersList: React.FC<Props> = (props) => {
|
|||
const isEditingAllowed = alwaysAllowEditing;
|
||||
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-1.5">
|
||||
<div className="flex items-start justify-between gap-1.5 my-auto w-full">
|
||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
|
||||
{/* Applied filters */}
|
||||
{Object.entries(appliedFilters ?? {}).map(([key, value]) => {
|
||||
|
|
@ -132,7 +132,7 @@ export const ProjectAppliedFiltersList: React.FC<Props> = (props) => {
|
|||
</p>
|
||||
}
|
||||
>
|
||||
<span className="bg-custom-background-80 rounded-full text-sm font-medium py-1 px-2.5">
|
||||
<span className="bg-custom-background-80 rounded-full text-sm font-medium py-1 px-2.5 my-auto">
|
||||
{filteredProjects}/{totalProjects}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
|
|
|||
93
web/core/components/project/filters.tsx
Normal file
93
web/core/components/project/filters.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { useCallback } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ListFilter } from "lucide-react";
|
||||
// types
|
||||
import { cn } from "@plane/editor";
|
||||
import { TProjectFilters } from "@plane/types";
|
||||
// components
|
||||
import { FiltersDropdown } from "@/components/issues";
|
||||
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useMember, useProjectFilter } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
filterMenuButton?: React.ReactNode;
|
||||
classname?: string;
|
||||
filterClassname?: string;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
const HeaderFilters = ({ filterMenuButton, isMobile, classname = "", filterClassname = "" }: Props) => {
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
const {
|
||||
currentWorkspaceDisplayFilters: displayFilters,
|
||||
currentWorkspaceFilters: filters,
|
||||
updateFilters,
|
||||
updateDisplayFilters,
|
||||
} = useProjectFilter();
|
||||
const {
|
||||
workspace: { workspaceMemberIds },
|
||||
} = useMember();
|
||||
const handleFilters = useCallback(
|
||||
(key: keyof TProjectFilters, value: string | string[]) => {
|
||||
if (!workspaceSlug) return;
|
||||
let newValues = filters?.[key] ?? [];
|
||||
if (Array.isArray(value)) {
|
||||
if (key === "created_at" && newValues.find((v) => v.includes("custom"))) newValues = [];
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
} else {
|
||||
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else {
|
||||
if (key === "created_at") newValues = [value];
|
||||
else newValues.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug.toString(), { [key]: newValues });
|
||||
},
|
||||
[filters, updateFilters, workspaceSlug]
|
||||
);
|
||||
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className={cn("flex gap-3", classname)}>
|
||||
<ProjectOrderByDropdown
|
||||
value={displayFilters?.order_by}
|
||||
onChange={(val) => {
|
||||
if (!workspaceSlug || val === displayFilters?.order_by) return;
|
||||
updateDisplayFilters(workspaceSlug.toString(), {
|
||||
order_by: val,
|
||||
});
|
||||
}}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<div className={cn(filterClassname)}>
|
||||
<FiltersDropdown
|
||||
icon={<ListFilter className="h-3 w-3" />}
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
menuButton={filterMenuButton || null}
|
||||
>
|
||||
<ProjectFiltersSelection
|
||||
displayFilters={displayFilters ?? {}}
|
||||
filters={filters ?? {}}
|
||||
handleFiltersUpdate={handleFilters}
|
||||
handleDisplayFiltersUpdate={(val) => {
|
||||
if (!workspaceSlug) return;
|
||||
updateDisplayFilters(workspaceSlug.toString(), val);
|
||||
}}
|
||||
memberIds={workspaceMemberIds ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default HeaderFilters;
|
||||
|
|
@ -1,29 +1,23 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { Search, Briefcase, X, ListFilter } from "lucide-react";
|
||||
// types
|
||||
import { TProjectFilters } from "@plane/types";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Search, Briefcase, X } from "lucide-react";
|
||||
// ui
|
||||
import { Breadcrumbs, Button } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, CustomHeader } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { FiltersDropdown } from "@/components/issues";
|
||||
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useCommandPalette, useEventTracker, useMember, useProjectFilter, useUser } from "@/hooks/store";
|
||||
import { useCommandPalette, useEventTracker, useProjectFilter, useUser } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import HeaderFilters from "./filters";
|
||||
|
||||
export const ProjectsBaseHeader = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// states
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
// refs
|
||||
|
|
@ -36,17 +30,8 @@ export const ProjectsBaseHeader = observer(() => {
|
|||
} = useUser();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
currentWorkspaceDisplayFilters: displayFilters,
|
||||
currentWorkspaceFilters: filters,
|
||||
updateFilters,
|
||||
updateDisplayFilters,
|
||||
searchQuery,
|
||||
updateSearchQuery,
|
||||
} = useProjectFilter();
|
||||
const {
|
||||
workspace: { workspaceMemberIds },
|
||||
} = useMember();
|
||||
const { searchQuery, updateSearchQuery } = useProjectFilter();
|
||||
|
||||
// outside click detector hook
|
||||
useOutsideClickDetector(inputRef, () => {
|
||||
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||
|
|
@ -55,29 +40,6 @@ export const ProjectsBaseHeader = observer(() => {
|
|||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
const isArchived = pathname.includes("/archives");
|
||||
|
||||
const handleFilters = useCallback(
|
||||
(key: keyof TProjectFilters, value: string | string[]) => {
|
||||
if (!workspaceSlug) return;
|
||||
let newValues = filters?.[key] ?? [];
|
||||
if (Array.isArray(value)) {
|
||||
if (key === "created_at" && newValues.find((v) => v.includes("custom"))) newValues = [];
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
} else {
|
||||
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else {
|
||||
if (key === "created_at") newValues = [value];
|
||||
else newValues.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug.toString(), { [key]: newValues });
|
||||
},
|
||||
[filters, updateFilters, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Escape") {
|
||||
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
|
||||
|
|
@ -89,22 +51,18 @@ export const ProjectsBaseHeader = observer(() => {
|
|||
if (searchQuery.trim() !== "") setIsSearchOpen(true);
|
||||
}, [searchQuery]);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<div>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={<BreadcrumbLink label="Projects" icon={<Briefcase className="h-4 w-4 text-custom-text-300" />} />}
|
||||
/>
|
||||
{isArchived && <Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label="Archived" />} />}
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex items-center justify-end gap-3">
|
||||
<CustomHeader>
|
||||
<CustomHeader.LeftItem>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={<BreadcrumbLink label="Projects" icon={<Briefcase className="h-4 w-4 text-custom-text-300" />} />}
|
||||
/>
|
||||
{isArchived && <Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label="Archived" />} />}
|
||||
</Breadcrumbs>
|
||||
</CustomHeader.LeftItem>
|
||||
<CustomHeader.RightItem>
|
||||
<div className="flex items-center">
|
||||
{!isSearchOpen && (
|
||||
<button
|
||||
|
|
@ -149,35 +107,11 @@ export const ProjectsBaseHeader = observer(() => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden md:flex gap-3">
|
||||
<ProjectOrderByDropdown
|
||||
value={displayFilters?.order_by}
|
||||
onChange={(val) => {
|
||||
if (!workspaceSlug || val === displayFilters?.order_by) return;
|
||||
updateDisplayFilters(workspaceSlug.toString(), {
|
||||
order_by: val,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FiltersDropdown
|
||||
icon={<ListFilter className="h-3 w-3" />}
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<ProjectFiltersSelection
|
||||
displayFilters={displayFilters ?? {}}
|
||||
filters={filters ?? {}}
|
||||
handleFiltersUpdate={handleFilters}
|
||||
handleDisplayFiltersUpdate={(val) => {
|
||||
if (!workspaceSlug) return;
|
||||
updateDisplayFilters(workspaceSlug.toString(), val);
|
||||
}}
|
||||
memberIds={workspaceMemberIds ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
|
||||
<div className="hidden md:flex">
|
||||
<HeaderFilters />
|
||||
</div>
|
||||
{isAuthorizedUser && !isArchived && (
|
||||
{isAuthorizedUser && !isArchived ? (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
|
|
@ -188,8 +122,10 @@ export const ProjectsBaseHeader = observer(() => {
|
|||
>
|
||||
<span className="hidden sm:inline-block">Add</span> Project
|
||||
</Button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CustomHeader.RightItem>
|
||||
</CustomHeader>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { observer } from "mobx-react";
|
|||
import { useParams, usePathname } from "next/navigation";
|
||||
import { TProjectAppliedDisplayFilterKeys, TProjectFilters } from "@plane/types";
|
||||
// components
|
||||
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectAppliedFiltersList, ProjectCardList } from "@/components/project";
|
||||
// helpers
|
||||
|
|
@ -32,9 +33,8 @@ const Root = observer(() => {
|
|||
|
||||
const isArchived = pathname.includes("/archives");
|
||||
|
||||
const allowedDisplayFilters = currentWorkspaceAppliedDisplayFilters?.filter(
|
||||
(filter) => filter !== "archived_projects"
|
||||
) ?? [];
|
||||
const allowedDisplayFilters =
|
||||
currentWorkspaceAppliedDisplayFilters?.filter((filter) => filter !== "archived_projects") ?? [];
|
||||
|
||||
const handleRemoveFilter = useCallback(
|
||||
(key: keyof TProjectFilters, value: string | null) => {
|
||||
|
|
@ -65,17 +65,17 @@ const Root = observer(() => {
|
|||
}, [clearAllFilters, clearAllAppliedDisplayFilters, workspaceSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
isArchived ? updateDisplayFilters(workspaceSlug.toString(), { archived_projects: true }) :
|
||||
updateDisplayFilters(workspaceSlug.toString(), { archived_projects: false });
|
||||
isArchived
|
||||
? updateDisplayFilters(workspaceSlug.toString(), { archived_projects: true })
|
||||
: updateDisplayFilters(workspaceSlug.toString(), { archived_projects: false });
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="flex h-full w-full flex-col">
|
||||
{(calculateTotalFilters(currentWorkspaceFilters ?? {}) !== 0 ||
|
||||
(allowedDisplayFilters.length>0)) && (
|
||||
<div className="border-b border-custom-border-200 px-5 py-3">
|
||||
{(calculateTotalFilters(currentWorkspaceFilters ?? {}) !== 0 || allowedDisplayFilters.length > 0) && (
|
||||
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||
<ProjectAppliedFiltersList
|
||||
appliedFilters={currentWorkspaceFilters ?? {}}
|
||||
appliedDisplayFilters={allowedDisplayFilters}
|
||||
|
|
@ -86,7 +86,7 @@ const Root = observer(() => {
|
|||
totalProjects={totalProjectIds?.length ?? 0}
|
||||
alwaysAllowEditing
|
||||
/>
|
||||
</div>
|
||||
</CustomHeader>
|
||||
)}
|
||||
<ProjectCardList />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export const SpreadsheetIssueRowLoader = (props: { columnCount: number }) => (
|
|||
);
|
||||
|
||||
export const SpreadsheetLayoutLoader = () => (
|
||||
<div className="horizontal-scroll-enable h-full w-full ">
|
||||
<div className="horizontal-scroll-enable h-full w-full overflow-y-auto ">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { FC } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
// constants
|
||||
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||
import { ENotificationFilterType, FILTER_TYPE_OPTIONS } from "@/constants/notification";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store";
|
||||
|
|
@ -35,30 +36,32 @@ export const AppliedFilters: FC<TAppliedFilters> = observer((props) => {
|
|||
|
||||
if (!isFiltersEnabled || !workspaceSlug) return <></>;
|
||||
return (
|
||||
<div className="border-b border-custom-border-200 px-5 py-3 flex items-center flex-wrap gap-2">
|
||||
{FILTER_TYPE_OPTIONS.map((filter) => {
|
||||
const isSelected = filters?.type?.[filter?.value] || false;
|
||||
if (!isSelected) return <></>;
|
||||
return (
|
||||
<div
|
||||
key={filter.value}
|
||||
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all border border-custom-border-200 rounded-sm text-xs"
|
||||
onClick={() => handleFilterTypeChange(filter?.value, !isSelected)}
|
||||
>
|
||||
<div className="whitespace-nowrap text-custom-text-200">{filter.label}</div>
|
||||
<div className="w-4 h-4 flex justify-center items-center transition-all rounded-sm bg-custom-background-90 hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100">
|
||||
<X className="h-3 w-3" />
|
||||
<CustomHeader variant={EHeaderVariant.TERNARY}>
|
||||
<CustomHeader.LeftItem>
|
||||
{FILTER_TYPE_OPTIONS.map((filter) => {
|
||||
const isSelected = filters?.type?.[filter?.value] || false;
|
||||
if (!isSelected) return <></>;
|
||||
return (
|
||||
<div
|
||||
key={filter.value}
|
||||
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all border border-custom-border-200 rounded-sm text-xs"
|
||||
onClick={() => handleFilterTypeChange(filter?.value, !isSelected)}
|
||||
>
|
||||
<div className="whitespace-nowrap text-custom-text-200">{filter.label}</div>
|
||||
<div className="w-4 h-4 flex justify-center items-center transition-all rounded-sm bg-custom-background-90 hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100">
|
||||
<X className="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all border border-custom-border-200 rounded-sm text-xs bg-custom-background-90 hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={handleClearFilters}
|
||||
>
|
||||
<div className="whitespace-nowrap">Clear all</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all border border-custom-border-200 rounded-sm text-xs bg-custom-background-90 hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={handleClearFilters}
|
||||
>
|
||||
<div className="whitespace-nowrap">Clear all</div>
|
||||
</div>
|
||||
</CustomHeader.LeftItem>
|
||||
</CustomHeader>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Inbox } from "lucide-react";
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
import { Breadcrumbs, CustomHeader } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { SidebarHamburgerToggle } from "@/components/core";
|
||||
|
|
@ -18,8 +18,8 @@ export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observe
|
|||
|
||||
if (!workspaceSlug) return <></>;
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<CustomHeader className="my-auto">
|
||||
<CustomHeader.LeftItem>
|
||||
<div className="block bg-custom-sidebar-background-100 md:hidden">
|
||||
<SidebarHamburgerToggle />
|
||||
</div>
|
||||
|
|
@ -31,9 +31,10 @@ export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observe
|
|||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
|
||||
<NotificationSidebarHeaderOptions workspaceSlug={workspaceSlug} />
|
||||
</div>
|
||||
</CustomHeader.LeftItem>
|
||||
<CustomHeader.RightItem>
|
||||
<NotificationSidebarHeaderOptions workspaceSlug={workspaceSlug} />
|
||||
</CustomHeader.RightItem>
|
||||
</CustomHeader>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { FC } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { CustomHeader, CustomRow, EHeaderVariant, ERowVariant } from "@plane/ui";
|
||||
import { CountChip } from "@/components/common";
|
||||
import {
|
||||
NotificationsLoader,
|
||||
|
|
@ -46,15 +47,15 @@ export const NotificationsSidebar: FC = observer(() => {
|
|||
)}
|
||||
>
|
||||
<div className="relative w-full h-full overflow-hidden flex flex-col">
|
||||
<div className="border-b border-custom-border-200">
|
||||
<CustomRow className="h-[3.75rem] border-b border-custom-border-200 flex">
|
||||
<NotificationSidebarHeader workspaceSlug={workspaceSlug.toString()} />
|
||||
</div>
|
||||
</CustomRow>
|
||||
|
||||
<div className="flex-shrink-0 w-full h-[46px] border-b border-custom-border-200 px-5 relative flex items-center gap-2">
|
||||
<CustomHeader variant={EHeaderVariant.SECONDARY} className="flex">
|
||||
{NOTIFICATION_TABS.map((tab) => (
|
||||
<div
|
||||
key={tab.value}
|
||||
className="h-full px-3 relative flex flex-col cursor-pointer"
|
||||
className="h-full px-3 relative cursor-pointer"
|
||||
onClick={() => currentNotificationTab != tab.value && setCurrentNotificationTab(tab.value)}
|
||||
>
|
||||
<div
|
||||
|
|
@ -75,12 +76,10 @@ export const NotificationsSidebar: FC = observer(() => {
|
|||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CustomHeader>
|
||||
|
||||
{/* applied filters */}
|
||||
<div className="flex-shrink-0">
|
||||
<AppliedFilters workspaceSlug={workspaceSlug.toString()} />
|
||||
</div>
|
||||
<AppliedFilters workspaceSlug={workspaceSlug.toString()} />
|
||||
|
||||
{/* rendering notifications */}
|
||||
{loader === "init-loader" ? (
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Plus } from "lucide-react";
|
|||
// types
|
||||
import { TStaticViewTypes } from "@plane/types";
|
||||
// components
|
||||
import { CustomHeader, EHeaderVariant } from "@plane/ui";
|
||||
import {
|
||||
CreateUpdateWorkspaceViewModal,
|
||||
DefaultWorkspaceViewQuickActions,
|
||||
|
|
@ -103,30 +104,30 @@ export const GlobalViewsHeader: React.FC = observer(() => {
|
|||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomHeader variant={EHeaderVariant.SECONDARY} className="flex gap-4">
|
||||
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
|
||||
<div className="group relative flex border-b border-custom-border-200">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex w-full items-center overflow-x-auto px-4 horizontal-scrollbar scrollbar-sm"
|
||||
>
|
||||
{DEFAULT_GLOBAL_VIEWS_LIST.map((tab, index) => (
|
||||
<DefaultViewTab key={`${tab.key}-${index}`} tab={tab} />
|
||||
))}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex h-full w-full items-center overflow-x-auto horizontal-scrollbar scrollbar-sm"
|
||||
>
|
||||
{DEFAULT_GLOBAL_VIEWS_LIST.map((tab, index) => (
|
||||
<DefaultViewTab key={`${tab.key}-${index}`} tab={tab} />
|
||||
))}
|
||||
|
||||
{currentWorkspaceViews?.map((viewId) => <ViewTab key={viewId} viewId={viewId} />)}
|
||||
</div>
|
||||
|
||||
{isAuthorizedUser && (
|
||||
<button
|
||||
type="button"
|
||||
className="sticky -right-4 flex w-12 flex-shrink-0 items-center justify-center border-transparent bg-custom-background-100 py-3 hover:border-custom-border-200 hover:text-custom-text-400"
|
||||
onClick={() => setCreateViewModal(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4 text-custom-primary-200" />
|
||||
</button>
|
||||
)}
|
||||
{currentWorkspaceViews?.map((viewId) => <ViewTab key={viewId} viewId={viewId} />)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
{isAuthorizedUser ? (
|
||||
<button
|
||||
type="button"
|
||||
className="sticky -right-4 flex flex-shrink-0 items-center justify-center border-transparent bg-custom-background-100 py-3 hover:border-custom-border-200 hover:text-custom-text-400"
|
||||
onClick={() => setCreateViewModal(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4 text-custom-primary-200" />
|
||||
</button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</CustomHeader>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue