Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-1251] chore: view list enhancement #4427

Merged
merged 3 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions web/components/headers/project-views.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common";
// helpers
import { ProjectLogo } from "@/components/project";
import { ViewListHeader } from "@/components/views";
import { EUserProjectRoles } from "@/constants/project";
// constants
import { useCommandPalette, useProject, useUser } from "@/hooks/store";
Expand Down Expand Up @@ -58,6 +59,7 @@ export const ProjectViewsHeader: React.FC = observer(() => {
</div>
{canUserCreateIssue && (
<div className="flex flex-shrink-0 items-center gap-2">
<ViewListHeader />
<div>
<Button
variant="primary"
Expand Down
1 change: 1 addition & 0 deletions web/components/views/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./quick-actions";
export * from "./view-list-item";
export * from "./views-list";
export * from "./view-list-item-action";
export * from "./view-list-header";
84 changes: 84 additions & 0 deletions web/components/views/view-list-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { useRef, useState } from "react";
import { observer } from "mobx-react";
// icons
import { Search, X } from "lucide-react";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useProjectView } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";

export const ViewListHeader = observer(() => {
// states
const [isSearchOpen, setIsSearchOpen] = useState(false);
// const [isSearchOpen, setIsSearchOpen] = useState(searchQuery !== "" ? true : false);
// refs
const inputRef = useRef<HTMLInputElement>(null);

const { searchQuery, updateSearchQuery } = useProjectView();

// handlers
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
else {
setIsSearchOpen(false);
inputRef.current?.blur();
}
}
};

// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
});

return (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<div className="h-full flex items-center gap-2">
<div className="flex items-center">
{!isSearchOpen && (
<button
type="button"
className="-mr-1 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
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)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
updateSearchQuery("");
setIsSearchOpen(false);
}}
>
<X className="h-3 w-3" />
</button>
)}
</div>
</div>
</div>
);
});
10 changes: 9 additions & 1 deletion web/components/views/view-list-item-action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { EUserProjectRoles } from "@/constants/project";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useProjectView, useUser } from "@/hooks/store";
import { useMember, useProjectView, useUser } from "@/hooks/store";
import { ButtonAvatars } from "../dropdowns/member/avatar";

type Props = {
parentRef: React.RefObject<HTMLElement>;
Expand All @@ -31,6 +32,7 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
membership: { currentProjectRole },
} = useUser();
const { addViewToFavorites, removeViewFromFavorites } = useProjectView();
const { getUserDetails } = useMember();

// derived values
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
Expand All @@ -50,6 +52,8 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
removeViewFromFavorites(workspaceSlug.toString(), projectId.toString(), view.id);
};

const createdByDetails = view.created_by ? getUserDetails(view.created_by) : undefined;

return (
<>
{workspaceSlug && projectId && view && (
Expand All @@ -65,6 +69,10 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
<p className="hidden rounded bg-custom-background-80 px-2 py-1 text-xs text-custom-text-200 group-hover:block">
{totalFilters} {totalFilters === 1 ? "filter" : "filters"}
</p>

{/* created by */}
{createdByDetails && <ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />}

{isEditingAllowed && (
<FavoriteStar
onClick={(e) => {
Expand Down
82 changes: 1 addition & 81 deletions web/components/views/views-list.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,18 @@
import { useRef, useState } from "react";
import { observer } from "mobx-react-lite";
// ui
import { Search, X } from "lucide-react";
// components
import { ListLayout } from "@/components/core/list";
import { EmptyState } from "@/components/empty-state";
import { ViewListLoader } from "@/components/ui";
import { ProjectViewListItem } from "@/components/views";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// helper
import { cn } from "@/helpers/common.helper";
// hooks
import { useCommandPalette, useProjectView } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";

export const ProjectViewsList = observer(() => {
// states
const [searchQuery, setSearchQuery] = useState("");
const [isSearchOpen, setIsSearchOpen] = useState(searchQuery !== "" ? true : false);

// refs
const inputRef = useRef<HTMLInputElement>(null);

// store hooks
const { toggleCreateViewModal } = useCommandPalette();
const { projectViewIds, getViewById, loader } = useProjectView();

// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
});
const { projectViewIds, getViewById, loader, searchQuery } = useProjectView();

if (loader || !projectViewIds) return <ViewListLoader />;

Expand All @@ -39,72 +21,10 @@ export const ProjectViewsList = observer(() => {

const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(searchQuery.toLowerCase()));

// handlers
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (searchQuery && searchQuery.trim() !== "") setSearchQuery("");
else {
setIsSearchOpen(false);
inputRef.current?.blur();
}
}
};

return (
<>
{viewsList.length > 0 ? (
<div className="flex h-full w-full flex-col">
<div className="h-[50px] flex-shrink-0 w-full border-b border-custom-border-200 px-6 relative flex items-center gap-4 justify-between">
<div className="flex items-center">
<span className="block text-sm font-medium">Project Views</span>
</div>
<div className="h-full flex items-center gap-2">
<div className="flex items-center">
{!isSearchOpen && (
<button
type="button"
className="-mr-1 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
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) => setSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
setSearchQuery("");
setIsSearchOpen(false);
}}
>
<X className="h-3 w-3" />
</button>
)}
</div>
</div>
</div>
</div>
<ListLayout>
{filteredViewsList.length > 0 ? (
filteredViewsList.map((view) => <ProjectViewListItem key={view.id} view={view} />)
Expand Down
12 changes: 12 additions & 0 deletions web/store/project-view.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ export interface IProjectViewStore {
loader: boolean;
fetchedMap: Record<string, boolean>;
// observables
searchQuery: string;
viewMap: Record<string, IProjectView>;
// computed
projectViewIds: string[] | null;
// computed actions
getViewById: (viewId: string) => IProjectView;
updateSearchQuery: (query: string) => void;
// fetch actions
fetchViews: (workspaceSlug: string, projectId: string) => Promise<undefined | IProjectView[]>;
fetchViewDetails: (workspaceSlug: string, projectId: string, viewId: string) => Promise<IProjectView>;
Expand All @@ -38,6 +40,7 @@ export class ProjectViewStore implements IProjectViewStore {
// observables
loader: boolean = false;
viewMap: Record<string, IProjectView> = {};
searchQuery: string = "";
//loaders
fetchedMap: Record<string, boolean> = {};
// root store
Expand All @@ -51,6 +54,7 @@ export class ProjectViewStore implements IProjectViewStore {
loader: observable.ref,
viewMap: observable,
fetchedMap: observable,
searchQuery: observable.ref,
// computed
projectViewIds: computed,
// fetch actions
Expand All @@ -60,6 +64,8 @@ export class ProjectViewStore implements IProjectViewStore {
createView: action,
updateView: action,
deleteView: action,
// actions
updateSearchQuery: action,
// favorites actions
addViewToFavorites: action,
removeViewFromFavorites: action,
Expand All @@ -85,6 +91,12 @@ export class ProjectViewStore implements IProjectViewStore {
*/
getViewById = computedFn((viewId: string) => this.viewMap?.[viewId] ?? null);

/**
* @description update search query
* @param {string} query
*/
updateSearchQuery = (query: string) => (this.searchQuery = query);

/**
* Fetches views for current project
* @param workspaceSlug
Expand Down
Loading