style: views (#486)

This commit is contained in:
Aaryan Khandelwal 2023-03-22 14:47:13 +05:30 committed by GitHub
parent 818d1147d5
commit 283950c8e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 258 additions and 153 deletions

View File

@ -153,8 +153,8 @@ export const IssuesFilterView: React.FC = () => {
],
},
{
id: "assignee",
label: "Assignee",
id: "assignees",
label: "Assignees",
value: members,
children: [
...(members?.map((member) => ({
@ -168,7 +168,7 @@ export const IssuesFilterView: React.FC = () => {
</div>
),
value: {
key: "assignee",
key: "assignees",
value: member.member.id,
},
selected: filters?.assignees?.includes(member.member.id),

View File

@ -19,7 +19,7 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CreateUpdateViewModal } from "components/views";
// ui
import { EmptySpace, EmptySpaceItem, PrimaryButton } from "components/ui";
import { Avatar, EmptySpace, EmptySpaceItem, PrimaryButton } from "components/ui";
// icons
import { PlusIcon, RectangleStackIcon, TrashIcon, XMarkIcon } from "@heroicons/react/24/outline";
// helpers
@ -42,6 +42,8 @@ import {
PROJECT_MEMBERS,
STATE_LIST,
} from "constants/fetch-keys";
import { getPriorityIcon } from "components/icons/priority-icon";
import { getStateGroupIcon } from "components/icons";
type Props = {
type?: "issue" | "cycle" | "module";
@ -71,7 +73,7 @@ export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModa
const [trashBox, setTrashBox] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const {
groupedByIssues,
@ -369,6 +371,10 @@ export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModa
[trashBox, setTrashBox]
);
const nullFilters = Object.keys(filters).filter(
(key) => filters[key as keyof IIssueFilterOptions] === null
);
return (
<>
<CreateUpdateViewModal
@ -394,123 +400,134 @@ export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModa
isOpen={deleteIssueModal}
data={issueToDelete}
/>
<div className="mb-3 flex items-center justify-between gap-2">
<div className="flex gap-x-3">
<div className="mb-5 -mt-4 flex items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-3 text-xs">
{Object.keys(filters).map((key) => {
if (filters[key as keyof typeof filters] !== null)
return (
<div key={key} className="flex gap-x-2 text-sm">
<p>
Filter for <span className="font-medium">{key}</span>:{" "}
</p>
<div key={key} className="flex items-center gap-x-2 rounded bg-white px-2 py-1">
<span className="font-medium capitalize text-gray-500">{key}:</span>
{filters[key as keyof IIssueFilterOptions] === null ||
(filters[key as keyof IIssueFilterOptions]?.length ?? 0) <= 0 ? (
<p className="font-medium">None</p>
<span>None</span>
) : Array.isArray(filters[key as keyof IIssueFilterOptions]) ? (
<div className="space-x-2">
{key === "state"
? filters.state?.map((stateId: any) => {
const state = states?.find((s) => s.id === stateId);
return (
<p
key={state?.id}
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium text-white"
style={{
color: state?.color,
backgroundColor: `${state?.color}20`,
}}
>
<span>
{getStateGroupIcon(
state?.group ?? "backlog",
"12",
"12",
state?.color
)}
</span>
<span>{state?.name ?? ""}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
state: filters.state?.filter((s: any) => s !== stateId),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</p>
);
})
: key === "priority"
? filters.priority?.map((priority: any) => (
<p
key={priority}
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium capitalize text-white ${
priority === "urgent"
? "bg-red-100 text-red-600 hover:bg-red-100"
: priority === "high"
? "bg-orange-100 text-orange-500 hover:bg-orange-100"
: priority === "medium"
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100"
: priority === "low"
? "bg-green-100 text-green-500 hover:bg-green-100"
: "bg-gray-100"
}`}
>
<span>{getPriorityIcon(priority)}</span>
<span>{priority}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
priority: filters.priority?.filter((p: any) => p !== priority),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</p>
))
: key === "assignees"
? filters.assignees?.map((memberId: string) => {
const member = members?.find((m) => m.member.id === memberId)?.member;
return (
<p
key={memberId}
className="inline-flex items-center gap-x-1 rounded-full border px-2 py-0.5 font-medium capitalize"
>
<Avatar user={member} />
<span>{member?.first_name}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
assignees: filters.assignees?.filter(
(p: any) => p !== memberId
),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</p>
);
})
: (filters[key as keyof IIssueFilterOptions] as any)?.join(", ")}
</div>
) : (
Array.isArray(filters[key as keyof IIssueFilterOptions]) && (
<p className="space-x-2 font-medium">
{key === "state"
? (filters[key as keyof IIssueFilterOptions] as any)?.map(
(stateId: any) => {
const state = states?.find((s) => s.id === stateId);
return (
<p
key={state?.id}
className="inline-flex items-center gap-x-1 rounded-full bg-gray-500 px-2 py-0.5 text-xs font-medium text-white"
>
<span>{state?.name ?? "Loading..."}</span>
<span
className="cursor-pointer"
onClick={() => {
setFilters({
...filters,
[key]: (
filters[key as keyof IIssueFilterOptions] as any
)?.filter((s: any) => s !== stateId),
});
}}
>
<XMarkIcon className="h-3 w-3" />
</span>
</p>
);
}
)
: key === "priority"
? (filters[key as keyof IIssueFilterOptions] as any)?.map(
(priority: any) => (
<p
key={priority}
className="inline-flex items-center gap-x-1 rounded-full bg-gray-500 px-2 py-0.5 text-xs font-medium capitalize text-white"
>
<span>{priority}</span>
<span
className="cursor-pointer"
onClick={() => {
setFilters({
...filters,
[key]: (
filters[key as keyof IIssueFilterOptions] as any
)?.filter((p: any) => p !== priority),
});
}}
>
<XMarkIcon className="h-3 w-3" />
</span>
</p>
)
)
: key === "assignee"
? (filters[key as keyof IIssueFilterOptions] as any)?.map(
(member: any) => (
<p
key={member}
className="inline-flex items-center gap-x-1 rounded-full bg-gray-500 px-2 py-0.5 text-xs font-medium capitalize text-white"
>
<span>
{
members?.find((m) => m.member.id === member)?.member
.first_name
}
</span>
<span
className="cursor-pointer"
onClick={() => {
setFilters({
...filters,
[key as keyof IIssueFilterOptions]: (
filters[key as keyof IIssueFilterOptions] as any
)?.filter((p: any) => p !== member),
});
}}
>
<XMarkIcon className="h-3 w-3" />
</span>
</p>
)
)
: (filters[key as keyof IIssueFilterOptions] as any)?.join(", ")}
</p>
)
<span className="capitalize">{filters[key as keyof typeof filters]}</span>
)}
</div>
);
})}
</div>
{Object.keys(filters).length > 0 && (
<PrimaryButton
onClick={() =>
setCreateViewModal({
query: filters,
})
}
className="flex items-center gap-2 text-sm"
>
<PlusIcon className="h-4 w-4" />
Save view
</PrimaryButton>
)}
{Object.keys(filters).length > 0 &&
nullFilters.length !== Object.keys(filters).length &&
!viewId && (
<PrimaryButton
onClick={() =>
setCreateViewModal({
query: filters,
})
}
className="flex items-center gap-2 text-sm"
>
<PlusIcon className="h-4 w-4" />
Save view
</PrimaryButton>
)}
</div>
<DragDropContext onDragEnd={handleOnDragEnd}>
<StrictModeDroppable droppableId="trashBox">

View File

@ -7,13 +7,14 @@ import { PlusIcon } from "@heroicons/react/24/outline";
import { capitalizeFirstLetter } from "helpers/string.helper";
type Props = {
type: "cycle" | "module" | "project" | "issue";
type: "cycle" | "module" | "project" | "issue" | "view";
title: string;
description: React.ReactNode | string;
imgURL: string;
action?: () => void;
};
export const EmptyState: React.FC<Props> = ({ type, title, description, imgURL }) => {
export const EmptyState: React.FC<Props> = ({ type, title, description, imgURL, action }) => {
const shortcutKey = (type: string) => {
switch (type) {
case "cycle":
@ -22,8 +23,10 @@ export const EmptyState: React.FC<Props> = ({ type, title, description, imgURL }
return "M";
case "project":
return "P";
default:
case "issue":
return "C";
default:
return null;
}
};
return (
@ -33,20 +36,30 @@ export const EmptyState: React.FC<Props> = ({ type, title, description, imgURL }
</div>
<h3 className="text-xl font-semibold">{title}</h3>
<span>
Use shortcut{" "}
<span className="rounded-sm mx-1 border border-gray-200 bg-gray-100 px-2 py-1 text-sm font-medium text-gray-800">
{shortcutKey(type)}
</span>{" "}
to create {type} from anywhere.
</span>
{shortcutKey(type) && (
<span>
Use shortcut{" "}
<span className="mx-1 rounded-sm border border-gray-200 bg-gray-100 px-2 py-1 text-sm font-medium text-gray-800">
{shortcutKey(type)}
</span>{" "}
to create {type} from anywhere.
</span>
)}
<p className="max-w-md text-sm text-gray-500">{description}</p>
<button
type="button"
className="flex items-center gap-1 rounded-lg bg-theme px-2.5 py-2 text-sm text-white"
onClick={() => {
if (action) {
action();
return;
}
if (!shortcutKey(type)) return;
const e = new KeyboardEvent("keydown", {
key: shortcutKey(type),
key: shortcutKey(type) as string,
});
document.dispatchEvent(e);
}}

View File

@ -6,7 +6,6 @@ import { PlusIcon } from "@heroicons/react/24/outline";
// image
import emptyModule from "public/empty-state/empty-module.svg";
// layouts
import AppLayout from "layouts/app-layout";
// lib

View File

@ -17,11 +17,13 @@ import AppLayout from "layouts/app-layout";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { TrashIcon } from "@heroicons/react/20/solid";
import { TrashIcon } from "@heroicons/react/24/outline";
// image
import emptyView from "public/empty-state/empty-view.svg";
// fetching keys
import { PROJECT_DETAILS, VIEWS_LIST } from "constants/fetch-keys";
// components
import { CustomMenu, Spinner, PrimaryButton } from "components/ui";
import { CustomMenu, PrimaryButton, Loader, EmptyState } from "components/ui";
import { DeleteViewModal, CreateUpdateViewModal } from "components/views";
// types
import { IView } from "types";
@ -78,36 +80,48 @@ const ProjectViews: NextPage = () => {
onClose={() => setSelectedView(null)}
onSuccess={() => setSelectedView(null)}
/>
<div className="rounded-md border border-gray-400">
{views ? (
views.map((view) => (
<div
className="flex items-center justify-between border-b border-gray-400 p-4 last:border-b-0"
key={view.id}
>
<Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}>
<a>{view.name}</a>
</Link>
<CustomMenu width="auto" verticalEllipsis>
<CustomMenu.MenuItem
onClick={() => {
setSelectedView(view);
}}
>
<span className="flex items-center justify-start gap-2 text-gray-800">
<TrashIcon className="h-4 w-4" />
<span>Delete</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
))
) : (
<div className="flex justify-center pt-20">
<Spinner />
{views ? (
views.length > 0 ? (
<div className="rounded-md border">
{views.map((view) => (
<div
className="flex items-center justify-between rounded-md border-b bg-white p-4"
key={view.id}
>
<Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}>
<a>{view.name}</a>
</Link>
<CustomMenu width="auto" verticalEllipsis>
<CustomMenu.MenuItem
onClick={() => {
setSelectedView(view);
}}
>
<span className="flex items-center justify-start gap-2 text-gray-800">
<TrashIcon className="h-4 w-4" />
<span>Delete</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
))}
</div>
)}
</div>
) : (
<EmptyState
type="view"
title="Create New View"
description="Views are smaller, focused projects that help you group and organize issues within a specific time frame."
imgURL={emptyView}
action={() => setIsCreateViewModalOpen(true)}
/>
)
) : (
<Loader>
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</Loader>
)}
</AppLayout>
);
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB