ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: унификация фильтров внешнего контура с внутренним rich-filters слоем
This commit is contained in:
parent
2c54a8f274
commit
0a85ea3cb2
|
|
@ -323,6 +323,7 @@ class ExternalContourReadMixin:
|
||||||
for key in [
|
for key in [
|
||||||
"direction",
|
"direction",
|
||||||
"status",
|
"status",
|
||||||
|
"state_groups",
|
||||||
"state_ids",
|
"state_ids",
|
||||||
"priority",
|
"priority",
|
||||||
"assignee_ids",
|
"assignee_ids",
|
||||||
|
|
@ -337,6 +338,9 @@ class ExternalContourReadMixin:
|
||||||
|
|
||||||
filters["has_unread_updates"] = self.parse_bool_param(request, "has_unread_updates")
|
filters["has_unread_updates"] = self.parse_bool_param(request, "has_unread_updates")
|
||||||
filters["search"] = (request.query_params.get("search") or "").strip()
|
filters["search"] = (request.query_params.get("search") or "").strip()
|
||||||
|
filters["target_date_exact"] = (request.query_params.get("target_date_exact") or "").strip()
|
||||||
|
filters["target_date_from"] = (request.query_params.get("target_date_from") or "").strip()
|
||||||
|
filters["target_date_to"] = (request.query_params.get("target_date_to") or "").strip()
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
def get_sorting(self, request):
|
def get_sorting(self, request):
|
||||||
|
|
@ -392,6 +396,8 @@ class ExternalContourReadMixin:
|
||||||
|
|
||||||
queryset = self.apply_status_filter(queryset, filters["status"])
|
queryset = self.apply_status_filter(queryset, filters["status"])
|
||||||
|
|
||||||
|
if filters["state_groups"]:
|
||||||
|
queryset = queryset.filter(issue__state__group__in=filters["state_groups"])
|
||||||
if filters["state_ids"]:
|
if filters["state_ids"]:
|
||||||
queryset = queryset.filter(issue__state_id__in=filters["state_ids"])
|
queryset = queryset.filter(issue__state_id__in=filters["state_ids"])
|
||||||
if filters["priority"]:
|
if filters["priority"]:
|
||||||
|
|
@ -419,6 +425,12 @@ class ExternalContourReadMixin:
|
||||||
)
|
)
|
||||||
if filters["label_ids"]:
|
if filters["label_ids"]:
|
||||||
queryset = queryset.filter(issue__label_issue__label_id__in=filters["label_ids"])
|
queryset = queryset.filter(issue__label_issue__label_id__in=filters["label_ids"])
|
||||||
|
if filters["target_date_exact"]:
|
||||||
|
queryset = queryset.filter(issue__target_date=filters["target_date_exact"])
|
||||||
|
if filters["target_date_from"]:
|
||||||
|
queryset = queryset.filter(issue__target_date__gte=filters["target_date_from"])
|
||||||
|
if filters["target_date_to"]:
|
||||||
|
queryset = queryset.filter(issue__target_date__lte=filters["target_date_to"])
|
||||||
if filters["has_unread_updates"] is not None:
|
if filters["has_unread_updates"] is not None:
|
||||||
unread_request_ids = self.get_unread_request_ids(request.user)
|
unread_request_ids = self.get_unread_request_ids(request.user)
|
||||||
if filters["has_unread_updates"]:
|
if filters["has_unread_updates"]:
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,23 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
|
import { useParams } from "react-router";
|
||||||
import { AppHeader } from "@/components/core/app-header";
|
import { AppHeader } from "@/components/core/app-header";
|
||||||
import { ContentWrapper } from "@/components/core/content-wrapper";
|
import { ContentWrapper } from "@/components/core/content-wrapper";
|
||||||
import { ProjectExternalContoursHeader } from "@/plane-web/components/projects/external-contours/header";
|
import { ProjectExternalContoursHeader } from "@/plane-web/components/projects/external-contours/header";
|
||||||
|
import { ProjectExternalContoursFiltersProvider } from "@/plane-web/components/projects/external-contours/filters/provider";
|
||||||
|
|
||||||
export default function ProjectExternalContoursLayout() {
|
export default function ProjectExternalContoursLayout() {
|
||||||
|
const { projectId, workspaceSlug } = useParams();
|
||||||
|
|
||||||
|
if (!projectId || !workspaceSlug) return <Outlet />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ProjectExternalContoursFiltersProvider projectId={projectId} workspaceSlug={workspaceSlug}>
|
||||||
<AppHeader header={<ProjectExternalContoursHeader />} />
|
<AppHeader header={<ProjectExternalContoursHeader />} />
|
||||||
<ContentWrapper>
|
<ContentWrapper>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</ContentWrapper>
|
</ContentWrapper>
|
||||||
</>
|
</ProjectExternalContoursFiltersProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,544 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { ISSUE_PRIORITIES } from "@plane/constants";
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
import { Button } from "@plane/propel/button";
|
|
||||||
import { CheckIcon, ChevronDownIcon, PriorityIcon, SearchIcon, CloseIcon } from "@plane/propel/icons";
|
|
||||||
import type { TExternalContourBoardSorting, TExternalContourRequest } from "@plane/types";
|
|
||||||
import { Avatar, Dropdown, MultiSelectDropdown } from "@plane/ui";
|
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
import { useProjectExternalContoursBoard } from "@/hooks/store/use-project-external-contours-board";
|
|
||||||
import useDebounce from "@/hooks/use-debounce";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
projectId: string;
|
|
||||||
workspaceSlug: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TFilterOption = {
|
|
||||||
avatarUrl?: string | null;
|
|
||||||
color?: string | null;
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TSortingOption = {
|
|
||||||
key: string;
|
|
||||||
label: string;
|
|
||||||
value: TExternalContourBoardSorting;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_SORTING_KEY = "updated_at:desc";
|
|
||||||
|
|
||||||
export const ExternalContoursBoardFiltersRow = observer(function ExternalContoursBoardFiltersRow(props: Props) {
|
|
||||||
const { projectId, workspaceSlug } = props;
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const {
|
|
||||||
activeFiltersCount,
|
|
||||||
clearFilters,
|
|
||||||
filters,
|
|
||||||
getColumnRequestIds,
|
|
||||||
getRequestById,
|
|
||||||
isSortingDefault,
|
|
||||||
items,
|
|
||||||
sorting,
|
|
||||||
updateFilters,
|
|
||||||
updateSorting,
|
|
||||||
} = useProjectExternalContoursBoard();
|
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState(filters.search ?? "");
|
|
||||||
const debouncedSearchQuery = useDebounce(searchQuery, 400);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSearchQuery(filters.search ?? "");
|
|
||||||
}, [filters.search]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const nextSearch = debouncedSearchQuery.trim();
|
|
||||||
const liveSearch = searchQuery.trim();
|
|
||||||
const currentSearch = filters.search?.trim() ?? "";
|
|
||||||
|
|
||||||
if (nextSearch !== liveSearch) return;
|
|
||||||
if (nextSearch === currentSearch) return;
|
|
||||||
|
|
||||||
void updateFilters(workspaceSlug, projectId, { search: nextSearch || undefined });
|
|
||||||
}, [debouncedSearchQuery, filters.search, projectId, searchQuery, updateFilters, workspaceSlug]);
|
|
||||||
|
|
||||||
const visibleRequestIds = [...getColumnRequestIds("outgoing"), ...getColumnRequestIds("incoming")];
|
|
||||||
const visibleRequests = visibleRequestIds.flatMap((requestId) => {
|
|
||||||
const request = getRequestById(requestId);
|
|
||||||
return request ? [request] : [];
|
|
||||||
});
|
|
||||||
const cachedRequests = Object.values(items);
|
|
||||||
|
|
||||||
const contourOptions = useMemo(
|
|
||||||
() =>
|
|
||||||
getCounterpartyProjectOptions(
|
|
||||||
visibleRequests,
|
|
||||||
cachedRequests,
|
|
||||||
filters.counterparty_project_ids ?? [],
|
|
||||||
projectId
|
|
||||||
),
|
|
||||||
[cachedRequests, filters.counterparty_project_ids, projectId, visibleRequests]
|
|
||||||
);
|
|
||||||
const stateOptions = useMemo(
|
|
||||||
() => getStateOptions(visibleRequests, cachedRequests, filters.state_ids ?? []),
|
|
||||||
[cachedRequests, filters.state_ids, visibleRequests]
|
|
||||||
);
|
|
||||||
const assigneeOptions = useMemo(() => getAssigneeOptions(visibleRequests, cachedRequests, filters.assignee_ids ?? []), [cachedRequests, filters.assignee_ids, visibleRequests]);
|
|
||||||
const requesterOptions = useMemo(
|
|
||||||
() => getRequesterOptions(visibleRequests, cachedRequests, filters.requested_by_ids ?? []),
|
|
||||||
[cachedRequests, filters.requested_by_ids, visibleRequests]
|
|
||||||
);
|
|
||||||
const priorityOptions = useMemo(() => getPriorityOptions(visibleRequests, cachedRequests, filters.priority ?? [], t), [cachedRequests, filters.priority, t, visibleRequests]);
|
|
||||||
|
|
||||||
const sortingOptions = useMemo<TSortingOption[]>(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
key: "updated_at:desc",
|
|
||||||
label: t("external_contours_page.board.filters.sorting.updated_at_desc"),
|
|
||||||
value: { order_by: "updated_at", sort_by: "desc" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "requested_at:desc",
|
|
||||||
label: t("external_contours_page.board.filters.sorting.requested_at_desc"),
|
|
||||||
value: { order_by: "requested_at", sort_by: "desc" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "target_date:asc",
|
|
||||||
label: t("external_contours_page.board.filters.sorting.target_date_asc"),
|
|
||||||
value: { order_by: "target_date", sort_by: "asc" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[t]
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedSortingKey = `${sorting.order_by ?? "updated_at"}:${sorting.sort_by ?? "desc"}`;
|
|
||||||
const selectedSortingLabel =
|
|
||||||
sortingOptions.find((option) => option.key === selectedSortingKey)?.label ??
|
|
||||||
sortingOptions.find((option) => option.key === DEFAULT_SORTING_KEY)?.label ??
|
|
||||||
t("external_contours_page.board.filters.sort");
|
|
||||||
|
|
||||||
const shouldShowClear = activeFiltersCount > 0 || !isSortingDefault;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="nodedc-filter-row-shell flex w-full flex-col gap-3 rounded-[1.75rem] px-4 py-4">
|
|
||||||
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
|
||||||
<div className="flex flex-1 flex-wrap items-center gap-2">
|
|
||||||
<div className="relative min-w-[17rem] flex-1 xl:max-w-[24rem]">
|
|
||||||
<SearchIcon className="pointer-events-none absolute top-1/2 left-3 h-3.5 w-3.5 -translate-y-1/2 text-placeholder" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
placeholder={t("external_contours_page.board.filters.search_placeholder")}
|
|
||||||
className="h-10 w-full rounded-full border-0 bg-white/5 pr-10 pl-9 text-13 text-primary outline-none placeholder:text-placeholder focus:bg-white/7"
|
|
||||||
/>
|
|
||||||
{searchQuery && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="absolute top-1/2 right-3 -translate-y-1/2 text-placeholder transition-colors hover:text-primary"
|
|
||||||
onClick={() => setSearchQuery("")}
|
|
||||||
>
|
|
||||||
<CloseIcon className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MultiSelectDropdown
|
|
||||||
value={filters.counterparty_project_ids ?? []}
|
|
||||||
onChange={(value) =>
|
|
||||||
void updateFilters(workspaceSlug, projectId, {
|
|
||||||
counterparty_project_ids: value.length > 0 ? value : undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
options={contourOptions}
|
|
||||||
keyExtractor={(option) => option.value}
|
|
||||||
queryArray={["label"]}
|
|
||||||
inputPlaceholder={t("external_contours_page.board.filters.search_contour")}
|
|
||||||
buttonContainerClassName="h-10"
|
|
||||||
optionsContainerClassName="w-72"
|
|
||||||
disabled={contourOptions.length === 0 && (filters.counterparty_project_ids?.length ?? 0) === 0}
|
|
||||||
buttonContent={(isOpen) => (
|
|
||||||
<FilterTrigger
|
|
||||||
label={t("external_contours_page.board.filters.contour")}
|
|
||||||
count={filters.counterparty_project_ids?.length ?? 0}
|
|
||||||
isOpen={isOpen}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
renderItem={({ value, selected }) => {
|
|
||||||
const option = contourOptions.find((item) => item.value === value);
|
|
||||||
if (!option) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-between gap-2 text-secondary">
|
|
||||||
<span className="truncate text-12 font-medium">{option.data.label}</span>
|
|
||||||
{selected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MultiSelectDropdown
|
|
||||||
value={filters.state_ids ?? []}
|
|
||||||
onChange={(value) => void updateFilters(workspaceSlug, projectId, { state_ids: value.length > 0 ? value : undefined })}
|
|
||||||
options={stateOptions}
|
|
||||||
keyExtractor={(option) => option.value}
|
|
||||||
queryArray={["label"]}
|
|
||||||
inputPlaceholder={t("external_contours_page.board.filters.search_state")}
|
|
||||||
buttonContainerClassName="h-10"
|
|
||||||
optionsContainerClassName="w-64"
|
|
||||||
disabled={stateOptions.length === 0 && (filters.state_ids?.length ?? 0) === 0}
|
|
||||||
buttonContent={(isOpen) => (
|
|
||||||
<FilterTrigger label={t("state")} count={filters.state_ids?.length ?? 0} isOpen={isOpen} />
|
|
||||||
)}
|
|
||||||
renderItem={({ value, selected }) => {
|
|
||||||
const option = stateOptions.find((item) => item.value === value);
|
|
||||||
if (!option) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-between gap-2 text-secondary">
|
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
|
||||||
<span
|
|
||||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
|
||||||
style={{ backgroundColor: option.data.color || "rgba(255,255,255,0.3)" }}
|
|
||||||
/>
|
|
||||||
<span className="truncate text-12 font-medium">{option.data.label}</span>
|
|
||||||
</div>
|
|
||||||
{selected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MultiSelectDropdown
|
|
||||||
value={filters.priority ?? []}
|
|
||||||
onChange={(value) => void updateFilters(workspaceSlug, projectId, { priority: value.length > 0 ? value : undefined })}
|
|
||||||
options={priorityOptions}
|
|
||||||
keyExtractor={(option) => option.value}
|
|
||||||
queryArray={["label"]}
|
|
||||||
inputPlaceholder={t("external_contours_page.board.filters.search_priority")}
|
|
||||||
buttonContainerClassName="h-10"
|
|
||||||
optionsContainerClassName="w-56"
|
|
||||||
disableSearch={priorityOptions.length <= 7}
|
|
||||||
disabled={priorityOptions.length === 0 && (filters.priority?.length ?? 0) === 0}
|
|
||||||
buttonContent={(isOpen) => (
|
|
||||||
<FilterTrigger
|
|
||||||
icon={<PriorityIcon priority="medium" className="h-3.5 w-3.5" />}
|
|
||||||
label={t("priority")}
|
|
||||||
count={filters.priority?.length ?? 0}
|
|
||||||
isOpen={isOpen}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
renderItem={({ value, selected }) => {
|
|
||||||
const option = priorityOptions.find((item) => item.value === value);
|
|
||||||
if (!option) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-between gap-2 text-secondary">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<PriorityIcon priority={option.value} className="h-3.5 w-3.5" />
|
|
||||||
<span className="text-12 font-medium">{option.data.label}</span>
|
|
||||||
</div>
|
|
||||||
{selected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MultiSelectDropdown
|
|
||||||
value={filters.assignee_ids ?? []}
|
|
||||||
onChange={(value) =>
|
|
||||||
void updateFilters(workspaceSlug, projectId, { assignee_ids: value.length > 0 ? value : undefined })
|
|
||||||
}
|
|
||||||
options={assigneeOptions}
|
|
||||||
keyExtractor={(option) => option.value}
|
|
||||||
queryArray={["label"]}
|
|
||||||
inputPlaceholder={t("external_contours_page.board.filters.search_assignee")}
|
|
||||||
buttonContainerClassName="h-10"
|
|
||||||
optionsContainerClassName="w-64"
|
|
||||||
disabled={assigneeOptions.length === 0 && (filters.assignee_ids?.length ?? 0) === 0}
|
|
||||||
buttonContent={(isOpen) => (
|
|
||||||
<FilterTrigger label={t("assignee")} count={filters.assignee_ids?.length ?? 0} isOpen={isOpen} />
|
|
||||||
)}
|
|
||||||
renderItem={({ value, selected }) => {
|
|
||||||
const option = assigneeOptions.find((item) => item.value === value);
|
|
||||||
if (!option) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-between gap-2 text-secondary">
|
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
|
||||||
<Avatar src={option.data.avatarUrl || ""} name={option.data.label} size="sm" />
|
|
||||||
<span className="truncate text-12 font-medium">{option.data.label}</span>
|
|
||||||
</div>
|
|
||||||
{selected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MultiSelectDropdown
|
|
||||||
value={filters.requested_by_ids ?? []}
|
|
||||||
onChange={(value) =>
|
|
||||||
void updateFilters(workspaceSlug, projectId, { requested_by_ids: value.length > 0 ? value : undefined })
|
|
||||||
}
|
|
||||||
options={requesterOptions}
|
|
||||||
keyExtractor={(option) => option.value}
|
|
||||||
queryArray={["label"]}
|
|
||||||
inputPlaceholder={t("external_contours_page.board.filters.search_requester")}
|
|
||||||
buttonContainerClassName="h-10"
|
|
||||||
optionsContainerClassName="w-64"
|
|
||||||
disabled={requesterOptions.length === 0 && (filters.requested_by_ids?.length ?? 0) === 0}
|
|
||||||
buttonContent={(isOpen) => (
|
|
||||||
<FilterTrigger
|
|
||||||
label={t("external_contours_page.board.filters.requester")}
|
|
||||||
count={filters.requested_by_ids?.length ?? 0}
|
|
||||||
isOpen={isOpen}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
renderItem={({ value, selected }) => {
|
|
||||||
const option = requesterOptions.find((item) => item.value === value);
|
|
||||||
if (!option) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-between gap-2 text-secondary">
|
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
|
||||||
<Avatar src={option.data.avatarUrl || ""} name={option.data.label} size="sm" />
|
|
||||||
<span className="truncate text-12 font-medium">{option.data.label}</span>
|
|
||||||
</div>
|
|
||||||
{selected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cn(
|
|
||||||
"nodedc-toolbar-pill !min-h-10 !px-4 text-12 font-medium",
|
|
||||||
filters.has_unread_updates
|
|
||||||
? "!bg-[rgb(var(--nodedc-accent-rgb))] !text-[#0b1117]"
|
|
||||||
: "text-secondary"
|
|
||||||
)}
|
|
||||||
onClick={() =>
|
|
||||||
void updateFilters(workspaceSlug, projectId, {
|
|
||||||
has_unread_updates: filters.has_unread_updates ? undefined : true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="size-2 rounded-full bg-current" />
|
|
||||||
<span>{t("external_contours_page.board.filters.unread_only")}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{shouldShowClear && (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="lg"
|
|
||||||
className="nodedc-toolbar-pill !h-10 !rounded-full !px-4"
|
|
||||||
onClick={() => void clearFilters(workspaceSlug, projectId)}
|
|
||||||
>
|
|
||||||
{t("common.clear")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2 self-start xl:self-auto">
|
|
||||||
<div className="text-11 font-medium text-secondary">{t("external_contours_page.board.filters.sort")}</div>
|
|
||||||
<Dropdown
|
|
||||||
value={selectedSortingKey}
|
|
||||||
onChange={(value) => {
|
|
||||||
const nextSorting = sortingOptions.find((option) => option.key === value)?.value;
|
|
||||||
if (!nextSorting) return;
|
|
||||||
void updateSorting(workspaceSlug, projectId, nextSorting);
|
|
||||||
}}
|
|
||||||
options={sortingOptions.map((option) => ({
|
|
||||||
data: option,
|
|
||||||
value: option.key,
|
|
||||||
}))}
|
|
||||||
keyExtractor={(option) => option.value}
|
|
||||||
queryArray={["label"]}
|
|
||||||
disableSearch
|
|
||||||
buttonContainerClassName="h-10"
|
|
||||||
optionsContainerClassName="w-64"
|
|
||||||
buttonContent={(isOpen) => (
|
|
||||||
<FilterTrigger label={selectedSortingLabel} isOpen={isOpen} />
|
|
||||||
)}
|
|
||||||
renderItem={({ value, selected }) => {
|
|
||||||
const option = sortingOptions.find((item) => item.key === value);
|
|
||||||
if (!option) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-between gap-2 text-secondary">
|
|
||||||
<span className="text-12 font-medium">{option.label}</span>
|
|
||||||
{selected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
type TFilterTriggerProps = {
|
|
||||||
count?: number;
|
|
||||||
icon?: ReactNode;
|
|
||||||
isOpen: boolean;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function FilterTrigger(props: TFilterTriggerProps) {
|
|
||||||
const { count = 0, icon, isOpen, label } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-active={isOpen || count > 0}
|
|
||||||
className={cn(
|
|
||||||
"nodedc-toolbar-pill !min-h-10 gap-2 !px-4 text-12 font-medium",
|
|
||||||
isOpen || count > 0 ? "text-[rgb(var(--nodedc-accent-rgb))]" : "text-secondary"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
<span>{label}</span>
|
|
||||||
{count > 0 && <span className="rounded-full bg-accent-primary/15 px-1.5 py-0.5 text-[10px] font-semibold text-accent-primary">{count}</span>}
|
|
||||||
<ChevronDownIcon className={cn("h-3 w-3 flex-shrink-0 transition-transform", { "rotate-180": isOpen })} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortFilterOptions = (options: TFilterOption[]) => options.sort((left, right) => left.label.localeCompare(right.label));
|
|
||||||
|
|
||||||
const buildOptionsWithSelectedFallback = (
|
|
||||||
visibleRequests: TExternalContourRequest[],
|
|
||||||
cachedRequests: TExternalContourRequest[],
|
|
||||||
selectedIds: string[],
|
|
||||||
getOption: (request: TExternalContourRequest) => TFilterOption | null
|
|
||||||
) => {
|
|
||||||
const optionMap = new Map<string, TFilterOption>();
|
|
||||||
|
|
||||||
const upsertOption = (request: TExternalContourRequest) => {
|
|
||||||
const option = getOption(request);
|
|
||||||
if (!option?.id || optionMap.has(option.id)) return;
|
|
||||||
optionMap.set(option.id, option);
|
|
||||||
};
|
|
||||||
|
|
||||||
visibleRequests.forEach(upsertOption);
|
|
||||||
cachedRequests.forEach(upsertOption);
|
|
||||||
|
|
||||||
return sortFilterOptions(Array.from(optionMap.values())).map((option) => ({
|
|
||||||
data: option,
|
|
||||||
value: option.id,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCounterpartyProjectOptions = (
|
|
||||||
visibleRequests: TExternalContourRequest[],
|
|
||||||
cachedRequests: TExternalContourRequest[],
|
|
||||||
selectedIds: string[],
|
|
||||||
currentProjectId: string
|
|
||||||
) =>
|
|
||||||
buildOptionsWithSelectedFallback(visibleRequests, cachedRequests, selectedIds, (request) => {
|
|
||||||
const project = request.direction === "incoming" ? request.source_project : request.target_project;
|
|
||||||
const fallbackName = request.direction === "incoming" ? request.source_project_name : request.target_project_name;
|
|
||||||
|
|
||||||
if (!project?.id || project.id === currentProjectId) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: project.id,
|
|
||||||
label: project.name || project.identifier || fallbackName || "NODE.DC",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const getStateOptions = (
|
|
||||||
visibleRequests: TExternalContourRequest[],
|
|
||||||
cachedRequests: TExternalContourRequest[],
|
|
||||||
selectedIds: string[]
|
|
||||||
) =>
|
|
||||||
buildOptionsWithSelectedFallback(visibleRequests, cachedRequests, selectedIds, (request) => {
|
|
||||||
const state = request.issue.state_detail;
|
|
||||||
if (!state?.id) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: state.id,
|
|
||||||
label: state.name || "Без статуса",
|
|
||||||
color: state.color || null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const getPriorityOptions = (
|
|
||||||
visibleRequests: TExternalContourRequest[],
|
|
||||||
cachedRequests: TExternalContourRequest[],
|
|
||||||
selectedIds: string[],
|
|
||||||
t: ReturnType<typeof useTranslation>["t"]
|
|
||||||
) => {
|
|
||||||
const priorityKeys = new Set<string>();
|
|
||||||
|
|
||||||
[...visibleRequests, ...cachedRequests].forEach((request) => {
|
|
||||||
if (request.issue.priority && request.issue.priority !== "none") priorityKeys.add(request.issue.priority);
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedIds.forEach((priority) => {
|
|
||||||
if (priority && priority !== "none") priorityKeys.add(priority);
|
|
||||||
});
|
|
||||||
|
|
||||||
return ISSUE_PRIORITIES.filter((priority) => priority.key !== "none" && priorityKeys.has(priority.key))
|
|
||||||
.map((priority) => ({
|
|
||||||
data: {
|
|
||||||
id: priority.key,
|
|
||||||
label: t(priority.key),
|
|
||||||
},
|
|
||||||
value: priority.key,
|
|
||||||
}))
|
|
||||||
.sort((left, right) => left.data.label.localeCompare(right.data.label));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAssigneeOptions = (
|
|
||||||
visibleRequests: TExternalContourRequest[],
|
|
||||||
cachedRequests: TExternalContourRequest[],
|
|
||||||
selectedIds: string[]
|
|
||||||
) => {
|
|
||||||
const assigneeMap = new Map<string, TFilterOption>();
|
|
||||||
|
|
||||||
const upsertAssignees = (request: TExternalContourRequest) => {
|
|
||||||
request.issue.assignee_details?.forEach((assignee) => {
|
|
||||||
if (!assignee?.id || assigneeMap.has(assignee.id)) return;
|
|
||||||
assigneeMap.set(assignee.id, {
|
|
||||||
id: assignee.id,
|
|
||||||
label: assignee.display_name || "NODE.DC",
|
|
||||||
avatarUrl: assignee.avatar_url || "",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
visibleRequests.forEach(upsertAssignees);
|
|
||||||
cachedRequests.forEach(upsertAssignees);
|
|
||||||
|
|
||||||
return sortFilterOptions(Array.from(assigneeMap.values())).map((option) => ({ data: option, value: option.id }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRequesterOptions = (
|
|
||||||
visibleRequests: TExternalContourRequest[],
|
|
||||||
cachedRequests: TExternalContourRequest[],
|
|
||||||
selectedIds: string[]
|
|
||||||
) =>
|
|
||||||
buildOptionsWithSelectedFallback(visibleRequests, cachedRequests, selectedIds, (request) => {
|
|
||||||
const requesterId = request.requested_by?.id || request.requested_by_id || request.issue.created_by_detail?.id;
|
|
||||||
const requesterLabel =
|
|
||||||
request.requested_by?.display_name || request.requested_by_name || request.issue.created_by_detail?.display_name;
|
|
||||||
|
|
||||||
if (!requesterId || !requesterLabel) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: requesterId,
|
|
||||||
label: requesterLabel,
|
|
||||||
avatarUrl: request.issue.created_by_detail?.avatar_url || "",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { observer } from "mobx-react";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
import { useProjectExternalContoursBoard } from "@/hooks/store/use-project-external-contours-board";
|
import { useProjectExternalContoursBoard } from "@/hooks/store/use-project-external-contours-board";
|
||||||
import { ExternalContoursBoardFiltersRow } from "./board-filters-row";
|
|
||||||
import { ExternalContoursBoardColumn } from "./board-column";
|
import { ExternalContoursBoardColumn } from "./board-column";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -23,10 +22,6 @@ export const ExternalContoursBoardRoot = observer(function ExternalContoursBoard
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col overflow-hidden px-8 pb-6">
|
<div className="flex h-full min-h-0 flex-col overflow-hidden px-8 pb-6">
|
||||||
<div className="shrink-0 py-4">
|
|
||||||
<ExternalContoursBoardFiltersRow workspaceSlug={workspaceSlug} projectId={projectId} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loader === "init-loading" && !hasAnyItems ? (
|
{loader === "init-loading" && !hasAnyItems ? (
|
||||||
<div className="flex flex-1 items-center justify-center text-13 text-secondary">{t("loading")}...</div>
|
<div className="flex flex-1 items-center justify-center text-13 text-secondary">{t("loading")}...</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createContext, useContext, useEffect, useMemo } from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { isEqual } from "lodash-es";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { FilterInstance, workItemFiltersAdapter } from "@plane/shared-state";
|
||||||
|
import type { IFilterInstance } from "@plane/shared-state";
|
||||||
|
import type { TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||||
|
import { useProjectExternalContoursBoard } from "@/hooks/store/use-project-external-contours-board";
|
||||||
|
import { useExternalContoursFiltersConfig } from "./use-external-contours-filters-config";
|
||||||
|
import {
|
||||||
|
buildExternalContourBoardFilters,
|
||||||
|
buildExternalContourRichFilterExpression,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
const ExternalContoursFilterContext = createContext<
|
||||||
|
IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression> | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
projectId: string;
|
||||||
|
workspaceSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectExternalContoursFiltersProvider = observer(function ProjectExternalContoursFiltersProvider(props: Props) {
|
||||||
|
const { children, projectId, workspaceSlug } = props;
|
||||||
|
const { filters, items, replaceFilters } = useProjectExternalContoursBoard();
|
||||||
|
|
||||||
|
const requests = useMemo(() => Object.values(items), [items]);
|
||||||
|
const { areAllConfigsInitialized, configs } = useExternalContoursFiltersConfig({
|
||||||
|
projectId,
|
||||||
|
requests,
|
||||||
|
workspaceSlug,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filter = useMemo(
|
||||||
|
() =>
|
||||||
|
new FilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression>({
|
||||||
|
adapter: workItemFiltersAdapter,
|
||||||
|
initialExpression: buildExternalContourRichFilterExpression(filters),
|
||||||
|
onExpressionChange: (expression) => {
|
||||||
|
void replaceFilters(workspaceSlug, projectId, buildExternalContourBoardFilters(expression));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[projectId, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
filter.onExpressionChange = (expression) => {
|
||||||
|
void replaceFilters(workspaceSlug, projectId, buildExternalContourBoardFilters(expression));
|
||||||
|
};
|
||||||
|
}, [filter, projectId, replaceFilters, workspaceSlug]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
filter.configManager.setAreConfigsReady(areAllConfigsInitialized);
|
||||||
|
filter.configManager.registerAll(configs);
|
||||||
|
}, [areAllConfigsInitialized, configs, filter.configManager]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nextExpression = buildExternalContourRichFilterExpression(filters);
|
||||||
|
const currentExpression = workItemFiltersAdapter.toExternal(filter.expression);
|
||||||
|
|
||||||
|
if (isEqual(currentExpression, nextExpression)) return;
|
||||||
|
|
||||||
|
const onExpressionChange = filter.onExpressionChange;
|
||||||
|
filter.onExpressionChange = undefined;
|
||||||
|
filter.resetExpression(nextExpression);
|
||||||
|
filter.onExpressionChange = onExpressionChange;
|
||||||
|
}, [filter, filters]);
|
||||||
|
|
||||||
|
return <ExternalContoursFilterContext.Provider value={filter}>{children}</ExternalContoursFilterContext.Provider>;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useExternalContoursFilter = () => useContext(ExternalContoursFilterContext);
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { Briefcase } from "lucide-react";
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { Logo } from "@plane/propel/emoji-icon-picker";
|
||||||
|
import {
|
||||||
|
DueDatePropertyIcon,
|
||||||
|
LabelPropertyIcon,
|
||||||
|
MembersPropertyIcon,
|
||||||
|
PriorityIcon,
|
||||||
|
PriorityPropertyIcon,
|
||||||
|
StateGroupIcon,
|
||||||
|
StatePropertyIcon,
|
||||||
|
UserCirclePropertyIcon,
|
||||||
|
} from "@plane/propel/icons";
|
||||||
|
import type {
|
||||||
|
IProject,
|
||||||
|
IState,
|
||||||
|
IIssueLabel,
|
||||||
|
IUserLite,
|
||||||
|
TExternalContourRequest,
|
||||||
|
TFilterConfig,
|
||||||
|
TWorkItemFilterProperty,
|
||||||
|
} from "@plane/types";
|
||||||
|
import { Avatar } from "@plane/ui";
|
||||||
|
import {
|
||||||
|
getAssigneeFilterConfig,
|
||||||
|
getCreatedByFilterConfig,
|
||||||
|
getFileURL,
|
||||||
|
getLabelFilterConfig,
|
||||||
|
getPriorityFilterConfig,
|
||||||
|
getProjectFilterConfig,
|
||||||
|
getStateFilterConfig,
|
||||||
|
getStateGroupFilterConfig,
|
||||||
|
getTargetDateFilterConfig,
|
||||||
|
} from "@plane/utils";
|
||||||
|
import { useFiltersOperatorConfigs } from "@/plane-web/hooks/rich-filters/use-filters-operator-configs";
|
||||||
|
|
||||||
|
const sortByName = <T extends { name?: string | null; display_name?: string | null }>(items: T[]) =>
|
||||||
|
[...items].sort((left, right) =>
|
||||||
|
(left.display_name || left.name || "").localeCompare(right.display_name || right.name || "", "ru")
|
||||||
|
);
|
||||||
|
|
||||||
|
const buildCounterpartyProjects = (requests: TExternalContourRequest[], projectId: string): IProject[] => {
|
||||||
|
const projectMap = new Map<string, IProject>();
|
||||||
|
|
||||||
|
requests.forEach((request) => {
|
||||||
|
const project =
|
||||||
|
request.direction === "incoming"
|
||||||
|
? request.source_project
|
||||||
|
: request.target_project || request.issue.project_detail || null;
|
||||||
|
|
||||||
|
if (!project?.id || project.id === projectId || projectMap.has(project.id)) return;
|
||||||
|
|
||||||
|
projectMap.set(
|
||||||
|
project.id,
|
||||||
|
{
|
||||||
|
id: project.id,
|
||||||
|
name: project.name,
|
||||||
|
logo_props: project.logo_props,
|
||||||
|
} as IProject
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortByName(Array.from(projectMap.values()));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildStates = (requests: TExternalContourRequest[]): IState[] => {
|
||||||
|
const stateMap = new Map<string, IState>();
|
||||||
|
|
||||||
|
requests.forEach((request, index) => {
|
||||||
|
const state = request.issue.state_detail;
|
||||||
|
if (!state?.id || stateMap.has(state.id)) return;
|
||||||
|
|
||||||
|
stateMap.set(
|
||||||
|
state.id,
|
||||||
|
{
|
||||||
|
id: state.id,
|
||||||
|
color: state.color,
|
||||||
|
default: false,
|
||||||
|
description: "",
|
||||||
|
group: state.group,
|
||||||
|
name: state.name,
|
||||||
|
order: index + 1,
|
||||||
|
project_id: request.issue.project_id || request.target_project?.id || request.source_project?.id || "",
|
||||||
|
sequence: index + 1,
|
||||||
|
workspace_id: "",
|
||||||
|
} as IState
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortByName(Array.from(stateMap.values()));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildLabels = (requests: TExternalContourRequest[]): IIssueLabel[] => {
|
||||||
|
const labelMap = new Map<string, IIssueLabel>();
|
||||||
|
|
||||||
|
requests.forEach((request) => {
|
||||||
|
request.issue.label_details?.forEach((label) => {
|
||||||
|
if (!label.id || labelMap.has(label.id)) return;
|
||||||
|
labelMap.set(
|
||||||
|
label.id,
|
||||||
|
{
|
||||||
|
id: label.id,
|
||||||
|
color: label.color,
|
||||||
|
name: label.name,
|
||||||
|
parent: null,
|
||||||
|
project_id: request.issue.project_id || "",
|
||||||
|
sort_order: 0,
|
||||||
|
workspace_id: "",
|
||||||
|
} as IIssueLabel
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortByName(Array.from(labelMap.values()));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildAssignees = (requests: TExternalContourRequest[]): IUserLite[] => {
|
||||||
|
const memberMap = new Map<string, IUserLite>();
|
||||||
|
|
||||||
|
requests.forEach((request) => {
|
||||||
|
request.issue.assignee_details?.forEach((assignee) => {
|
||||||
|
if (!assignee.id || memberMap.has(assignee.id)) return;
|
||||||
|
memberMap.set(
|
||||||
|
assignee.id,
|
||||||
|
{
|
||||||
|
id: assignee.id,
|
||||||
|
avatar_url: assignee.avatar_url,
|
||||||
|
display_name: assignee.display_name,
|
||||||
|
} as IUserLite
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortByName(Array.from(memberMap.values()));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildRequesters = (requests: TExternalContourRequest[]): IUserLite[] => {
|
||||||
|
const memberMap = new Map<string, IUserLite>();
|
||||||
|
|
||||||
|
requests.forEach((request) => {
|
||||||
|
const requesterId = request.requested_by?.id || request.requested_by_id || request.issue.created_by_detail?.id;
|
||||||
|
const requesterName =
|
||||||
|
request.requested_by?.display_name || request.requested_by_name || request.issue.created_by_detail?.display_name;
|
||||||
|
|
||||||
|
if (!requesterId || !requesterName || memberMap.has(requesterId)) return;
|
||||||
|
|
||||||
|
memberMap.set(
|
||||||
|
requesterId,
|
||||||
|
{
|
||||||
|
id: requesterId,
|
||||||
|
avatar_url: request.issue.created_by_detail?.avatar_url,
|
||||||
|
display_name: requesterName,
|
||||||
|
} as IUserLite
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortByName(Array.from(memberMap.values()));
|
||||||
|
};
|
||||||
|
|
||||||
|
type TUseExternalContoursFiltersConfigProps = {
|
||||||
|
projectId: string;
|
||||||
|
requests: TExternalContourRequest[];
|
||||||
|
workspaceSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TExternalContoursFiltersConfig = {
|
||||||
|
areAllConfigsInitialized: boolean;
|
||||||
|
configs: TFilterConfig<TWorkItemFilterProperty>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useExternalContoursFiltersConfig = (
|
||||||
|
props: TUseExternalContoursFiltersConfigProps
|
||||||
|
): TExternalContoursFiltersConfig => {
|
||||||
|
const { projectId, requests, workspaceSlug } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const operatorConfigs = useFiltersOperatorConfigs({ workspaceSlug });
|
||||||
|
|
||||||
|
const counterpartyProjects = useMemo(() => buildCounterpartyProjects(requests, projectId), [projectId, requests]);
|
||||||
|
const states = useMemo(() => buildStates(requests), [requests]);
|
||||||
|
const labels = useMemo(() => buildLabels(requests), [requests]);
|
||||||
|
const assignees = useMemo(() => buildAssignees(requests), [requests]);
|
||||||
|
const requesters = useMemo(() => buildRequesters(requests), [requests]);
|
||||||
|
|
||||||
|
const configs = useMemo(
|
||||||
|
() => [
|
||||||
|
getProjectFilterConfig<TWorkItemFilterProperty>("project_id")({
|
||||||
|
isEnabled: true,
|
||||||
|
filterLabel: "Contour",
|
||||||
|
filterIcon: Briefcase,
|
||||||
|
projects: counterpartyProjects,
|
||||||
|
getOptionIcon: (project) => <Logo logo={project.logo_props} size={12} />,
|
||||||
|
...operatorConfigs,
|
||||||
|
}),
|
||||||
|
getStateGroupFilterConfig<TWorkItemFilterProperty>("state_group")({
|
||||||
|
isEnabled: true,
|
||||||
|
filterIcon: StatePropertyIcon,
|
||||||
|
getItemLabel: (stateGroupKey) => t(`workspace_projects.state.${stateGroupKey}`),
|
||||||
|
getOptionIcon: (stateGroupKey) => <StateGroupIcon stateGroup={stateGroupKey} />,
|
||||||
|
...operatorConfigs,
|
||||||
|
}),
|
||||||
|
getStateFilterConfig<TWorkItemFilterProperty>("state_id")({
|
||||||
|
isEnabled: true,
|
||||||
|
filterIcon: StatePropertyIcon,
|
||||||
|
getOptionIcon: (state) => <StateGroupIcon stateGroup={state.group} color={state.color} />,
|
||||||
|
states,
|
||||||
|
...operatorConfigs,
|
||||||
|
}),
|
||||||
|
getPriorityFilterConfig<TWorkItemFilterProperty>("priority")({
|
||||||
|
isEnabled: true,
|
||||||
|
filterLabel: t("common.priority"),
|
||||||
|
filterIcon: PriorityPropertyIcon,
|
||||||
|
getItemLabel: (priorityKey) => (priorityKey === "none" ? t("common.none") : t(priorityKey)),
|
||||||
|
getOptionIcon: (priority) => <PriorityIcon priority={priority} />,
|
||||||
|
...operatorConfigs,
|
||||||
|
}),
|
||||||
|
getAssigneeFilterConfig<TWorkItemFilterProperty>("assignee_id")({
|
||||||
|
isEnabled: true,
|
||||||
|
filterIcon: MembersPropertyIcon,
|
||||||
|
members: assignees,
|
||||||
|
getOptionIcon: (memberDetails) => (
|
||||||
|
<Avatar
|
||||||
|
name={memberDetails.display_name}
|
||||||
|
src={getFileURL(memberDetails.avatar_url)}
|
||||||
|
showTooltip={false}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
...operatorConfigs,
|
||||||
|
}),
|
||||||
|
getCreatedByFilterConfig<TWorkItemFilterProperty>("created_by_id")({
|
||||||
|
isEnabled: true,
|
||||||
|
filterIcon: UserCirclePropertyIcon,
|
||||||
|
members: requesters,
|
||||||
|
getOptionIcon: (memberDetails) => (
|
||||||
|
<Avatar
|
||||||
|
name={memberDetails.display_name}
|
||||||
|
src={getFileURL(memberDetails.avatar_url)}
|
||||||
|
showTooltip={false}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
...operatorConfigs,
|
||||||
|
}),
|
||||||
|
getLabelFilterConfig<TWorkItemFilterProperty>("label_id")({
|
||||||
|
isEnabled: true,
|
||||||
|
filterIcon: LabelPropertyIcon,
|
||||||
|
labels,
|
||||||
|
getOptionIcon: (color) => (
|
||||||
|
<span className="flex size-2.5 flex-shrink-0 rounded-full" style={{ backgroundColor: color }} />
|
||||||
|
),
|
||||||
|
...operatorConfigs,
|
||||||
|
}),
|
||||||
|
getTargetDateFilterConfig<TWorkItemFilterProperty>("target_date")({
|
||||||
|
isEnabled: true,
|
||||||
|
filterIcon: DueDatePropertyIcon,
|
||||||
|
...operatorConfigs,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[assignees, counterpartyProjects, labels, operatorConfigs, requesters, states, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
areAllConfigsInitialized: true,
|
||||||
|
configs,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildWorkItemFilterExpressionFromConditions,
|
||||||
|
workItemFiltersAdapter,
|
||||||
|
} from "@plane/shared-state";
|
||||||
|
import type { TWorkItemFilterCondition } from "@plane/shared-state";
|
||||||
|
import type {
|
||||||
|
TExternalContourBoardFilter,
|
||||||
|
TFilterValue,
|
||||||
|
TWorkItemFilterExpression,
|
||||||
|
TWorkItemFilterProperty,
|
||||||
|
} from "@plane/types";
|
||||||
|
import { COLLECTION_OPERATOR, COMPARISON_OPERATOR, EQUALITY_OPERATOR } from "@plane/types";
|
||||||
|
import { extractConditionsWithDisplayOperators, renderFormattedPayloadDate } from "@plane/utils";
|
||||||
|
|
||||||
|
export const EXTERNAL_CONTOUR_FILTER_PROPERTIES = [
|
||||||
|
"project_id",
|
||||||
|
"state_group",
|
||||||
|
"state_id",
|
||||||
|
"priority",
|
||||||
|
"assignee_id",
|
||||||
|
"created_by_id",
|
||||||
|
"label_id",
|
||||||
|
"target_date",
|
||||||
|
] as const satisfies TWorkItemFilterProperty[];
|
||||||
|
|
||||||
|
const toStringArray = (value: TFilterValue | TFilterValue[] | undefined): string[] => {
|
||||||
|
if (value === undefined || value === null || value === "") return [];
|
||||||
|
return (Array.isArray(value) ? value : [value]).map((item) => String(item)).filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toPayloadDate = (value: TFilterValue | undefined): string | undefined => {
|
||||||
|
if (value === undefined || value === null || value === "") return undefined;
|
||||||
|
return renderFormattedPayloadDate(value instanceof Date ? value : String(value)) ?? undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildExternalContourRichFilterExpression = (
|
||||||
|
filters: Partial<TExternalContourBoardFilter>
|
||||||
|
): TWorkItemFilterExpression => {
|
||||||
|
const conditions: TWorkItemFilterCondition[] = [];
|
||||||
|
|
||||||
|
if (filters.counterparty_project_ids?.length) {
|
||||||
|
conditions.push({
|
||||||
|
property: "project_id",
|
||||||
|
operator: COLLECTION_OPERATOR.IN,
|
||||||
|
value: filters.counterparty_project_ids,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.state_groups?.length) {
|
||||||
|
conditions.push({
|
||||||
|
property: "state_group",
|
||||||
|
operator: COLLECTION_OPERATOR.IN,
|
||||||
|
value: filters.state_groups,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.state_ids?.length) {
|
||||||
|
conditions.push({
|
||||||
|
property: "state_id",
|
||||||
|
operator: COLLECTION_OPERATOR.IN,
|
||||||
|
value: filters.state_ids,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.priority?.length) {
|
||||||
|
conditions.push({
|
||||||
|
property: "priority",
|
||||||
|
operator: COLLECTION_OPERATOR.IN,
|
||||||
|
value: filters.priority,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.assignee_ids?.length) {
|
||||||
|
conditions.push({
|
||||||
|
property: "assignee_id",
|
||||||
|
operator: COLLECTION_OPERATOR.IN,
|
||||||
|
value: filters.assignee_ids,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const authorIds = filters.requested_by_ids?.length ? filters.requested_by_ids : filters.created_by_ids;
|
||||||
|
if (authorIds?.length) {
|
||||||
|
conditions.push({
|
||||||
|
property: "created_by_id",
|
||||||
|
operator: COLLECTION_OPERATOR.IN,
|
||||||
|
value: authorIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.label_ids?.length) {
|
||||||
|
conditions.push({
|
||||||
|
property: "label_id",
|
||||||
|
operator: COLLECTION_OPERATOR.IN,
|
||||||
|
value: filters.label_ids,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.target_date_exact) {
|
||||||
|
conditions.push({
|
||||||
|
property: "target_date",
|
||||||
|
operator: EQUALITY_OPERATOR.EXACT,
|
||||||
|
value: filters.target_date_exact,
|
||||||
|
});
|
||||||
|
} else if (filters.target_date_from && filters.target_date_to) {
|
||||||
|
conditions.push({
|
||||||
|
property: "target_date",
|
||||||
|
operator: COMPARISON_OPERATOR.RANGE,
|
||||||
|
value: [filters.target_date_from, filters.target_date_to],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildWorkItemFilterExpressionFromConditions({ conditions }) ?? {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildExternalContourBoardFilters = (
|
||||||
|
expression: TWorkItemFilterExpression
|
||||||
|
): Partial<TExternalContourBoardFilter> => {
|
||||||
|
const internalExpression = workItemFiltersAdapter.toInternal(expression);
|
||||||
|
if (!internalExpression) return {};
|
||||||
|
|
||||||
|
const filters: Partial<TExternalContourBoardFilter> = {};
|
||||||
|
const conditions = extractConditionsWithDisplayOperators(internalExpression);
|
||||||
|
|
||||||
|
conditions.forEach((condition) => {
|
||||||
|
switch (condition.property) {
|
||||||
|
case "project_id":
|
||||||
|
filters.counterparty_project_ids = toStringArray(condition.value);
|
||||||
|
break;
|
||||||
|
case "state_group":
|
||||||
|
filters.state_groups = toStringArray(condition.value);
|
||||||
|
break;
|
||||||
|
case "state_id":
|
||||||
|
filters.state_ids = toStringArray(condition.value);
|
||||||
|
break;
|
||||||
|
case "priority":
|
||||||
|
filters.priority = toStringArray(condition.value);
|
||||||
|
break;
|
||||||
|
case "assignee_id":
|
||||||
|
filters.assignee_ids = toStringArray(condition.value);
|
||||||
|
break;
|
||||||
|
case "created_by_id":
|
||||||
|
filters.requested_by_ids = toStringArray(condition.value);
|
||||||
|
break;
|
||||||
|
case "label_id":
|
||||||
|
filters.label_ids = toStringArray(condition.value);
|
||||||
|
break;
|
||||||
|
case "target_date":
|
||||||
|
if (condition.operator === COMPARISON_OPERATOR.RANGE) {
|
||||||
|
const [from, to] = toStringArray(condition.value);
|
||||||
|
filters.target_date_from = toPayloadDate(from);
|
||||||
|
filters.target_date_to = toPayloadDate(to);
|
||||||
|
} else {
|
||||||
|
filters.target_date_exact = toPayloadDate(
|
||||||
|
Array.isArray(condition.value) ? condition.value[0] : condition.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filters;
|
||||||
|
};
|
||||||
|
|
@ -14,10 +14,12 @@ import { TransferIcon } from "@plane/propel/icons";
|
||||||
import { Breadcrumbs, Header } from "@plane/ui";
|
import { Breadcrumbs, Header } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||||
import { AppHeaderPrimaryActionButton } from "@/components/core/app-header/primary-action-button";
|
import { AppHeaderPrimaryActionButton } from "@/components/core/app-header/primary-action-button";
|
||||||
|
import { FiltersToggle } from "@/components/rich-filters/filters-toggle";
|
||||||
import { useProject } from "@/hooks/store/use-project";
|
import { useProject } from "@/hooks/store/use-project";
|
||||||
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
|
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
|
||||||
import { useUserPermissions } from "@/hooks/store/user";
|
import { useUserPermissions } from "@/hooks/store/user";
|
||||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
||||||
|
import { useExternalContoursFilter } from "./filters/provider";
|
||||||
import { ExternalContourCreateModalRoot } from "./create-modal";
|
import { ExternalContourCreateModalRoot } from "./create-modal";
|
||||||
|
|
||||||
export const ProjectExternalContoursHeader = observer(function ProjectExternalContoursHeader() {
|
export const ProjectExternalContoursHeader = observer(function ProjectExternalContoursHeader() {
|
||||||
|
|
@ -27,6 +29,7 @@ export const ProjectExternalContoursHeader = observer(function ProjectExternalCo
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
const { loader: currentProjectDetailsLoader } = useProject();
|
const { loader: currentProjectDetailsLoader } = useProject();
|
||||||
const { loader } = useProjectExternalContours();
|
const { loader } = useProjectExternalContours();
|
||||||
|
const filter = useExternalContoursFilter();
|
||||||
|
|
||||||
const isAuthorized = allowPermissions(
|
const isAuthorized = allowPermissions(
|
||||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
[EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||||
|
|
@ -63,6 +66,9 @@ export const ProjectExternalContoursHeader = observer(function ProjectExternalCo
|
||||||
<Header.RightItem>
|
<Header.RightItem>
|
||||||
{workspaceSlug && projectId && isAuthorized ? (
|
{workspaceSlug && projectId && isAuthorized ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="nodedc-top-toolbar-cluster flex items-center gap-2">
|
||||||
|
<FiltersToggle filter={filter} showAddFilterButtonWhenEmpty={false} />
|
||||||
|
</div>
|
||||||
<ExternalContourCreateModalRoot
|
<ExternalContourCreateModalRoot
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ import { observer } from "mobx-react";
|
||||||
import { TransferIcon } from "@plane/propel/icons";
|
import { TransferIcon } from "@plane/propel/icons";
|
||||||
import type { TInboxIssueCurrentTab } from "@plane/types";
|
import type { TInboxIssueCurrentTab } from "@plane/types";
|
||||||
import { EInboxIssueCurrentTab } from "@plane/types";
|
import { EInboxIssueCurrentTab } from "@plane/types";
|
||||||
|
import { FiltersRow } from "@/components/rich-filters/filters-row";
|
||||||
import { useProjectExternalContoursBoard } from "@/hooks/store/use-project-external-contours-board";
|
import { useProjectExternalContoursBoard } from "@/hooks/store/use-project-external-contours-board";
|
||||||
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
|
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
|
||||||
import { ExternalContoursBoardRoot } from "./board-root";
|
import { ExternalContoursBoardRoot } from "./board-root";
|
||||||
import { ExternalContoursContentRoot } from "./content-root";
|
import { ExternalContoursContentRoot } from "./content-root";
|
||||||
|
import { useExternalContoursFilter } from "./filters/provider";
|
||||||
|
|
||||||
type TExternalContoursRoot = {
|
type TExternalContoursRoot = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
|
@ -31,6 +33,7 @@ export const ExternalContoursRoot = observer(function ExternalContoursRoot(props
|
||||||
fetchBoard,
|
fetchBoard,
|
||||||
loader: boardLoader,
|
loader: boardLoader,
|
||||||
} = useProjectExternalContoursBoard();
|
} = useProjectExternalContoursBoard();
|
||||||
|
const filter = useExternalContoursFilter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
@ -84,7 +87,10 @@ export const ExternalContoursRoot = observer(function ExternalContoursRoot(props
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full overflow-hidden bg-surface-1 pt-2">
|
<div className="flex h-full w-full flex-col overflow-hidden bg-surface-1">
|
||||||
|
{filter && <FiltersRow filter={filter} />}
|
||||||
|
|
||||||
|
<div className="flex min-h-0 flex-1 overflow-hidden pt-2">
|
||||||
<ExternalContoursBoardRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
<ExternalContoursBoardRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||||
{inboxIssueId && (
|
{inboxIssueId && (
|
||||||
<ExternalContoursContentRoot
|
<ExternalContoursContentRoot
|
||||||
|
|
@ -94,5 +100,6 @@ export const ExternalContoursRoot = observer(function ExternalContoursRoot(props
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { AddFilterButton } from "@/components/rich-filters/add-filters/button";
|
||||||
|
|
||||||
type TFiltersToggleProps<P extends TFilterProperty, E extends TExternalFilter> = {
|
type TFiltersToggleProps<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||||
filter: IFilterInstance<P, E> | undefined;
|
filter: IFilterInstance<P, E> | undefined;
|
||||||
|
showAddFilterButtonWhenEmpty?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const COMMON_CLASSNAME =
|
const COMMON_CLASSNAME =
|
||||||
|
|
@ -24,13 +25,13 @@ const COMMON_CLASSNAME =
|
||||||
export const FiltersToggle = observer(function FiltersToggle<P extends TFilterProperty, E extends TExternalFilter>(
|
export const FiltersToggle = observer(function FiltersToggle<P extends TFilterProperty, E extends TExternalFilter>(
|
||||||
props: TFiltersToggleProps<P, E>
|
props: TFiltersToggleProps<P, E>
|
||||||
) {
|
) {
|
||||||
const { filter } = props;
|
const { filter, showAddFilterButtonWhenEmpty = true } = props;
|
||||||
// derived values
|
// derived values
|
||||||
const hasAnyConditions = (filter?.allConditionsForDisplay.length ?? 0) > 0;
|
const hasAnyConditions = (filter?.allConditionsForDisplay.length ?? 0) > 0;
|
||||||
const isFilterRowVisible = filter?.isVisible ?? false;
|
const isFilterRowVisible = filter?.isVisible ?? false;
|
||||||
const hasUpdates = filter?.canUpdateView === true && filter?.hasChanges === true;
|
const hasUpdates = filter?.canUpdateView === true && filter?.hasChanges === true;
|
||||||
const showFilterRowChangesPill = hasUpdates || hasAnyConditions === true;
|
const showFilterRowChangesPill = hasUpdates || hasAnyConditions === true;
|
||||||
const showAddFilterButton = !hasAnyConditions && !isFilterRowVisible && !hasUpdates;
|
const showAddFilterButton = showAddFilterButtonWhenEmpty && !hasAnyConditions && !isFilterRowVisible && !hasUpdates;
|
||||||
|
|
||||||
const handleToggleFilter = () => {
|
const handleToggleFilter = () => {
|
||||||
if (!filter) {
|
if (!filter) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ const FILTER_LABEL_MAP: Record<string, string> = {
|
||||||
Label: "Метка",
|
Label: "Метка",
|
||||||
Labels: "Метки",
|
Labels: "Метки",
|
||||||
Project: "Проект",
|
Project: "Проект",
|
||||||
|
Projects: "Проекты",
|
||||||
|
Contour: "Контур",
|
||||||
"Created by": "Автор",
|
"Created by": "Автор",
|
||||||
"Created at": "Дата создания",
|
"Created at": "Дата создания",
|
||||||
"Updated at": "Дата обновления",
|
"Updated at": "Дата обновления",
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,5 @@ export const WorkItemFiltersToggle = observer(function WorkItemFiltersToggle(pro
|
||||||
// derived values
|
// derived values
|
||||||
const filter = getFilter(entityType, entityId);
|
const filter = getFilter(entityType, entityId);
|
||||||
|
|
||||||
return <FiltersToggle filter={filter} />;
|
return <FiltersToggle filter={filter} showAddFilterButtonWhenEmpty={false} />;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ export interface IProjectExternalContoursBoardStore {
|
||||||
isFiltering: boolean;
|
isFiltering: boolean;
|
||||||
isSortingDefault: boolean;
|
isSortingDefault: boolean;
|
||||||
clearFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
|
clearFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||||
|
replaceFilters: (workspaceSlug: string, projectId: string, filters: Partial<TExternalContourBoardFilter>) => Promise<void>;
|
||||||
updateFilters: (workspaceSlug: string, projectId: string, filters: Partial<TExternalContourBoardFilter>) => Promise<void>;
|
updateFilters: (workspaceSlug: string, projectId: string, filters: Partial<TExternalContourBoardFilter>) => Promise<void>;
|
||||||
updateSorting: (workspaceSlug: string, projectId: string, sorting: TExternalContourBoardSorting) => Promise<void>;
|
updateSorting: (workspaceSlug: string, projectId: string, sorting: TExternalContourBoardSorting) => Promise<void>;
|
||||||
upsertBoardItems: (items: TExternalContourRequest[]) => void;
|
upsertBoardItems: (items: TExternalContourRequest[]) => void;
|
||||||
|
|
@ -99,6 +100,7 @@ export class ProjectExternalContoursBoardStore implements IProjectExternalContou
|
||||||
clearFilters: action,
|
clearFilters: action,
|
||||||
fetchBoard: action,
|
fetchBoard: action,
|
||||||
handleCurrentTab: action,
|
handleCurrentTab: action,
|
||||||
|
replaceFilters: action,
|
||||||
updateFilters: action,
|
updateFilters: action,
|
||||||
updateSorting: action,
|
updateSorting: action,
|
||||||
upsertBoardItems: action,
|
upsertBoardItems: action,
|
||||||
|
|
@ -161,6 +163,15 @@ export class ProjectExternalContoursBoardStore implements IProjectExternalContou
|
||||||
await this.fetchBoard(workspaceSlug, projectId);
|
await this.fetchBoard(workspaceSlug, projectId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
replaceFilters = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
filters: Partial<TExternalContourBoardFilter>
|
||||||
|
) => {
|
||||||
|
this.filters = sanitizeBoardFilters(filters);
|
||||||
|
await this.fetchBoard(workspaceSlug, projectId);
|
||||||
|
};
|
||||||
|
|
||||||
updateSorting = async (workspaceSlug: string, projectId: string, sorting: TExternalContourBoardSorting) => {
|
updateSorting = async (workspaceSlug: string, projectId: string, sorting: TExternalContourBoardSorting) => {
|
||||||
this.sorting = sorting;
|
this.sorting = sorting;
|
||||||
await this.fetchBoard(workspaceSlug, projectId);
|
await this.fetchBoard(workspaceSlug, projectId);
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ export type TExternalContourRequestResponse = TPaginationInfo & {
|
||||||
export type TExternalContourBoardFilter = {
|
export type TExternalContourBoardFilter = {
|
||||||
direction?: TExternalContourBoardDirection[];
|
direction?: TExternalContourBoardDirection[];
|
||||||
status?: TExternalContourRequest["status"] | TExternalContourRequest["status"][];
|
status?: TExternalContourRequest["status"] | TExternalContourRequest["status"][];
|
||||||
|
state_groups?: string[];
|
||||||
state_ids?: string[];
|
state_ids?: string[];
|
||||||
priority?: string[];
|
priority?: string[];
|
||||||
assignee_ids?: string[];
|
assignee_ids?: string[];
|
||||||
|
|
@ -114,6 +115,9 @@ export type TExternalContourBoardFilter = {
|
||||||
source_project_ids?: string[];
|
source_project_ids?: string[];
|
||||||
target_project_ids?: string[];
|
target_project_ids?: string[];
|
||||||
label_ids?: string[];
|
label_ids?: string[];
|
||||||
|
target_date_exact?: string;
|
||||||
|
target_date_from?: string;
|
||||||
|
target_date_to?: string;
|
||||||
has_unread_updates?: boolean;
|
has_unread_updates?: boolean;
|
||||||
search?: string;
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export const getProjectFilterConfig =
|
||||||
(params: TCreateProjectFilterParams) =>
|
(params: TCreateProjectFilterParams) =>
|
||||||
createFilterConfig<P>({
|
createFilterConfig<P>({
|
||||||
id: key,
|
id: key,
|
||||||
label: "Projects",
|
label: params.filterLabel ?? "Projects",
|
||||||
...params,
|
...params,
|
||||||
icon: params.filterIcon,
|
icon: params.filterIcon,
|
||||||
supportedOperatorConfigsMap: new Map([
|
supportedOperatorConfigsMap: new Map([
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export const getSupportedDateOperators = (params: TCreateDateFilterParams): TOpe
|
||||||
*/
|
*/
|
||||||
export type TCreateProjectFilterParams = TCreateFilterConfigParams &
|
export type TCreateProjectFilterParams = TCreateFilterConfigParams &
|
||||||
IFilterIconConfig<IProject> & {
|
IFilterIconConfig<IProject> & {
|
||||||
|
filterLabel?: string;
|
||||||
projects: IProject[];
|
projects: IProject[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue