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

View File

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

View File

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

View File

@ -17,11 +17,13 @@ import AppLayout from "layouts/app-layout";
// ui // ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // 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 // fetching keys
import { PROJECT_DETAILS, VIEWS_LIST } from "constants/fetch-keys"; import { PROJECT_DETAILS, VIEWS_LIST } from "constants/fetch-keys";
// components // components
import { CustomMenu, Spinner, PrimaryButton } from "components/ui"; import { CustomMenu, PrimaryButton, Loader, EmptyState } from "components/ui";
import { DeleteViewModal, CreateUpdateViewModal } from "components/views"; import { DeleteViewModal, CreateUpdateViewModal } from "components/views";
// types // types
import { IView } from "types"; import { IView } from "types";
@ -78,11 +80,12 @@ const ProjectViews: NextPage = () => {
onClose={() => setSelectedView(null)} onClose={() => setSelectedView(null)}
onSuccess={() => setSelectedView(null)} onSuccess={() => setSelectedView(null)}
/> />
<div className="rounded-md border border-gray-400">
{views ? ( {views ? (
views.map((view) => ( views.length > 0 ? (
<div className="rounded-md border">
{views.map((view) => (
<div <div
className="flex items-center justify-between border-b border-gray-400 p-4 last:border-b-0" className="flex items-center justify-between rounded-md border-b bg-white p-4"
key={view.id} key={view.id}
> >
<Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}>
@ -101,13 +104,24 @@ const ProjectViews: NextPage = () => {
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</CustomMenu> </CustomMenu>
</div> </div>
)) ))}
</div>
) : ( ) : (
<div className="flex justify-center pt-20"> <EmptyState
<Spinner /> type="view"
</div> 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>
)} )}
</div>
</AppLayout> </AppLayout>
); );
}; };

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB