mirror of
https://github.com/makeplane/plane.git
synced 2026-02-25 04:35:21 +01:00
[WEB-3759]chore: header revamp (#2872)
* chore: updated header for customers * chore: updated header for initiatives and dashboards * chore: header revamp for cycles, modules, pages and views * feat: search functionality for project level epics --------- Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
This commit is contained in:
@@ -198,10 +198,16 @@ class EpicViewSet(BaseViewSet):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
@check_feature_flag(FeatureFlag.EPICS)
|
||||
def list(self, request, slug, project_id):
|
||||
search = request.GET.get("search", None)
|
||||
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
epics = self.get_queryset().filter(**filters)
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
# Add search functionality
|
||||
if search:
|
||||
epics = epics.filter(Q(name__icontains=search))
|
||||
|
||||
# epics queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
issue_queryset=epics, order_by_param=order_by_param
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
@@ -6,37 +6,19 @@ import { Eye, LayoutGrid, Pencil, Plus } from "lucide-react";
|
||||
// plane imports
|
||||
import { EWidgetChartModels, EWidgetChartTypes } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Breadcrumbs, Button, CustomMenu, getButtonStyling, Header, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
import { ICustomSearchSelectOption } from "@plane/types";
|
||||
import { Breadcrumbs, Button, CustomSearchSelect, getButtonStyling, Header, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// helpers
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
// plane web components
|
||||
import { DashboardQuickActions } from "@/plane-web/components/dashboards/quick-actions";
|
||||
import { DashboardWidgetChartTypesDropdown } from "@/plane-web/components/dashboards/widgets/dropdown";
|
||||
// plane web hooks
|
||||
import { useDashboards } from "@/plane-web/hooks/store";
|
||||
|
||||
const DashboardDropdownOption: React.FC<{ dashboardId: string }> = ({ dashboardId }) => {
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { getDashboardById } = useDashboards();
|
||||
// derived values
|
||||
const dashboard = getDashboardById(dashboardId);
|
||||
|
||||
if (!dashboard) return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem key={dashboard.id}>
|
||||
<Link href={`/${workspaceSlug}/dashboards/${dashboard.id}`} className="flex items-center gap-1.5">
|
||||
<LayoutGrid className="flex-shrink-0 size-3" />
|
||||
{truncateText(dashboard.name ?? "", 40)}
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceDashboardDetailsHeader = observer(() => {
|
||||
// refs
|
||||
const parentRef = useRef(null);
|
||||
// states
|
||||
const [isAddingWidget, setIsAddingWidget] = useState(false);
|
||||
// navigation
|
||||
@@ -75,6 +57,22 @@ export const WorkspaceDashboardDetailsHeader = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
const switcherOptions = currentWorkspaceDashboardIds
|
||||
.map((id) => {
|
||||
const _dashboard = getDashboardById(id);
|
||||
if (!_dashboard?.id || !_dashboard?.name) return null;
|
||||
return {
|
||||
value: _dashboard.id,
|
||||
query: _dashboard.name,
|
||||
content: (
|
||||
<Link href={`/${workspaceSlug}/dashboards/${_dashboard.id}`}>
|
||||
<SwitcherLabel name={_dashboard.name} LabelIcon={LayoutGrid} />
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
@@ -93,36 +91,19 @@ export const WorkspaceDashboardDetailsHeader = observer(() => {
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="component"
|
||||
component={
|
||||
<div className="flex items-center gap-2">
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
<LayoutGrid className="flex-shrink-0 size-3" />
|
||||
<div className="flex w-auto max-w-[70px] items-center gap-2 truncate sm:max-w-[200px]">
|
||||
<p className="truncate">{dashboardDetails?.name ?? ""}</p>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
className="ml-1.5 flex-shrink-0 truncate"
|
||||
placement="bottom-start"
|
||||
>
|
||||
{currentWorkspaceDashboardIds?.map((dashboardId) => (
|
||||
<DashboardDropdownOption key={dashboardId} dashboardId={dashboardId} />
|
||||
))}
|
||||
</CustomMenu>
|
||||
{dashboardDetails && !isViewModeEnabled && (
|
||||
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 rounded px-1 py-0.5 text-sm">
|
||||
{t("dashboards.common.editing")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<CustomSearchSelect
|
||||
label={<SwitcherLabel name={dashboardDetails?.name} LabelIcon={LayoutGrid} />}
|
||||
value={dashboardId.toString()}
|
||||
onChange={() => {}}
|
||||
options={switcherOptions}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
{dashboardDetails && (
|
||||
<Header.RightItem>
|
||||
<Header.RightItem className="items-center">
|
||||
{!isViewModeEnabled && canCurrentUserCreateWidget && (
|
||||
<DashboardWidgetChartTypesDropdown
|
||||
buttonClassName={getButtonStyling("neutral-primary", "sm")}
|
||||
@@ -145,6 +126,12 @@ export const WorkspaceDashboardDetailsHeader = observer(() => {
|
||||
>
|
||||
{t(isViewModeEnabled ? "common.edit" : "common.view")}
|
||||
</Button>
|
||||
<DashboardQuickActions
|
||||
dashboardId={dashboardId.toString()}
|
||||
parentRef={parentRef}
|
||||
showEdit={false}
|
||||
customClassName="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70"
|
||||
/>
|
||||
</Header.RightItem>
|
||||
)}
|
||||
</Header>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"use client";
|
||||
import { useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Sidebar } from "lucide-react";
|
||||
import { PanelRight } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Breadcrumbs, Header, InitiativeIcon } from "@plane/ui";
|
||||
import { ICustomSearchSelectOption } from "@plane/types";
|
||||
import { Breadcrumbs, CustomSearchSelect, Header, InitiativeIcon } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
@@ -25,7 +27,7 @@ export const InitiativesDetailsHeader = observer(() => {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
// store hooks
|
||||
const {
|
||||
initiative: { getInitiativeById },
|
||||
initiative: { getInitiativeById, initiativeIds },
|
||||
} = useInitiatives();
|
||||
const { initiativesSidebarCollapsed, toggleInitiativesSidebar } = useAppTheme();
|
||||
|
||||
@@ -33,6 +35,22 @@ export const InitiativesDetailsHeader = observer(() => {
|
||||
// derived values
|
||||
const initiativesDetails = initiativeId ? getInitiativeById(initiativeId.toString()) : undefined;
|
||||
|
||||
const switcherOptions = initiativeIds
|
||||
.map((id) => {
|
||||
const _initiative = getInitiativeById(id);
|
||||
if (!_initiative?.id || !_initiative?.name) return null;
|
||||
return {
|
||||
value: _initiative.id,
|
||||
query: _initiative.name,
|
||||
content: (
|
||||
<Link href={`/${workspaceSlug}/initiatives/${_initiative.id}`}>
|
||||
<SwitcherLabel name={_initiative.name} LabelIcon={InitiativeIcon} />
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
@@ -48,23 +66,39 @@ export const InitiativesDetailsHeader = observer(() => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label={initiativesDetails?.name} />} />
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="component"
|
||||
component={
|
||||
<CustomSearchSelect
|
||||
label={<SwitcherLabel name={initiativesDetails?.name} LabelIcon={InitiativeIcon} />}
|
||||
value={initiativeId.toString()}
|
||||
onChange={() => {}}
|
||||
options={switcherOptions}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
{initiativesDetails && (
|
||||
<div ref={parentRef} className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70"
|
||||
onClick={() => toggleInitiativesSidebar()}
|
||||
>
|
||||
<PanelRight
|
||||
className={cn("size-4 cursor-pointer", {
|
||||
"text-custom-primary-100": !initiativesSidebarCollapsed,
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
<InitiativeQuickActions
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
parentRef={parentRef}
|
||||
initiative={initiativesDetails}
|
||||
/>
|
||||
<Sidebar
|
||||
className={cn("size-4 cursor-pointer", {
|
||||
"text-custom-primary-100": !initiativesSidebarCollapsed,
|
||||
})}
|
||||
onClick={() => toggleInitiativesSidebar()}
|
||||
customClassName="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { Breadcrumbs, Header, CustomSearchSelect } from "@plane/ui";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
import { PageEditInformationPopover } from "@/components/pages";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
@@ -35,7 +34,7 @@ export const PageDetailsHeader = observer(() => {
|
||||
const { getPageById, getCurrentProjectPageIds } = usePageStore(EPageStoreType.PROJECT);
|
||||
// derived values
|
||||
const projectPageIds = getCurrentProjectPageIds(projectId?.toString());
|
||||
|
||||
|
||||
if (!page) return null;
|
||||
const switcherOptions = projectPageIds
|
||||
.map((id) => {
|
||||
|
||||
@@ -17,10 +17,11 @@ type Props = {
|
||||
customerId: string;
|
||||
workspaceSlug: string;
|
||||
parentRef: React.RefObject<HTMLDivElement> | null;
|
||||
customClassName?: string;
|
||||
};
|
||||
|
||||
export const CustomerQuickActions: React.FC<Props> = observer((props) => {
|
||||
const { customerId, workspaceSlug, parentRef } = props;
|
||||
const { customerId, workspaceSlug, parentRef, customClassName } = props;
|
||||
// states
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
// i18n
|
||||
@@ -82,7 +83,7 @@ export const CustomerQuickActions: React.FC<Props> = observer((props) => {
|
||||
handleClose={() => setIsDeleteModalOpen(false)}
|
||||
/>
|
||||
{parentRef && <ContextMenu parentRef={parentRef} items={MENU_ITEMS} />}
|
||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect>
|
||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect buttonClassName={customClassName}>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
if (item.shouldRender === false) return null;
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"use client";
|
||||
import React, { FC, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { Sidebar } from "lucide-react";
|
||||
import { Breadcrumbs, CustomersIcon, Header } from "@plane/ui";
|
||||
import { PanelRight } from "lucide-react";
|
||||
import { ICustomSearchSelectOption } from "@plane/types";
|
||||
import { Breadcrumbs, CustomersIcon, Header, CustomSearchSelect } from "@plane/ui";
|
||||
// components
|
||||
import { cn } from "@plane/utils";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
// plane web imports
|
||||
@@ -18,7 +20,7 @@ export const CustomerDetailHeader: FC = observer(() => {
|
||||
const { workspaceSlug, customerId } = useParams();
|
||||
// hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { getCustomerById } = useCustomers();
|
||||
const { getCustomerById, customerIds } = useCustomers();
|
||||
const { toggleCustomerDetailSidebar, customerDetailSidebarCollapsed } = useCustomers();
|
||||
// derived values
|
||||
const workspaceId = currentWorkspace?.id || undefined;
|
||||
@@ -28,6 +30,22 @@ export const CustomerDetailHeader: FC = observer(() => {
|
||||
const customer = getCustomerById(customerId.toString());
|
||||
if (!workspaceSlug || !workspaceId) return <></>;
|
||||
|
||||
const switcherOptions = customerIds
|
||||
.map((id) => {
|
||||
const _customer = getCustomerById(id);
|
||||
if (!_customer?.id || !_customer?.name) return null;
|
||||
return {
|
||||
value: _customer.id,
|
||||
query: _customer.name,
|
||||
content: (
|
||||
<Link href={`/${workspaceSlug}/customers/${_customer.id}`}>
|
||||
<SwitcherLabel logo_url={_customer.logo_url} name={_customer.name} LabelIcon={CustomersIcon} />
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
@@ -45,23 +63,39 @@ export const CustomerDetailHeader: FC = observer(() => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label={customer?.name} />} />
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="component"
|
||||
component={
|
||||
<CustomSearchSelect
|
||||
label={
|
||||
<SwitcherLabel logo_url={customer?.logo_url} name={customer?.name} LabelIcon={CustomersIcon} />
|
||||
}
|
||||
value={customerId.toString()}
|
||||
onChange={() => {}}
|
||||
options={switcherOptions}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
{customer && (
|
||||
<div ref={parentRef} className="flex gap-2 items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70"
|
||||
onClick={() => toggleCustomerDetailSidebar()}
|
||||
>
|
||||
<PanelRight
|
||||
className={cn("h-4 w-4", !customerDetailSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")}
|
||||
/>
|
||||
</button>
|
||||
<CustomerQuickActions
|
||||
customerId={customerId.toString()}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
parentRef={parentRef}
|
||||
/>
|
||||
<Sidebar
|
||||
className={cn("size-4 cursor-pointer", {
|
||||
"text-custom-primary-100": !customerDetailSidebarCollapsed,
|
||||
})}
|
||||
onClick={() => toggleCustomerDetailSidebar()}
|
||||
customClassName="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { CustomersIcon, CustomSearchSelect } from "@plane/ui";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
import { SwitcherLabel } from "@/components/common";
|
||||
import { useCustomers } from "@/plane-web/hooks/store";
|
||||
|
||||
type TProps = {
|
||||
@@ -43,24 +43,7 @@ export const CustomerDropDown: FC<TProps> = observer((props) => {
|
||||
return {
|
||||
value: customer?.id,
|
||||
query: `${customer?.name}`,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1">
|
||||
{customer?.logo_url ? (
|
||||
<img
|
||||
src={getFileURL(customer.logo_url)}
|
||||
alt="customer-logo"
|
||||
className="rounded-sm w-3 h-3 object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-custom-background-90 rounded-md flex items-center justify-center h-3 w-3">
|
||||
<CustomersIcon className="size-4 opacity-50" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm">{customer?.name}</p>
|
||||
</div>
|
||||
),
|
||||
content: <SwitcherLabel logo_url={customer?.logo_url} name={customer?.name} LabelIcon={CustomersIcon} />,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@ import { DashboardDeleteModal } from "./modals/delete-modal";
|
||||
type Props = {
|
||||
dashboardId: string;
|
||||
parentRef: React.RefObject<HTMLElement>;
|
||||
showEdit?: boolean;
|
||||
customClassName?: string;
|
||||
};
|
||||
|
||||
export const DashboardQuickActions: React.FC<Props> = observer((props) => {
|
||||
const { dashboardId, parentRef } = props;
|
||||
const { dashboardId, parentRef, showEdit = true, customClassName } = props;
|
||||
// states
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
// store hooks
|
||||
@@ -41,7 +43,7 @@ export const DashboardQuickActions: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
title: t("common.actions.edit"),
|
||||
icon: Pencil,
|
||||
shouldRender: !!canCurrentUserEditDashboard,
|
||||
shouldRender: !!canCurrentUserEditDashboard && showEdit,
|
||||
},
|
||||
{
|
||||
key: "open-in-new-tab",
|
||||
@@ -92,7 +94,13 @@ export const DashboardQuickActions: React.FC<Props> = observer((props) => {
|
||||
isOpen={isDeleteModalOpen}
|
||||
/>
|
||||
{parentRef && <ContextMenu parentRef={parentRef} items={MENU_ITEMS} />}
|
||||
<CustomMenu placement="bottom-end" optionsClassName="max-h-[90vh]" ellipsis closeOnSelect>
|
||||
<CustomMenu
|
||||
placement="bottom-end"
|
||||
optionsClassName="max-h-[90vh]"
|
||||
buttonClassName={customClassName}
|
||||
ellipsis
|
||||
closeOnSelect
|
||||
>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
if (item.shouldRender === false) return null;
|
||||
return (
|
||||
|
||||
@@ -20,10 +20,11 @@ type Props = {
|
||||
initiative: TInitiative;
|
||||
workspaceSlug: string;
|
||||
disabled?: boolean;
|
||||
customClassName?: string;
|
||||
};
|
||||
|
||||
export const InitiativeQuickActions: React.FC<Props> = observer((props) => {
|
||||
const { parentRef, initiative, workspaceSlug, disabled = false } = props;
|
||||
const { parentRef, initiative, workspaceSlug, disabled = false, customClassName } = props;
|
||||
// states
|
||||
const [updateModal, setUpdateModal] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
@@ -101,7 +102,7 @@ export const InitiativeQuickActions: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect>
|
||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect buttonClassName={customClassName}>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
if (item.shouldRender === false) return null;
|
||||
return (
|
||||
|
||||
@@ -30,6 +30,7 @@ type InitiativeCollapsible = "links" | "attachments" | "projects" | "epics";
|
||||
|
||||
export interface IInitiativeStore {
|
||||
initiativesMap: Record<string, TInitiative> | undefined;
|
||||
initiativeIds: string[];
|
||||
initiativesStatsMap: Record<string, TInitiativeStats> | undefined;
|
||||
initiativeLinks: IInitiativeLinkStore;
|
||||
initiativeCommentActivities: IInitiativeCommentActivityStore;
|
||||
@@ -156,6 +157,10 @@ export class InitiativeStore implements IInitiativeStore {
|
||||
return this.getGroupedInitiativeIds(workspaceSlug);
|
||||
}
|
||||
|
||||
get initiativeIds() {
|
||||
return Object.keys(this.initiativesMap ?? {});
|
||||
}
|
||||
|
||||
getGroupedInitiativeIds = computedFn((workspaceSlug: string) => {
|
||||
const workspace = this.rootStore.workspaceRoot.getWorkspaceBySlug(workspaceSlug);
|
||||
if (!workspace) return;
|
||||
|
||||
Reference in New Issue
Block a user