chore: accept issue confirmation modal (#1377)

* chore: accept issue confirmation modal

* chore: add inbox option to the command menu

* fix: status colors not loading

* chore: show state name on the inbox issue sidebar
This commit is contained in:
Aaryan Khandelwal 2023-06-23 19:30:53 +05:30 committed by GitHub
parent b87e2fff4c
commit ca7d3309d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 177 additions and 40 deletions

View File

@ -9,6 +9,7 @@ import {
ChatBubbleOvalLeftEllipsisIcon,
DocumentTextIcon,
FolderPlusIcon,
InboxIcon,
LinkIcon,
MagnifyingGlassIcon,
RocketLaunchIcon,
@ -34,6 +35,7 @@ import { Dialog, Transition } from "@headlessui/react";
// cmdk
import { Command } from "cmdk";
// hooks
import useProjectDetails from "hooks/use-project-details";
import useTheme from "hooks/use-theme";
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
@ -64,10 +66,11 @@ import {
// services
import issuesService from "services/issues.service";
import workspaceService from "services/workspace.service";
import inboxService from "services/inbox.service";
// types
import { IIssue, IWorkspaceSearchResults } from "types";
// fetch keys
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
import { INBOX_LIST, ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
export const CommandPalette: React.FC = () => {
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
@ -105,6 +108,8 @@ export const CommandPalette: React.FC = () => {
const { workspaceSlug, projectId, issueId, inboxId } = router.query;
const { user } = useUser();
const { projectDetails } = useProjectDetails();
const { setToastAlert } = useToast();
const { toggleCollapsed } = useTheme();
@ -116,6 +121,13 @@ export const CommandPalette: React.FC = () => {
: null
);
const { data: inboxList } = useSWR(
workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => inboxService.getInboxes(workspaceSlug as string, projectId as string)
: null
);
const updateIssue = useCallback(
async (formData: Partial<IIssue>) => {
if (!workspaceSlug || !projectId || !issueId) return;
@ -321,9 +333,9 @@ export const CommandPalette: React.FC = () => {
setDeleteIssueModal(true);
};
const goToSettings = (path: string = "") => {
const redirect = (path: string) => {
setIsPaletteOpen(false);
router.push(`/${workspaceSlug}/settings/${path}`);
router.push(path);
};
return (
@ -396,7 +408,7 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20">
@ -409,14 +421,14 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl border border-brand-base bg-brand-surface-2 shadow-2xl transition-all">
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl border border-brand-base bg-brand-base shadow-2xl transition-all">
<Command
filter={(value, search) => {
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
return 0;
}}
onKeyDown={(e) => {
// when seach is empty and page is undefined
// when search is empty and page is undefined
// when user tries to close the modal with esc
if (e.key === "Escape" && !page && !searchTerm) {
setIsPaletteOpen(false);
@ -698,6 +710,24 @@ export const CommandPalette: React.FC = () => {
<kbd>D</kbd>
</Command.Item>
</Command.Group>
{projectDetails && projectDetails.inbox_view && (
<Command.Group heading="Inbox">
<Command.Item
onSelect={() =>
redirect(
`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`
)
}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<InboxIcon className="h-4 w-4" color="#6b7280" />
Open inbox
</div>
</Command.Item>
</Command.Group>
)}
</>
)}
@ -814,7 +844,7 @@ export const CommandPalette: React.FC = () => {
{page === "settings" && workspaceSlug && (
<>
<Command.Item
onSelect={() => goToSettings()}
onSelect={() => redirect(`/${workspaceSlug}/settings`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
@ -823,7 +853,7 @@ export const CommandPalette: React.FC = () => {
</div>
</Command.Item>
<Command.Item
onSelect={() => goToSettings("members")}
onSelect={() => redirect(`/${workspaceSlug}/settings/members`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
@ -832,7 +862,7 @@ export const CommandPalette: React.FC = () => {
</div>
</Command.Item>
<Command.Item
onSelect={() => goToSettings("billing")}
onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
@ -841,7 +871,7 @@ export const CommandPalette: React.FC = () => {
</div>
</Command.Item>
<Command.Item
onSelect={() => goToSettings("integrations")}
onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
@ -850,12 +880,12 @@ export const CommandPalette: React.FC = () => {
</div>
</Command.Item>
<Command.Item
onSelect={() => goToSettings("import-export")}
onSelect={() => redirect(`/${workspaceSlug}/settings/import-export`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Import/ Export
Import/Export
</div>
</Command.Item>
</>

View File

@ -0,0 +1,92 @@
import React, { useState } from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// icons
import { CheckCircleIcon } from "@heroicons/react/24/outline";
// ui
import { SecondaryButton, PrimaryButton } from "components/ui";
// types
import type { IInboxIssue } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
data: IInboxIssue | undefined;
onSubmit: () => Promise<void>;
};
export const AcceptIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, onSubmit }) => {
const [isAccepting, setIsAccepting] = useState(false);
const onClose = () => {
setIsAccepting(false);
handleClose();
};
const handleAccept = () => {
setIsAccepting(true);
onSubmit().finally(() => setIsAccepting(false));
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-brand-base bg-brand-base text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-green-500/20 p-4">
<CheckCircleIcon className="h-6 w-6 text-green-600" aria-hidden="true" />
</span>
<span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">Accept Issue</h3>
</span>
</div>
<span>
<p className="text-sm text-brand-secondary">
Are you sure you want to accept issue{" "}
<span className="break-all font-medium text-brand-base">
{data?.project_detail?.identifier}-{data?.sequence_id}
</span>
{""}? Once accepted, this issue will be added to the project issues list.
</p>
</span>
<div className="flex justify-end gap-2">
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
<PrimaryButton onClick={handleAccept} loading={isAccepting}>
{isAccepting ? "Accepting..." : "Accept Issue"}
</PrimaryButton>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -18,6 +18,7 @@ import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast";
// components
import {
AcceptIssueModal,
DeclineIssueModal,
DeleteIssueModal,
FiltersDropdown,
@ -41,9 +42,9 @@ import type { IInboxIssueDetail, TInboxStatus } from "types";
import { INBOX_ISSUE_DETAILS } from "constants/fetch-keys";
export const InboxActionHeader = () => {
const [isAccepting, setIsAccepting] = useState(false);
const [date, setDate] = useState(new Date());
const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
const [acceptIssueModal, setAcceptIssueModal] = useState(false);
const [declineIssueModal, setDeclineIssueModal] = useState(false);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
@ -102,14 +103,6 @@ export const InboxActionHeader = () => {
});
};
const handleAcceptIssue = () => {
setIsAccepting(true);
markInboxStatus({
status: 1,
}).finally(() => setIsAccepting(false));
};
const issue = inboxIssues?.find((issue) => issue.bridge_id === inboxIssueId);
const currentIssueIndex =
inboxIssues?.findIndex((issue) => issue.bridge_id === inboxIssueId) ?? 0;
@ -144,6 +137,16 @@ export const InboxActionHeader = () => {
}).finally(() => setSelectDuplicateIssue(false));
}}
/>
<AcceptIssueModal
isOpen={acceptIssueModal}
handleClose={() => setAcceptIssueModal(false)}
data={inboxIssues?.find((i) => i.bridge_id === inboxIssueId)}
onSubmit={async () => {
await markInboxStatus({
status: 1,
}).finally(() => setAcceptIssueModal(false));
}}
/>
<DeclineIssueModal
isOpen={declineIssueModal}
handleClose={() => setDeclineIssueModal(false)}
@ -252,11 +255,10 @@ export const InboxActionHeader = () => {
<SecondaryButton
size="sm"
className="flex gap-2 items-center"
onClick={handleAcceptIssue}
loading={isAccepting}
onClick={() => setAcceptIssueModal(true)}
>
<CheckCircleIcon className="h-4 w-4 text-green-500" />
<span>{isAccepting ? "Accepting..." : "Accept"}</span>
<span>Accept</span>
</SecondaryButton>
</div>
)}

View File

@ -227,7 +227,9 @@ export const InboxMainContent: React.FC = () => {
issueStatus === 0 &&
new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date()
? "text-red-500 border-red-500 bg-red-500/10"
: `${inboxStatusDetails?.textColor} ${inboxStatusDetails?.bgColor} ${inboxStatusDetails?.borderColor}`
: inboxStatusDetails
? `${inboxStatusDetails.textColor} ${inboxStatusDetails.bgColor} ${inboxStatusDetails.borderColor}`
: ""
}`}
>
{issueStatus === -2 ? (

View File

@ -1,3 +1,4 @@
export * from "./accept-issue-modal";
export * from "./decline-issue-modal";
export * from "./delete-issue-modal";
export * from "./filters-dropdown";

View File

@ -11,7 +11,7 @@ export const IssuesListSidebar = () => {
const router = useRouter();
const { inboxIssueId } = router.query;
const { issues: inboxIssues } = useInboxView();
const { issues: inboxIssues, filtersLength } = useInboxView();
return (
<div className="h-full flex flex-col overflow-hidden">
@ -29,7 +29,8 @@ export const IssuesListSidebar = () => {
</div>
) : (
<div className="h-full p-4 grid place-items-center text-center text-sm text-brand-secondary">
No issues found for the selected filters. Try changing the filters.
{filtersLength > 0 &&
"No issues found for the selected filters. Try changing the filters."}
</div>
)
) : (

View File

@ -27,7 +27,7 @@ type Props = {
export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, userAuth }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { workspaceSlug, projectId, inboxIssueId } = router.query;
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
@ -50,15 +50,24 @@ export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, userAuth
<div className="sm:basis-1/2">
<CustomSelect
label={
<div className="flex items-center gap-2 text-left text-brand-base">
{getStateGroupIcon(
selectedState?.group ?? "backlog",
"16",
"16",
selectedState?.color ?? ""
)}
{addSpaceIfCamelCase(selectedState?.name ?? "")}
</div>
selectedState ? (
<div className="flex items-center gap-2 text-left text-brand-base">
{getStateGroupIcon(
selectedState?.group ?? "backlog",
"16",
"16",
selectedState?.color ?? ""
)}
{addSpaceIfCamelCase(selectedState?.name ?? "")}
</div>
) : inboxIssueId ? (
<div className="flex items-center gap-2 text-left text-brand-base">
{getStateGroupIcon("backlog", "16", "16", "#ff7700")}
Triage
</div>
) : (
"None"
)
}
value={value}
onChange={onChange}

View File

@ -35,7 +35,7 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
const [openChildFor, setOpenChildFor] = useState<string | null>(null);
return (
<Menu as="div" className="relative z-30 inline-block text-left">
<Menu as="div" className="relative z-10 inline-block text-left">
{({ open }) => (
<>
<div>

View File

@ -31,9 +31,9 @@
}
[cmdk-item]:hover {
background-color: rgba(var(--color-bg-base));
background-color: rgba(var(--color-bg-surface-2));
}
[cmdk-item][aria-selected="true"] {
background-color: rgba(var(--color-bg-base));
background-color: rgba(var(--color-bg-surface-2));
}