[WEB-1255] feat: filters for project views and changes required for advanced views (#4949)
* View with filters and changes required for advanced views * minor refactoring of views * minor name change
This commit is contained in:
parent
adaf3b15de
commit
82661589fb
52 changed files with 922 additions and 146 deletions
|
|
@ -6,8 +6,8 @@ import { LogoSpinner } from "@/components/common";
|
|||
import { WorkspaceLogo } from "@/components/workspace/logo";
|
||||
// helpers
|
||||
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
type TAuthHeader = {
|
||||
workspaceSlug: string | undefined;
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
|||
import useDebounce from "@/hooks/use-debounce";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import { IssueService } from "@/services/issue";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
// ui
|
||||
// components
|
||||
// types
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { observer } from "mobx-react";
|
||||
// icons
|
||||
import { X } from "lucide-react";
|
||||
// helpers
|
||||
// constants
|
||||
import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { capitalizeFirstLetter } from "@/helpers/string.helper";
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
editable: boolean | undefined;
|
||||
2
web/core/components/common/applied-filters/index.ts
Normal file
2
web/core/components/common/applied-filters/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./date";
|
||||
export * from "./members";
|
||||
2
web/core/components/common/filters/index.ts
Normal file
2
web/core/components/common/filters/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./created-at";
|
||||
export * from "./created-by";
|
||||
|
|
@ -11,7 +11,7 @@ import { MarkdownRenderer } from "@/components/ui";
|
|||
// icons
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { IGithubRepoCollaborator } from "@plane/types";
|
|||
// services
|
||||
import { Avatar, CustomSelect, CustomSearchSelect, Input } from "@plane/ui";
|
||||
import { WORKSPACE_MEMBERS } from "@/constants/fetch-keys";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// ui
|
||||
// types
|
||||
import { IUserDetails } from "./root";
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { IJiraImporterForm } from "@plane/types";
|
|||
// services
|
||||
import { Avatar, CustomSelect, CustomSearchSelect, Input, ToggleSwitch } from "@plane/ui";
|
||||
import { WORKSPACE_MEMBERS } from "@/constants/fetch-keys";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// ui
|
||||
// types
|
||||
// fetch keys
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { UpdateViewComponent } from "@/components/views/update-view-component";
|
|||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useProjectView, useUser } from "@/hooks/store";
|
||||
import { useIssues, useLabel, useProjectState, useProjectView, useUser } from "@/hooks/store";
|
||||
import { getAreFiltersEqual } from "../../../utils";
|
||||
|
||||
export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||
|
|
@ -26,6 +26,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
||||
const { projectLabels } = useLabel();
|
||||
const { projectStates } = useProjectState();
|
||||
const { viewMap, updateView } = useProjectView();
|
||||
const {
|
||||
data,
|
||||
|
|
@ -129,6 +130,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
disableEditing={isLocked}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "@/constants/workspace";
|
|||
// hooks
|
||||
import { useEventTracker, useUserProfile, useUserSettings, useWorkspace } from "@/hooks/store";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
|
||||
type Props = {
|
||||
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { getUserRole } from "@/helpers/user.helper";
|
|||
// hooks
|
||||
import { useEventTracker, useWorkspace } from "@/hooks/store";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
|
||||
type Props = {
|
||||
handleNextStep: () => Promise<void>;
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ import { getUserRole } from "@/helpers/user.helper";
|
|||
import { useEventTracker } from "@/hooks/store";
|
||||
import useDynamicDropdownPosition from "@/hooks/use-dynamic-dropdown";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// assets
|
||||
import InviteMembersDark from "@/public/onboarding/invite-members-dark.svg";
|
||||
import InviteMembersLight from "@/public/onboarding/invite-members-light.svg";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
// components
|
||||
import { OnboardingHeader } from "./header";
|
||||
import { SwitchAccountDropdown } from "./switch-account-dropdown";
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
export * from "./date";
|
||||
export * from "./members";
|
||||
export * from "./root";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { X } from "lucide-react";
|
||||
import { TPageFilterProps } from "@plane/types";
|
||||
// components
|
||||
import { AppliedDateFilters, AppliedMembersFilters } from "@/components/pages";
|
||||
import { AppliedDateFilters, AppliedMembersFilters } from "@/components/common/applied-filters";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper";
|
||||
// types
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
export * from "./created-at";
|
||||
export * from "./created-by";
|
||||
export * from "./root";
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { observer } from "mobx-react";
|
|||
import { Search, X } from "lucide-react";
|
||||
import { TPageFilterProps, TPageFilters } from "@plane/types";
|
||||
// components
|
||||
import { FilterCreatedBy, FilterCreatedDate } from "@/components/common/filters";
|
||||
import { FilterOption } from "@/components/issues";
|
||||
import { FilterCreatedBy, FilterCreatedDate } from "@/components/pages";
|
||||
|
||||
type Props = {
|
||||
filters: TPageFilters;
|
||||
|
|
|
|||
81
web/core/components/views/filters/applied-filters.tsx
Normal file
81
web/core/components/views/filters/applied-filters.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { X } from "lucide-react";
|
||||
import { TViewFilterProps } from "@plane/types";
|
||||
// components
|
||||
import { AppliedDateFilters, AppliedMembersFilters } from "@/components/common/applied-filters";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
appliedFilters: TViewFilterProps;
|
||||
handleClearAllFilters: () => void;
|
||||
handleRemoveFilter: (key: keyof TViewFilterProps, value: string | null) => void;
|
||||
alwaysAllowEditing?: boolean;
|
||||
};
|
||||
|
||||
const MEMBERS_FILTERS = ["owned_by"];
|
||||
const DATE_FILTERS = ["created_at"];
|
||||
|
||||
export const ViewAppliedFiltersList: React.FC<Props> = (props) => {
|
||||
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, alwaysAllowEditing } = props;
|
||||
|
||||
if (!appliedFilters) return null;
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
const isEditingAllowed = alwaysAllowEditing;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
|
||||
{Object.entries(appliedFilters).map(([key, value]) => {
|
||||
const filterKey = key as keyof TViewFilterProps;
|
||||
|
||||
if (!value) return;
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={filterKey}
|
||||
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span>
|
||||
{DATE_FILTERS.includes(filterKey) && (
|
||||
<AppliedDateFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||
values={Array.isArray(value) ? (value as string[]) : []}
|
||||
/>
|
||||
)}
|
||||
{MEMBERS_FILTERS.includes(filterKey) && (
|
||||
<AppliedMembersFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||
values={Array.isArray(value) ? (value as string[]) : []}
|
||||
/>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemoveFilter(filterKey, null)}
|
||||
>
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearAllFilters}
|
||||
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
|
||||
>
|
||||
Clear all
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
101
web/core/components/views/filters/filter-selection.tsx
Normal file
101
web/core/components/views/filters/filter-selection.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { TViewFilterProps, TViewFilters } from "@plane/types";
|
||||
// components
|
||||
import { FilterCreatedBy, FilterCreatedDate } from "@/components/common/filters";
|
||||
import { FilterOption } from "@/components/issues";
|
||||
// constants
|
||||
import { EViewAccess } from "@/constants/views";
|
||||
|
||||
type Props = {
|
||||
filters: TViewFilters;
|
||||
handleFiltersUpdate: <T extends keyof TViewFilters>(filterKey: T, filterValue: TViewFilters[T]) => void;
|
||||
memberIds?: string[] | undefined;
|
||||
};
|
||||
|
||||
export const ViewFiltersSelection: React.FC<Props> = observer((props) => {
|
||||
const { filters, handleFiltersUpdate, memberIds } = props;
|
||||
// states
|
||||
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
|
||||
|
||||
// handles filter update
|
||||
const handleFilters = (key: keyof TViewFilterProps, value: boolean | string | EViewAccess | string[]) => {
|
||||
const currValues = (filters.filters?.[key] ?? []) as (string | EViewAccess)[];
|
||||
|
||||
if (typeof currValues === "boolean" && typeof value === "boolean") return;
|
||||
|
||||
if (Array.isArray(currValues)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!currValues.includes(val)) currValues.push(val);
|
||||
else currValues.splice(currValues.indexOf(val), 1);
|
||||
});
|
||||
} else if (typeof value !== "boolean") {
|
||||
if (currValues?.includes(value)) currValues.splice(currValues.indexOf(value), 1);
|
||||
else currValues.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
handleFiltersUpdate("filters", {
|
||||
...filters.filters,
|
||||
[key]: currValues,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="bg-custom-background-100 p-2.5 pb-0">
|
||||
<div className="flex items-center gap-1.5 rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-1.5 py-1 text-xs">
|
||||
<Search className="text-custom-text-400" size={12} strokeWidth={2} />
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
|
||||
placeholder="Search"
|
||||
value={filtersSearchQuery}
|
||||
onChange={(e) => setFiltersSearchQuery(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
{filtersSearchQuery !== "" && (
|
||||
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>
|
||||
<X className="text-custom-text-300" size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full divide-y divide-custom-border-200 overflow-y-auto px-2.5 vertical-scrollbar scrollbar-sm">
|
||||
<div className="py-2">
|
||||
<FilterOption
|
||||
isChecked={!!filters.filters?.favorites}
|
||||
onClick={() =>
|
||||
handleFiltersUpdate("filters", {
|
||||
...filters.filters,
|
||||
favorites: !filters.filters?.favorites,
|
||||
})
|
||||
}
|
||||
title="Favorites"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* created date */}
|
||||
<div className="py-2">
|
||||
<FilterCreatedDate
|
||||
appliedFilters={filters.filters?.created_at ?? null}
|
||||
handleUpdate={(val: string | string[]) => handleFilters("created_at", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* created by */}
|
||||
<div className="py-2">
|
||||
<FilterCreatedBy
|
||||
appliedFilters={filters.filters?.owned_by ?? null}
|
||||
handleUpdate={(val) => handleFilters("owned_by", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
memberIds={memberIds}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
75
web/core/components/views/filters/order-by.tsx
Normal file
75
web/core/components/views/filters/order-by.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
"use client";
|
||||
|
||||
import { ArrowDownWideNarrow, Check, ChevronDown } from "lucide-react";
|
||||
// types
|
||||
import { TViewFiltersSortBy, TViewFiltersSortKey } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu, getButtonStyling } from "@plane/ui";
|
||||
// constants
|
||||
import { VIEW_SORTING_KEY_OPTIONS } from "@/constants/views";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
type Props = {
|
||||
onChange: (value: { key?: TViewFiltersSortKey; order?: TViewFiltersSortBy }) => void;
|
||||
sortBy: TViewFiltersSortBy;
|
||||
sortKey: TViewFiltersSortKey;
|
||||
};
|
||||
|
||||
export const ViewOrderByDropdown: React.FC<Props> = (props) => {
|
||||
const { onChange, sortBy, sortKey } = props;
|
||||
|
||||
const orderByDetails = VIEW_SORTING_KEY_OPTIONS.find((option) => sortKey === option.key);
|
||||
const isDescending = sortBy === "desc";
|
||||
|
||||
const sortByOptions = [
|
||||
{ key: "asc", label: "Ascending", isSelected: !isDescending },
|
||||
{ key: "desc", label: "Descending", isSelected: isDescending },
|
||||
];
|
||||
|
||||
return (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div className={cn(getButtonStyling("neutral-primary", "sm"), "px-2 text-custom-text-300")}>
|
||||
<ArrowDownWideNarrow className="h-3 w-3" />
|
||||
{orderByDetails?.label}
|
||||
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
placement="bottom-end"
|
||||
maxHeight="lg"
|
||||
closeOnSelect
|
||||
>
|
||||
{VIEW_SORTING_KEY_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
className="flex items-center justify-between gap-2"
|
||||
onClick={() =>
|
||||
onChange({
|
||||
key: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
{sortKey === option.key && <Check className="h-3 w-3" />}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
<hr className="my-2 border-custom-border-200" />
|
||||
{sortByOptions.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
className="flex items-center justify-between gap-2"
|
||||
onClick={() => {
|
||||
if (!option.isSelected)
|
||||
onChange({
|
||||
order: option.key as TViewFiltersSortBy,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
{option.isSelected && <Check className="h-3 w-3" />}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
);
|
||||
};
|
||||
|
|
@ -14,6 +14,7 @@ import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/componen
|
|||
import { EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// helpers
|
||||
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
||||
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
import { LayoutDropDown } from "../dropdowns/layout";
|
||||
|
|
@ -28,6 +29,8 @@ type Props = {
|
|||
const defaultValues: Partial<IProjectView> = {
|
||||
name: "",
|
||||
description: "",
|
||||
display_properties: getComputedDisplayProperties(),
|
||||
display_filters: getComputedDisplayFilters(),
|
||||
};
|
||||
|
||||
export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
|
|
|
|||
|
|
@ -51,14 +51,14 @@ export const UpdateViewComponent = (props: Props) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 h-fit">
|
||||
{isLocked ? (
|
||||
<LockedComponent toolTipContent={lockedTooltipContent} />
|
||||
) : (
|
||||
!areFiltersEqual &&
|
||||
isAuthorizedUser && (
|
||||
<>
|
||||
<Button variant="outline-primary" size="sm" className="flex-shrink-0" onClick={() => setIsModalOpen(true)}>
|
||||
<Button variant="outline-primary" size="md" className="flex-shrink-0" onClick={() => setIsModalOpen(true)}>
|
||||
Save as
|
||||
</Button>
|
||||
{isOwner && <>{updateButton}</>}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// icons
|
||||
import { Search, X } from "lucide-react";
|
||||
import { ListFilter, Search, X } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useProjectView } from "@/hooks/store";
|
||||
import { useMember, useProjectView } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { FiltersDropdown } from "../issues";
|
||||
import { ViewFiltersSelection } from "./filters/filter-selection";
|
||||
import { ViewOrderByDropdown } from "./filters/order-by";
|
||||
|
||||
export const ViewListHeader = observer(() => {
|
||||
// states
|
||||
|
|
@ -14,13 +17,17 @@ export const ViewListHeader = observer(() => {
|
|||
// refs
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
// store hooks
|
||||
const { searchQuery, updateSearchQuery } = useProjectView();
|
||||
const { filters, updateFilters } = useProjectView();
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
|
||||
// handlers
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Escape") {
|
||||
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
|
||||
else {
|
||||
if (filters?.searchQuery && filters?.searchQuery.trim() !== "") {
|
||||
updateFilters("searchQuery", "");
|
||||
} else {
|
||||
setIsSearchOpen(false);
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
|
|
@ -29,12 +36,12 @@ export const ViewListHeader = observer(() => {
|
|||
|
||||
// outside click detector hook
|
||||
useOutsideClickDetector(inputRef, () => {
|
||||
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||
if (isSearchOpen && filters?.searchQuery.trim() === "") setIsSearchOpen(false);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery.trim() !== "") setIsSearchOpen(true);
|
||||
}, [searchQuery]);
|
||||
if (filters?.searchQuery.trim() !== "") setIsSearchOpen(true);
|
||||
}, [filters?.searchQuery]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex items-center gap-2">
|
||||
|
|
@ -64,8 +71,8 @@ export const ViewListHeader = observer(() => {
|
|||
ref={inputRef}
|
||||
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
|
||||
placeholder="Search"
|
||||
value={searchQuery}
|
||||
onChange={(e) => updateSearchQuery(e.target.value)}
|
||||
value={filters?.searchQuery}
|
||||
onChange={(e) => updateFilters("searchQuery", e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
{isSearchOpen && (
|
||||
|
|
@ -73,7 +80,7 @@ export const ViewListHeader = observer(() => {
|
|||
type="button"
|
||||
className="grid place-items-center"
|
||||
onClick={() => {
|
||||
updateSearchQuery("");
|
||||
updateFilters("searchQuery", "");
|
||||
setIsSearchOpen(false);
|
||||
}}
|
||||
>
|
||||
|
|
@ -82,6 +89,26 @@ export const ViewListHeader = observer(() => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ViewOrderByDropdown
|
||||
sortBy={filters.sortBy}
|
||||
sortKey={filters.sortKey}
|
||||
onChange={(val) => {
|
||||
if (val.key) updateFilters("sortKey", val.key);
|
||||
if (val.order) updateFilters("sortBy", val.order);
|
||||
}}
|
||||
/>
|
||||
<FiltersDropdown
|
||||
icon={<ListFilter className="h-3 w-3" />}
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={false}
|
||||
>
|
||||
<ViewFiltersSelection
|
||||
filters={filters}
|
||||
handleFiltersUpdate={updateFilters}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { ListLayout } from "@/components/core/list";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
|
|
@ -8,26 +10,48 @@ import { ProjectViewListItem } from "@/components/views";
|
|||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// hooks
|
||||
import { useCommandPalette, useProjectView } from "@/hooks/store";
|
||||
// assets
|
||||
import AllFiltersImage from "@/public/empty-state/pages/all-filters.svg";
|
||||
import NameFilterImage from "@/public/empty-state/pages/name-filter.svg";
|
||||
|
||||
export const ProjectViewsList = observer(() => {
|
||||
const { projectId } = useParams();
|
||||
// store hooks
|
||||
const { toggleCreateViewModal } = useCommandPalette();
|
||||
const { projectViewIds, getViewById, loader, searchQuery } = useProjectView();
|
||||
const { getProjectViews, getFilteredProjectViews, filters, loader } = useProjectView();
|
||||
|
||||
if (loader || !projectViewIds) return <ViewListLoader />;
|
||||
const projectViews = getProjectViews(projectId?.toString());
|
||||
const filteredProjectViews = getFilteredProjectViews(projectId?.toString());
|
||||
|
||||
// derived values
|
||||
const viewsList = projectViewIds.map((viewId) => getViewById(viewId));
|
||||
if (loader || !projectViews || !filteredProjectViews) return <ViewListLoader />;
|
||||
|
||||
const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
if (filteredProjectViews.length === 0 && projectViews) {
|
||||
return (
|
||||
<div className="h-full w-full grid place-items-center">
|
||||
<div className="text-center">
|
||||
<Image
|
||||
src={filters.searchQuery.length > 0 ? NameFilterImage : AllFiltersImage}
|
||||
className="h-36 sm:h-48 w-36 sm:w-48 mx-auto"
|
||||
alt="No matching modules"
|
||||
/>
|
||||
<h5 className="text-xl font-medium mt-7 mb-1">No matching views</h5>
|
||||
<p className="text-custom-text-400 text-base">
|
||||
{filters.searchQuery.length > 0
|
||||
? "Remove the search criteria to see all views"
|
||||
: "Remove the filters to see all views"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{viewsList.length > 0 ? (
|
||||
{filteredProjectViews.length > 0 ? (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<ListLayout>
|
||||
{filteredViewsList.length > 0 ? (
|
||||
filteredViewsList.map((view) => <ProjectViewListItem key={view.id} view={view} />)
|
||||
{filteredProjectViews.length > 0 ? (
|
||||
filteredProjectViews.map((view) => <ProjectViewListItem key={view.id} view={view} />)
|
||||
) : (
|
||||
<p className="mt-10 text-center text-sm text-custom-text-300">No results found</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "@/constants/workspace";
|
|||
import { useEventTracker, useWorkspace } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
|
||||
type Props = {
|
||||
onSubmit?: (res: IWorkspace) => Promise<void>;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { Button, Input, TextArea } from "@plane/ui";
|
|||
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// helpers
|
||||
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useLabel, useMember } from "@/hooks/store";
|
||||
|
||||
|
|
@ -24,6 +26,8 @@ type Props = {
|
|||
const defaultValues: Partial<IWorkspaceView> = {
|
||||
name: "",
|
||||
description: "",
|
||||
display_properties: getComputedDisplayProperties(),
|
||||
display_filters: getComputedDisplayFilters(),
|
||||
};
|
||||
|
||||
export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue