[WEB-4969] feat: add toggle button for work item filters row visibility (#7865)
* [WEB-4969] feat: add toggle button for work item filters row visibility * fix: improve error handling in filter update and refine visibility condition check * chore: correct visibility toggle parameter in filter store
This commit is contained in:
parent
992457efd2
commit
7ce21a6488
31 changed files with 394 additions and 141 deletions
|
|
@ -38,6 +38,7 @@ import {
|
|||
LayoutSelection,
|
||||
MobileLayoutSelection,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
|
|
@ -209,6 +210,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||
activeLayout={activeLayout}
|
||||
/>
|
||||
</div>
|
||||
<WorkItemFiltersToggle entityType={EIssuesStoreType.CYCLE} entityId={cycleId} />
|
||||
<FiltersDropdown
|
||||
title={t("common.display")}
|
||||
placement="bottom-end"
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ import {
|
|||
LayoutSelection,
|
||||
MobileLayoutSelection,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
// helpers
|
||||
import { ModuleQuickActions } from "@/components/modules";
|
||||
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
|
|
@ -57,7 +57,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = useParams();
|
||||
const { workspaceSlug, projectId, moduleId: routerModuleId } = useParams();
|
||||
const moduleId = routerModuleId ? routerModuleId.toString() : undefined;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
// store hooks
|
||||
|
|
@ -75,7 +76,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||
// derived values
|
||||
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
||||
const moduleDetails = moduleId ? getModuleById(moduleId) : undefined;
|
||||
const canUserCreateIssue = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
|
|
@ -197,6 +198,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||
activeLayout={activeLayout}
|
||||
/>
|
||||
</div>
|
||||
{moduleId && <WorkItemFiltersToggle entityType={EIssuesStoreType.MODULE} entityId={moduleId} />}
|
||||
<FiltersDropdown
|
||||
title="Display"
|
||||
placement="bottom-end"
|
||||
|
|
@ -251,13 +253,15 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||
>
|
||||
<PanelRight className={cn("h-4 w-4", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
|
||||
</button>
|
||||
<ModuleQuickActions
|
||||
parentRef={parentRef}
|
||||
moduleId={moduleId?.toString()}
|
||||
projectId={projectId.toString()}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
customClassName="flex-shrink-0 flex items-center justify-center bg-custom-background-80/70 rounded size-[26px]"
|
||||
/>
|
||||
{moduleId && (
|
||||
<ModuleQuickActions
|
||||
parentRef={parentRef}
|
||||
moduleId={moduleId}
|
||||
projectId={projectId.toString()}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
customClassName="flex-shrink-0 flex items-center justify-center bg-custom-background-80/70 rounded size-[26px]"
|
||||
/>
|
||||
)}
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { SwitcherIcon, SwitcherLabel } from "@/components/common/switcher-label"
|
|||
import { DisplayFiltersSelection, FiltersDropdown, LayoutSelection } from "@/components/issues/issue-layouts/filters";
|
||||
// constants
|
||||
import { ViewQuickActions } from "@/components/views/quick-actions";
|
||||
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
|
|
@ -45,8 +46,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||
// refs
|
||||
const parentRef = useRef(null);
|
||||
// router
|
||||
const { workspaceSlug, projectId, viewId } = useParams();
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId, viewId: routerViewId } = useParams();
|
||||
const viewId = routerViewId ? routerViewId.toString() : undefined;
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
|
|
@ -163,8 +165,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||
)}
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem className="items-center">
|
||||
{!viewDetails?.is_locked ? (
|
||||
<>
|
||||
<>
|
||||
{!viewDetails.is_locked && (
|
||||
<LayoutSelection
|
||||
layouts={[
|
||||
EIssueLayoutTypes.LIST,
|
||||
|
|
@ -176,6 +178,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
)}
|
||||
{viewId && <WorkItemFiltersToggle entityType={EIssuesStoreType.PROJECT_VIEW} entityId={viewId} />}
|
||||
{!viewDetails.is_locked && (
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
|
|
@ -189,10 +194,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
)}
|
||||
</>
|
||||
{canUserCreateIssue ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { Breadcrumbs, Header, BreadcrumbNavigationSearchDropdown } from "@plane/
|
|||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
import { SwitcherLabel } from "@/components/common/switcher-label";
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
|
||||
import { DefaultWorkspaceViewQuickActions } from "@/components/workspace/views/default-view-quick-action";
|
||||
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace/views/modal";
|
||||
import { WorkspaceViewQuickActions } from "@/components/workspace/views/quick-action";
|
||||
|
|
@ -39,7 +40,8 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
const [createViewModal, setCreateViewModal] = useState(false);
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, globalViewId } = useParams();
|
||||
const { workspaceSlug, globalViewId: routerGlobalViewId } = useParams();
|
||||
const globalViewId = routerGlobalViewId ? routerGlobalViewId.toString() : undefined;
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { filters, updateFilters },
|
||||
|
|
@ -50,7 +52,7 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined;
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
const viewDetails = getViewDetailsById(globalViewId.toString());
|
||||
const viewDetails = globalViewId ? getViewDetailsById(globalViewId) : undefined;
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
|
|
@ -60,7 +62,7 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
undefined,
|
||||
EIssueFilterType.DISPLAY_FILTERS,
|
||||
updatedDisplayFilter,
|
||||
globalViewId.toString()
|
||||
globalViewId
|
||||
);
|
||||
},
|
||||
[workspaceSlug, updateFilters, globalViewId]
|
||||
|
|
@ -69,13 +71,7 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !globalViewId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.DISPLAY_PROPERTIES,
|
||||
property,
|
||||
globalViewId.toString()
|
||||
);
|
||||
updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.DISPLAY_PROPERTIES, property, globalViewId);
|
||||
},
|
||||
[workspaceSlug, updateFilters, globalViewId]
|
||||
);
|
||||
|
|
@ -88,7 +84,7 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
undefined,
|
||||
EIssueFilterType.DISPLAY_FILTERS,
|
||||
{ layout: layout },
|
||||
globalViewId.toString()
|
||||
globalViewId
|
||||
);
|
||||
},
|
||||
[workspaceSlug, updateFilters, globalViewId]
|
||||
|
|
@ -158,25 +154,24 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
</Header.LeftItem>
|
||||
|
||||
<Header.RightItem className="items-center">
|
||||
{!isLocked ? (
|
||||
<>
|
||||
<GlobalViewLayoutSelection
|
||||
onChange={handleLayoutChange}
|
||||
selectedLayout={activeLayout ?? EIssueLayoutTypes.SPREADSHEET}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
{!isLocked && (
|
||||
<GlobalViewLayoutSelection
|
||||
onChange={handleLayoutChange}
|
||||
selectedLayout={activeLayout ?? EIssueLayoutTypes.SPREADSHEET}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
/>
|
||||
)}
|
||||
{globalViewId && <WorkItemFiltersToggle entityType={EIssuesStoreType.GLOBAL} entityId={globalViewId} />}
|
||||
{!isLocked && (
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={currentLayoutFilters}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={currentLayoutFilters}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
</FiltersDropdown>
|
||||
)}
|
||||
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -9,13 +9,16 @@ import { EHeaderVariant, Header } from "@plane/ui";
|
|||
// components
|
||||
import { ArchiveTabsList } from "@/components/archives";
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
|
||||
export const ArchivedIssuesHeader: FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug.toString() : undefined;
|
||||
const projectId = routerProjectId ? routerProjectId.toString() : undefined;
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const {
|
||||
|
|
@ -29,7 +32,7 @@ export const ArchivedIssuesHeader: FC = observer(() => {
|
|||
const handleDisplayFiltersUpdate = (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, {
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, {
|
||||
...issueFilters?.displayFilters,
|
||||
...updatedDisplayFilter,
|
||||
});
|
||||
|
|
@ -38,15 +41,17 @@ export const ArchivedIssuesHeader: FC = observer(() => {
|
|||
const handleDisplayPropertiesUpdate = (property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property);
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property);
|
||||
};
|
||||
|
||||
if (!workspaceSlug || !projectId) return null;
|
||||
return (
|
||||
<Header variant={EHeaderVariant.SECONDARY}>
|
||||
<Header.LeftItem>
|
||||
<ArchiveTabsList />
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem className="items-center">
|
||||
<WorkItemFiltersToggle entityType={EIssuesStoreType.ARCHIVED} entityId={projectId} />
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilters?.displayFilters || {}}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { useIssues } from "@/hooks/store/use-issues";
|
|||
import { TProject } from "@/plane-web/types";
|
||||
// local imports
|
||||
import { WorkItemsModal } from "../analytics/work-items/modal";
|
||||
import { WorkItemFiltersToggle } from "../work-item-filters/filters-toggle";
|
||||
import {
|
||||
DisplayFiltersSelection,
|
||||
FiltersDropdown,
|
||||
|
|
@ -102,6 +103,7 @@ export const HeaderFilters = observer((props: Props) => {
|
|||
activeLayout={activeLayout}
|
||||
/>
|
||||
</div>
|
||||
<WorkItemFiltersToggle entityType={storeType} entityId={projectId} />
|
||||
<FiltersDropdown
|
||||
miniIcon={<SlidersHorizontal className="size-3.5" />}
|
||||
title={t("common.display")}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { EmptyState } from "@/components/common/empty-state";
|
|||
import { IssuePeekOverview } from "@/components/issues/peek-overview";
|
||||
import { WorkspaceActiveLayout } from "@/components/views/helper";
|
||||
import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
// hooks
|
||||
import { useGlobalView } from "@/hooks/store/use-global-view";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { EIssuesStoreType } from "@plane/types";
|
|||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
// hooks
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
// local imports
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { TransferIssues } from "@/components/cycles/transfer-issues";
|
|||
import { TransferIssuesModal } from "@/components/cycles/transfer-issues-modal";
|
||||
// hooks
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { Row, ERowVariant } from "@plane/ui";
|
|||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
// hooks
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
// local imports
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { Spinner } from "@plane/ui";
|
|||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
|||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
// hooks
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useProjectView } from "@/hooks/store/use-project-view";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useTranslation } from "@plane/i18n";
|
|||
import { EIssuesStoreType, IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FiltersDropdown, LayoutSelection } from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
|
||||
|
|
@ -16,7 +17,8 @@ export const ProfileIssuesFilter = observer(() => {
|
|||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
const { workspaceSlug, userId: routeUserId } = useParams();
|
||||
const userId = routeUserId ? routeUserId.toString() : undefined;
|
||||
// store hook
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
|
|
@ -27,13 +29,7 @@ export const ProfileIssuesFilter = observer(() => {
|
|||
const handleLayoutChange = useCallback(
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.DISPLAY_FILTERS,
|
||||
{ layout: layout },
|
||||
userId.toString()
|
||||
);
|
||||
updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, userId);
|
||||
},
|
||||
[workspaceSlug, updateFilters, userId]
|
||||
);
|
||||
|
|
@ -46,7 +42,7 @@ export const ProfileIssuesFilter = observer(() => {
|
|||
undefined,
|
||||
EIssueFilterType.DISPLAY_FILTERS,
|
||||
updatedDisplayFilter,
|
||||
userId.toString()
|
||||
userId
|
||||
);
|
||||
},
|
||||
[workspaceSlug, updateFilters, userId]
|
||||
|
|
@ -55,13 +51,7 @@ export const ProfileIssuesFilter = observer(() => {
|
|||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.DISPLAY_PROPERTIES,
|
||||
property,
|
||||
userId.toString()
|
||||
);
|
||||
updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.DISPLAY_PROPERTIES, property, userId);
|
||||
},
|
||||
[workspaceSlug, updateFilters, userId]
|
||||
);
|
||||
|
|
@ -73,6 +63,7 @@ export const ProfileIssuesFilter = observer(() => {
|
|||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
{userId && <WorkItemFiltersToggle entityType={EIssuesStoreType.PROFILE} entityId={userId} />}
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { ProfileIssuesKanBanLayout } from "@/components/issues/issue-layouts/kan
|
|||
import { ProfileIssuesListLayout } from "@/components/issues/issue-layouts/list/roots/profile-issues-root";
|
||||
import { IssuePeekOverview } from "@/components/issues/peek-overview";
|
||||
import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { cn, getOperatorForPayload } from "@plane/utils";
|
|||
|
||||
export type TAddFilterButtonProps<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||
buttonConfig?: {
|
||||
label?: string;
|
||||
label: string | null;
|
||||
variant?: TButtonVariant;
|
||||
className?: string;
|
||||
defaultOpen?: boolean;
|
||||
|
|
@ -28,7 +28,7 @@ export const AddFilterButton = observer(
|
|||
<P extends TFilterProperty, E extends TExternalFilter>(props: TAddFilterButtonProps<P, E>) => {
|
||||
const { filter, buttonConfig, onFilterSelect } = props;
|
||||
const {
|
||||
label = "Filters",
|
||||
label,
|
||||
variant = "link-neutral",
|
||||
className,
|
||||
defaultOpen = false,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React, { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ListFilterPlus } from "lucide-react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { TExternalFilter, TFilterProperty } from "@plane/types";
|
||||
import { EHeaderVariant, Header } from "@plane/ui";
|
||||
import { cn, EHeaderVariant, Header } from "@plane/ui";
|
||||
// local imports
|
||||
import { AddFilterButton, TAddFilterButtonProps } from "./add-filters-button";
|
||||
import { FilterItem } from "./filter-item";
|
||||
|
|
@ -14,8 +15,7 @@ export type TFiltersRowProps<K extends TFilterProperty, E extends TExternalFilte
|
|||
buttonConfig?: TAddFilterButtonProps<K, E>["buttonConfig"];
|
||||
disabledAllOperations?: boolean;
|
||||
filter: IFilterInstance<K, E>;
|
||||
variant?: "default" | "header";
|
||||
visible?: boolean;
|
||||
variant?: "modal" | "header";
|
||||
trackerElements?: {
|
||||
clearFilter?: string;
|
||||
saveView?: string;
|
||||
|
|
@ -25,25 +25,35 @@ export type TFiltersRowProps<K extends TFilterProperty, E extends TExternalFilte
|
|||
|
||||
export const FiltersRow = observer(
|
||||
<K extends TFilterProperty, E extends TExternalFilter>(props: TFiltersRowProps<K, E>) => {
|
||||
const {
|
||||
buttonConfig,
|
||||
disabledAllOperations = false,
|
||||
filter,
|
||||
variant = "header",
|
||||
visible = true,
|
||||
trackerElements,
|
||||
} = props;
|
||||
const { buttonConfig, disabledAllOperations = false, filter, variant = "header", trackerElements } = props;
|
||||
// states
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
// derived values
|
||||
const hasAnyConditions = filter.allConditionsForDisplay.length > 0;
|
||||
const hasAvailableOperations =
|
||||
!disabledAllOperations && (filter.canClearFilters || filter.canSaveView || filter.canUpdateView);
|
||||
|
||||
const headerButtonConfig: Partial<TAddFilterButtonProps<K, E>["buttonConfig"]> = {
|
||||
variant: "link-neutral",
|
||||
className: "bg-custom-background-90",
|
||||
label: null,
|
||||
};
|
||||
|
||||
const modalButtonConfig: Partial<TAddFilterButtonProps<K, E>["buttonConfig"]> = {
|
||||
variant: "neutral-primary",
|
||||
className: "bg-custom-background-100",
|
||||
label: !hasAnyConditions ? "Filters" : null,
|
||||
};
|
||||
|
||||
const handleUpdate = useCallback(async () => {
|
||||
setIsUpdating(true);
|
||||
await filter.updateView();
|
||||
setTimeout(() => setIsUpdating(false), 240); // To avoid flickering
|
||||
try {
|
||||
await filter.updateView();
|
||||
} finally {
|
||||
setTimeout(() => setIsUpdating(false), 240); // To avoid flickering
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
const leftContent = (
|
||||
<>
|
||||
{filter.allConditionsForDisplay.map((condition) => (
|
||||
|
|
@ -52,7 +62,13 @@ export const FiltersRow = observer(
|
|||
<AddFilterButton
|
||||
filter={filter}
|
||||
buttonConfig={{
|
||||
variant: "neutral-primary",
|
||||
label: null,
|
||||
...(variant === "modal" ? modalButtonConfig : headerButtonConfig),
|
||||
iconConfig: {
|
||||
shouldShowIcon: true,
|
||||
iconComponent: ListFilterPlus,
|
||||
},
|
||||
defaultOpen: buttonConfig?.defaultOpen ?? !hasAnyConditions,
|
||||
...buttonConfig,
|
||||
isDisabled: disabledAllOperations,
|
||||
}}
|
||||
|
|
@ -100,22 +116,43 @@ export const FiltersRow = observer(
|
|||
</>
|
||||
);
|
||||
|
||||
if (variant === "default") {
|
||||
return (
|
||||
<div className="w-full flex flex-wrap items-center gap-2">
|
||||
{leftContent}
|
||||
const mainContent = (
|
||||
<div className="w-full flex items-start gap-2">
|
||||
<div className="w-full flex flex-wrap items-center gap-2">{leftContent}</div>
|
||||
<div
|
||||
className={cn("flex items-center gap-2 border-l border-custom-border-200 pl-4", {
|
||||
"border-l-transparent pl-0": !hasAvailableOperations,
|
||||
})}
|
||||
>
|
||||
{rightContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
const ModalVariant = (
|
||||
<div className="w-full flex flex-wrap items-center gap-2 min-h-11 bg-custom-background-90 rounded-lg p-2">
|
||||
{mainContent}
|
||||
</div>
|
||||
);
|
||||
|
||||
const HeaderVariant = (
|
||||
<Header variant={EHeaderVariant.TERNARY} className="min-h-11">
|
||||
{mainContent}
|
||||
</Header>
|
||||
);
|
||||
|
||||
return (
|
||||
<Header variant={EHeaderVariant.TERNARY}>
|
||||
<div className="w-full flex items-start gap-2">
|
||||
<div className="w-full flex flex-wrap items-center gap-2">{leftContent}</div>
|
||||
<div className="flex items-center gap-2">{rightContent}</div>
|
||||
</div>
|
||||
</Header>
|
||||
<Transition
|
||||
show={filter.isVisible}
|
||||
enter="transition-all duration-150 ease-out"
|
||||
enterFrom="opacity-0 -translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition-all duration-100 ease-in"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 -translate-y-1"
|
||||
>
|
||||
{variant === "modal" ? ModalVariant : HeaderVariant}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
76
apps/web/core/components/rich-filters/filters-toggle.tsx
Normal file
76
apps/web/core/components/rich-filters/filters-toggle.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { ListFilter } from "lucide-react";
|
||||
// plane imports
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { TExternalFilter, TFilterProperty } from "@plane/types";
|
||||
import { cn } from "@plane/ui";
|
||||
// components
|
||||
import { AddFilterButton } from "@/components/rich-filters/add-filters-button";
|
||||
|
||||
type TFiltersToggleProps<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||
filter: IFilterInstance<P, E> | undefined;
|
||||
};
|
||||
|
||||
const COMMON_CLASSNAME =
|
||||
"grid place-items-center h-7 w-full py-0.5 px-2 rounded border transition-all duration-200 cursor-pointer";
|
||||
|
||||
export const FiltersToggle = observer(
|
||||
<P extends TFilterProperty, E extends TExternalFilter>(props: TFiltersToggleProps<P, E>) => {
|
||||
const { filter } = props;
|
||||
// derived values
|
||||
const hasAnyConditions = (filter?.allConditionsForDisplay.length ?? 0) > 0;
|
||||
const isFilterRowVisible = filter?.isVisible ?? false;
|
||||
const hasUpdates = filter?.canUpdateView === true && filter?.hasChanges === true;
|
||||
const showFilterRowChangesPill = hasUpdates || hasAnyConditions === true;
|
||||
const showAddFilterButton = !hasAnyConditions && !isFilterRowVisible && !hasUpdates;
|
||||
|
||||
const handleToggleFilter = () => {
|
||||
if (!filter) {
|
||||
console.error("Filters toggle error - filter instance not available");
|
||||
return;
|
||||
}
|
||||
filter.toggleVisibility();
|
||||
};
|
||||
|
||||
// Show the add filter button when there are no active conditions, the filter row is hidden, and no unsaved changes exist
|
||||
if (filter && showAddFilterButton) {
|
||||
return (
|
||||
<AddFilterButton
|
||||
filter={filter}
|
||||
buttonConfig={{
|
||||
variant: "neutral-primary",
|
||||
className: COMMON_CLASSNAME,
|
||||
label: null,
|
||||
}}
|
||||
onFilterSelect={() => filter?.toggleVisibility(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(COMMON_CLASSNAME, {
|
||||
"border-transparent bg-custom-primary-100/10 hover:bg-custom-primary-100/20": isFilterRowVisible,
|
||||
"border-custom-border-200 hover:bg-custom-background-90": !isFilterRowVisible,
|
||||
})}
|
||||
onClick={handleToggleFilter}
|
||||
>
|
||||
<div className="relative">
|
||||
<ListFilter
|
||||
className={cn("size-4", {
|
||||
"text-custom-primary-100": isFilterRowVisible,
|
||||
"text-custom-text-300": !isFilterRowVisible,
|
||||
})}
|
||||
/>
|
||||
{showFilterRowChangesPill && (
|
||||
<span
|
||||
className={cn("p-[3px] rounded-full bg-custom-primary-100 absolute top-[0.2px] -right-[0.4px]", {
|
||||
"bg-custom-text-300": hasAnyConditions === false && filter?.hasChanges === true, // If there are no conditions and there are changes, show the pill in the background color
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -23,7 +23,7 @@ import { getComputedDisplayFilters, getComputedDisplayProperties, getTabIndex }
|
|||
// components
|
||||
import { Logo } from "@/components/common/logo";
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
|
@ -265,11 +265,12 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||
isTemporary
|
||||
updateFilters={(updateFilters) => onFiltersChange(updateFilters)}
|
||||
projectId={projectId}
|
||||
showOnMount
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: projectViewWorkItemsFilter }) =>
|
||||
projectViewWorkItemsFilter && (
|
||||
<WorkItemFiltersRow filter={projectViewWorkItemsFilter} variant="default" />
|
||||
<WorkItemFiltersRow filter={projectViewWorkItemsFilter} variant="modal" />
|
||||
)
|
||||
}
|
||||
</ProjectLevelWorkItemFiltersHOC>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { observer } from "mobx-react";
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
// plane imports
|
||||
import { TSaveViewOptions, TUpdateViewOptions } from "@plane/constants";
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { IIssueFilters, TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||
import { IWorkItemFilterInstance } from "@plane/shared-state";
|
||||
import { IIssueFilters, TWorkItemFilterExpression } from "@plane/types";
|
||||
// store hooks
|
||||
import { useWorkItemFilters } from "@/hooks/store/work-item-filters/use-work-item-filters";
|
||||
// plane web imports
|
||||
|
|
@ -39,9 +39,7 @@ export const WorkItemFiltersHOC = observer((props: TWorkItemFiltersHOCProps) =>
|
|||
type TWorkItemFilterProps = TSharedWorkItemFiltersProps &
|
||||
TAdditionalWorkItemFiltersProps & {
|
||||
initialWorkItemFilters: IIssueFilters;
|
||||
children:
|
||||
| React.ReactNode
|
||||
| ((props: { filter: IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression> }) => React.ReactNode);
|
||||
children: React.ReactNode | ((props: { filter: IWorkItemFilterInstance }) => React.ReactNode);
|
||||
};
|
||||
|
||||
const WorkItemFilterRoot = observer((props: TWorkItemFilterProps) => {
|
||||
|
|
@ -55,6 +53,7 @@ const WorkItemFilterRoot = observer((props: TWorkItemFilterProps) => {
|
|||
saveViewOptions,
|
||||
updateFilters,
|
||||
updateViewOptions,
|
||||
showOnMount,
|
||||
...entityConfigProps
|
||||
} = props;
|
||||
// store hooks
|
||||
|
|
@ -84,6 +83,7 @@ const WorkItemFilterRoot = observer((props: TWorkItemFilterProps) => {
|
|||
saveViewOptions,
|
||||
updateViewOptions,
|
||||
},
|
||||
showOnMount,
|
||||
});
|
||||
|
||||
// delete filter instance when component unmounts
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// plane imports
|
||||
import { TSaveViewOptions, TUpdateViewOptions } from "@plane/constants";
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { IWorkItemFilterInstance } from "@plane/shared-state";
|
||||
import { EIssuesStoreType, IIssueFilters, TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||
|
||||
export type TSharedWorkItemFiltersProps = {
|
||||
|
|
@ -8,14 +8,11 @@ export type TSharedWorkItemFiltersProps = {
|
|||
filtersToShowByLayout: TWorkItemFilterProperty[];
|
||||
updateFilters: (updatedFilters: TWorkItemFilterExpression) => void;
|
||||
isTemporary?: boolean;
|
||||
showOnMount?: boolean;
|
||||
} & ({ isTemporary: true; entityId?: string } | { isTemporary?: false; entityId: string }); // entity id (project_id, cycle_id, workspace_id, etc)
|
||||
|
||||
export type TSharedWorkItemFiltersHOCProps = TSharedWorkItemFiltersProps & {
|
||||
children:
|
||||
| React.ReactNode
|
||||
| ((props: {
|
||||
filter: IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression> | undefined;
|
||||
}) => React.ReactNode);
|
||||
children: React.ReactNode | ((props: { filter: IWorkItemFilterInstance | undefined }) => React.ReactNode);
|
||||
initialWorkItemFilters: IIssueFilters | undefined;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { IWorkItemFilterInstance } from "@plane/shared-state";
|
||||
import { TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||
// components
|
||||
import { FiltersRow, TFiltersRowProps } from "@/components/rich-filters/filters-row";
|
||||
|
||||
type TWorkItemFiltersRowProps = TFiltersRowProps<TWorkItemFilterProperty, TWorkItemFilterExpression>;
|
||||
type TWorkItemFiltersRowProps = TFiltersRowProps<TWorkItemFilterProperty, TWorkItemFilterExpression> & {
|
||||
filter: IWorkItemFilterInstance;
|
||||
};
|
||||
|
||||
export const WorkItemFiltersRow = observer((props: TWorkItemFiltersRowProps) => <FiltersRow {...props} />);
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
// components
|
||||
import { FiltersToggle } from "@/components/rich-filters/filters-toggle";
|
||||
// hooks
|
||||
import { useWorkItemFilters } from "@/hooks/store/work-item-filters/use-work-item-filters";
|
||||
|
||||
type TWorkItemFiltersToggleProps = {
|
||||
entityType: EIssuesStoreType;
|
||||
entityId: string;
|
||||
};
|
||||
|
||||
export const WorkItemFiltersToggle = observer((props: TWorkItemFiltersToggleProps) => {
|
||||
const { entityType, entityId } = props;
|
||||
// store hooks
|
||||
const { getFilter } = useWorkItemFilters();
|
||||
// derived values
|
||||
const filter = getFilter(entityType, entityId);
|
||||
|
||||
return <FiltersToggle filter={filter} />;
|
||||
});
|
||||
|
|
@ -21,7 +21,7 @@ import { getComputedDisplayFilters, getComputedDisplayProperties } from "@plane/
|
|||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level";
|
||||
// plane web imports
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
import { AccessController } from "@/plane-web/components/views/access-controller";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -176,11 +176,12 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
|
|||
initialWorkItemFilters={workItemFilters}
|
||||
isTemporary
|
||||
updateFilters={(updateFilters) => onFiltersChange(updateFilters)}
|
||||
showOnMount
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: workspaceViewWorkItemsFilter }) =>
|
||||
workspaceViewWorkItemsFilter && (
|
||||
<WorkItemFiltersRow filter={workspaceViewWorkItemsFilter} variant="default" />
|
||||
<WorkItemFiltersRow filter={workspaceViewWorkItemsFilter} variant="modal" />
|
||||
)
|
||||
}
|
||||
</WorkspaceLevelWorkItemFiltersHOC>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
// plane imports
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { EIssuesStoreType, TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||
import { IWorkItemFilterInstance } from "@plane/shared-state";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
// local imports
|
||||
import { useWorkItemFilters } from "./use-work-item-filters";
|
||||
|
||||
export const useWorkItemFilterInstance = (
|
||||
entityType: EIssuesStoreType,
|
||||
entityId: string
|
||||
): IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression> | undefined => {
|
||||
): IWorkItemFilterInstance | undefined => {
|
||||
const { getFilter } = useWorkItemFilters();
|
||||
return getFilter(entityType, entityId);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue