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,123 +400,134 @@ 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]) ? (
<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]) && ( <span className="capitalize">{filters[key as keyof typeof filters]}</span>
<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>
)
)} )}
</div> </div>
); );
})} })}
</div> </div>
{Object.keys(filters).length > 0 && ( {Object.keys(filters).length > 0 &&
<PrimaryButton nullFilters.length !== Object.keys(filters).length &&
onClick={() => !viewId && (
setCreateViewModal({ <PrimaryButton
query: filters, onClick={() =>
}) setCreateViewModal({
} query: filters,
className="flex items-center gap-2 text-sm" })
> }
<PlusIcon className="h-4 w-4" /> className="flex items-center gap-2 text-sm"
Save view >
</PrimaryButton> <PlusIcon className="h-4 w-4" />
)} Save view
</PrimaryButton>
)}
</div> </div>
<DragDropContext onDragEnd={handleOnDragEnd}> <DragDropContext onDragEnd={handleOnDragEnd}>
<StrictModeDroppable droppableId="trashBox"> <StrictModeDroppable droppableId="trashBox">

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>
<span> {shortcutKey(type) && (
Use shortcut{" "} <span>
<span className="rounded-sm mx-1 border border-gray-200 bg-gray-100 px-2 py-1 text-sm font-medium text-gray-800"> Use shortcut{" "}
{shortcutKey(type)} <span className="mx-1 rounded-sm border border-gray-200 bg-gray-100 px-2 py-1 text-sm font-medium text-gray-800">
</span>{" "} {shortcutKey(type)}
to create {type} from anywhere. </span>{" "}
</span> to create {type} from anywhere.
</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,36 +80,48 @@ 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.length > 0 ? (
views.map((view) => ( <div className="rounded-md border">
<div {views.map((view) => (
className="flex items-center justify-between border-b border-gray-400 p-4 last:border-b-0" <div
key={view.id} 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 href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}>
</Link> <a>{view.name}</a>
<CustomMenu width="auto" verticalEllipsis> </Link>
<CustomMenu.MenuItem <CustomMenu width="auto" verticalEllipsis>
onClick={() => { <CustomMenu.MenuItem
setSelectedView(view); onClick={() => {
}} setSelectedView(view);
> }}
<span className="flex items-center justify-start gap-2 text-gray-800"> >
<TrashIcon className="h-4 w-4" /> <span className="flex items-center justify-start gap-2 text-gray-800">
<span>Delete</span> <TrashIcon className="h-4 w-4" />
</span> <span>Delete</span>
</CustomMenu.MenuItem> </span>
</CustomMenu> </CustomMenu.MenuItem>
</div> </CustomMenu>
)) </div>
) : ( ))}
<div className="flex justify-center pt-20">
<Spinner />
</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> </AppLayout>
); );
}; };

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB