forked from github/plane
feat: notifications (#1566)
* feat: added new issue subscriber table
* dev: notification model
* feat: added CRUD operation for issue subscriber
* Revert "feat: added CRUD operation for issue subscriber"
This reverts commit b22e062576
.
* feat: added CRUD operation for issue subscriber
* dev: notification models and operations
* dev: remove delete endpoint response data
* dev: notification endpoints and fix bg worker for saving notifications
* feat: added list and unsubscribe function in issue subscriber
* dev: filter by snoozed and response update for list and permissions
* dev: update issue notifications
* dev: notification segregation
* dev: update notifications
* dev: notification filtering
* dev: add issue name in notifications
* dev: notification new endpoints
* fix: pushing local settings
* feat: notification workflow setup and made basic UI
* style: improved UX with toast alerts and other interactions
refactor: changed classnames according to new theme structure, changed all icons to material icons
* feat: showing un-read notification count
* feat: not showing 'subscribe' button on issue created by user & assigned to user
not showing 'Create by you' for view & guest of the workspace
* fix: 'read' -> 'unread' heading, my issue wrong filter
* feat: made snooze dropdown & modal
feat: switched to calendar
* fix: minor ui fixes
* feat: snooze modal date/time select
* fix: params for read/un-read notification
* style: snooze notification modal
---------
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
parent
98b9957753
commit
53e443d816
@ -61,6 +61,7 @@ type Props = {
|
|||||||
| "link"
|
| "link"
|
||||||
| "delete"
|
| "delete"
|
||||||
| "all"
|
| "all"
|
||||||
|
| "subscribe"
|
||||||
)[];
|
)[];
|
||||||
uneditable?: boolean;
|
uneditable?: boolean;
|
||||||
};
|
};
|
||||||
@ -232,7 +233,8 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{issueDetail?.created_by !== user?.id &&
|
{issueDetail?.created_by !== user?.id &&
|
||||||
!issueDetail?.assignees.includes(user?.id ?? "") &&
|
!issueDetail?.assignees.includes(user?.id ?? "") &&
|
||||||
(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
|
!router.pathname.includes("[archivedIssueId]") &&
|
||||||
|
(fieldsToShow.includes("all") || fieldsToShow.includes("subscribe")) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded-md flex items-center gap-2 border border-custom-primary-100 px-2 py-1 text-xs text-custom-primary-100 shadow-sm duration-300 focus:outline-none"
|
className="rounded-md flex items-center gap-2 border border-custom-primary-100 px-2 py-1 text-xs text-custom-primary-100 shadow-sm duration-300 focus:outline-none"
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
// next
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
@ -8,11 +7,17 @@ import { useRouter } from "next/router";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { Icon } from "components/ui";
|
import { CustomMenu, Icon, Tooltip } from "components/ui";
|
||||||
|
|
||||||
// helper
|
// helper
|
||||||
import { stripHTML, replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { stripHTML, replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
import { formatDateDistance, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import {
|
||||||
|
formatDateDistance,
|
||||||
|
render12HourFormatTime,
|
||||||
|
renderLongDateFormat,
|
||||||
|
renderShortDate,
|
||||||
|
renderShortDateWithYearFormat,
|
||||||
|
} from "helpers/date-time.helper";
|
||||||
|
|
||||||
// type
|
// type
|
||||||
import type { IUserNotification } from "types";
|
import type { IUserNotification } from "types";
|
||||||
@ -25,6 +30,33 @@ type NotificationCardProps = {
|
|||||||
markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
|
markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const snoozeOptions = [
|
||||||
|
{
|
||||||
|
label: "1 days",
|
||||||
|
value: new Date(new Date().getTime() + 24 * 60 * 60 * 1000),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "3 days",
|
||||||
|
value: new Date(new Date().getTime() + 3 * 24 * 60 * 60 * 1000),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "5 days",
|
||||||
|
value: new Date(new Date().getTime() + 5 * 24 * 60 * 60 * 1000),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "1 week",
|
||||||
|
value: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "2 weeks",
|
||||||
|
value: new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Custom",
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
notification,
|
notification,
|
||||||
@ -41,23 +73,20 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={notification.id}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
markNotificationReadStatus(notification.id);
|
markNotificationReadStatus(notification.id);
|
||||||
router.push(
|
router.push(
|
||||||
`/${workspaceSlug}/projects/${notification.project}/issues/${notification.data.issue.id}`
|
`/${workspaceSlug}/projects/${notification.project}/issues/${notification.data.issue.id}`
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className={`px-4 ${
|
className={`group relative py-3 px-6 cursor-pointer ${
|
||||||
notification.read_at === null ? "bg-custom-primary-70/10" : "hover:bg-custom-background-200"
|
notification.read_at === null ? "bg-custom-primary-70/5" : "hover:bg-custom-background-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="relative group flex items-center gap-3 py-3 cursor-pointer border-b-2 border-custom-border-200">
|
|
||||||
{notification.read_at === null && (
|
{notification.read_at === null && (
|
||||||
<span className="absolute top-1/2 -left-2 -translate-y-1/2 w-1.5 h-1.5 bg-custom-primary-100 rounded-full" />
|
<span className="absolute top-1/2 left-2 -translate-y-1/2 w-1.5 h-1.5 bg-custom-primary-100 rounded-full" />
|
||||||
)}
|
)}
|
||||||
<div className="flex w-full pl-2">
|
<div className="flex items-center gap-4 w-full">
|
||||||
<div className="pl-0 p-2">
|
|
||||||
<div className="relative w-12 h-12 rounded-full">
|
<div className="relative w-12 h-12 rounded-full">
|
||||||
{notification.triggered_by_details.avatar &&
|
{notification.triggered_by_details.avatar &&
|
||||||
notification.triggered_by_details.avatar !== "" ? (
|
notification.triggered_by_details.avatar !== "" ? (
|
||||||
@ -70,17 +99,15 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-12 h-12 bg-custom-background-100 rounded-full flex justify-center items-center">
|
<div className="w-12 h-12 bg-custom-background-100 rounded-full flex justify-center items-center">
|
||||||
<span className="text-custom-text-100 font-semibold text-lg">
|
<span className="text-custom-text-100 font-medium text-lg">
|
||||||
{notification.triggered_by_details.first_name[0].toUpperCase()}
|
{notification.triggered_by_details.first_name[0].toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="w-full space-y-2.5">
|
||||||
<div className="w-full flex flex-col overflow-hidden">
|
<div className="text-sm">
|
||||||
<div>
|
<span className="font-semibold">
|
||||||
<p>
|
|
||||||
<span className="font-semibold text-custom-text-200">
|
|
||||||
{notification.triggered_by_details.first_name}{" "}
|
{notification.triggered_by_details.first_name}{" "}
|
||||||
{notification.triggered_by_details.last_name}{" "}
|
{notification.triggered_by_details.last_name}{" "}
|
||||||
</span>
|
</span>
|
||||||
@ -95,7 +122,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
notification.data.issue_activity.field !== "None"
|
notification.data.issue_activity.field !== "None"
|
||||||
? "to"
|
? "to"
|
||||||
: ""}
|
: ""}
|
||||||
<span className="font-semibold text-custom-text-200">
|
<span className="font-semibold">
|
||||||
{" "}
|
{" "}
|
||||||
{notification.data.issue_activity.field !== "None" ? (
|
{notification.data.issue_activity.field !== "None" ? (
|
||||||
notification.data.issue_activity.field !== "comment" ? (
|
notification.data.issue_activity.field !== "comment" ? (
|
||||||
@ -121,22 +148,29 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
"the issue and assigned it to you."
|
"the issue and assigned it to you."
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full flex items-center justify-between mt-3">
|
<div className="w-full flex justify-between text-xs">
|
||||||
<p className="truncate inline max-w-lg text-custom-text-300 text-sm mr-3">
|
<p className="truncate inline max-w-lg text-custom-text-300 mr-3">
|
||||||
{notification.data.issue.identifier}-{notification.data.issue.sequence_id}{" "}
|
{notification.data.issue.identifier}-{notification.data.issue.sequence_id}{" "}
|
||||||
{notification.data.issue.name}
|
{notification.data.issue.name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-custom-text-300 text-xs">
|
{notification.snoozed_till ? (
|
||||||
{formatDateDistance(notification.created_at)}
|
<p className="text-custom-text-300 flex items-center gap-x-1">
|
||||||
|
<Icon iconName="schedule" />
|
||||||
|
<span>
|
||||||
|
Till {renderShortDate(notification.snoozed_till)},{" "}
|
||||||
|
{render12HourFormatTime(notification.snoozed_till)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-custom-text-300">{formatDateDistance(notification.created_at)}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute py-1 flex gap-x-3 right-0 top-3 opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto">
|
<div className="absolute py-1 gap-x-3 right-3 top-3 hidden group-hover:flex">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -168,19 +202,8 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: notification.snoozed_till ? "Unsnooze Notification" : "Snooze Notification",
|
|
||||||
icon: "schedule",
|
|
||||||
onClick: () => {
|
|
||||||
if (notification.snoozed_till)
|
|
||||||
markSnoozeNotification(notification.id).then(() => {
|
|
||||||
setToastAlert({ title: "Notification un-snoozed", type: "success" });
|
|
||||||
});
|
|
||||||
else setSelectedNotificationForSnooze(notification.id);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
|
<Tooltip tooltipContent={item.name} position="top-left">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -188,12 +211,53 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
item.onClick();
|
item.onClick();
|
||||||
}}
|
}}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="text-sm flex w-full items-center gap-x-2 hover:bg-custom-background-100 p-0.5 rounded"
|
className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded"
|
||||||
>
|
>
|
||||||
<Icon iconName={item.icon} className="h-5 w-5 text-custom-text-300" />
|
<Icon iconName={item.icon} className="h-5 w-5 text-custom-text-300" />
|
||||||
</button>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
|
<Tooltip tooltipContent="Snooze Notification" position="top-left">
|
||||||
|
<CustomMenu
|
||||||
|
menuButtonOnClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
customButton={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded"
|
||||||
|
>
|
||||||
|
<Icon iconName="schedule" className="h-5 w-5 text-custom-text-300" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
optionsClassName="!z-20"
|
||||||
|
>
|
||||||
|
{snoozeOptions.map((item) => (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={item.label}
|
||||||
|
renderAs="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (!item.value) {
|
||||||
|
setSelectedNotificationForSnooze(notification.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
markSnoozeNotification(notification.id, item.value).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
title: `Notification snoozed till ${renderLongDateFormat(item.value)}`,
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomMenu>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
@ -13,9 +12,10 @@ import useWorkspaceMembers from "hooks/use-workspace-members";
|
|||||||
import useUserNotification from "hooks/use-user-notifications";
|
import useUserNotification from "hooks/use-user-notifications";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { Spinner, Icon } from "components/ui";
|
import { Icon, Loader, EmptyState } from "components/ui";
|
||||||
import { SnoozeNotificationModal, NotificationCard } from "components/notifications";
|
import { SnoozeNotificationModal, NotificationCard } from "components/notifications";
|
||||||
|
// images
|
||||||
|
import emptyNotification from "public/empty-state/notification.svg";
|
||||||
// helpers
|
// helpers
|
||||||
import { getNumberCount } from "helpers/string.helper";
|
import { getNumberCount } from "helpers/string.helper";
|
||||||
|
|
||||||
@ -116,16 +116,15 @@ export const NotificationPopover = () => {
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute bg-custom-background-100 flex flex-col left-0 md:left-full ml-8 z-10 top-0 pt-5 md:w-[36rem] w-[20rem] h-[27rem] border border-custom-background-90 shadow-lg rounded">
|
<Popover.Panel className="absolute bg-custom-background-100 flex flex-col left-0 md:left-full ml-8 z-10 top-0 md:w-[36rem] w-[20rem] h-[27rem] border border-custom-border-300 shadow-lg rounded-xl">
|
||||||
<div className="flex justify-between items-center md:px-6 px-2">
|
<div className="flex items-center justify-between px-5 pt-5">
|
||||||
<h2 className="text-custom-sidebar-text-100 text-lg font-semibold mb-2">
|
<h2 className="text-xl font-semibold mb-2">Notifications</h2>
|
||||||
Notifications
|
<div className="flex gap-x-4 justify-center items-center text-custom-text-200">
|
||||||
</h2>
|
|
||||||
<div className="flex gap-x-2 justify-center items-center">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
notificationsMutate();
|
notificationsMutate();
|
||||||
|
|
||||||
const target = e.target as HTMLButtonElement;
|
const target = e.target as HTMLButtonElement;
|
||||||
target?.classList.add("animate-spin");
|
target?.classList.add("animate-spin");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -133,7 +132,7 @@ export const NotificationPopover = () => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon iconName="refresh" className="h-6 w-6 text-custom-text-300" />
|
<Icon iconName="refresh" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -143,7 +142,7 @@ export const NotificationPopover = () => {
|
|||||||
setReadNotification((prev) => !prev);
|
setReadNotification((prev) => !prev);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon iconName="filter_list" className="h-6 w-6 text-custom-text-300" />
|
<Icon iconName="filter_list" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -153,7 +152,7 @@ export const NotificationPopover = () => {
|
|||||||
setSnoozed((prev) => !prev);
|
setSnoozed((prev) => !prev);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon iconName="schedule" className="h-6 w-6 text-custom-text-300" />
|
<Icon iconName="schedule" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -163,18 +162,15 @@ export const NotificationPopover = () => {
|
|||||||
setArchived((prev) => !prev);
|
setArchived((prev) => !prev);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon iconName="archive" className="h-6 w-6 text-custom-text-300" />
|
<Icon iconName="archive" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => closePopover()}>
|
<button type="button" onClick={() => closePopover()}>
|
||||||
<Icon iconName="close" className="h-6 w-6 text-custom-text-300" />
|
<Icon iconName="close" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="border-b border-custom-border-300 w-full px-5 mt-5">
|
||||||
<div className="mt-5 flex flex-col items-center">
|
|
||||||
{snoozed || archived || readNotification ? (
|
{snoozed || archived || readNotification ? (
|
||||||
<div className="w-full mb-3">
|
|
||||||
<div className="flex flex-col flex-1 px-2 md:px-6 overflow-y-auto">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -183,22 +179,19 @@ export const NotificationPopover = () => {
|
|||||||
setReadNotification(false);
|
setReadNotification(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h4 className="text-custom-text-300 text-center flex items-center">
|
<h4 className="flex items-center gap-2 pb-4">
|
||||||
<Icon iconName="arrow_back" className="h-5 w-5 text-custom-text-300" />
|
<Icon iconName="arrow_back" />
|
||||||
<span className="ml-2 font-semibold">
|
<span className="ml-2 font-medium">
|
||||||
{snoozed
|
{snoozed
|
||||||
? "Snoozed Notifications"
|
? "Snoozed Notifications"
|
||||||
: readNotification
|
: readNotification
|
||||||
? "Read Notifications"
|
? "Unread Notifications"
|
||||||
: "Archived Notifications"}
|
: "Archived Notifications"}
|
||||||
</span>
|
</span>
|
||||||
</h4>
|
</h4>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="border-b border-custom-border-300 md:px-6 px-2 w-full">
|
<nav className="flex space-x-5 overflow-x-auto" aria-label="Tabs">
|
||||||
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
|
||||||
{notificationTabs.map((tab) =>
|
{notificationTabs.map((tab) =>
|
||||||
tab.value === "created" ? (
|
tab.value === "created" ? (
|
||||||
isMember || isOwner ? (
|
isMember || isOwner ? (
|
||||||
@ -206,15 +199,20 @@ export const NotificationPopover = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
key={tab.value}
|
key={tab.value}
|
||||||
onClick={() => setSelectedTab(tab.value)}
|
onClick={() => setSelectedTab(tab.value)}
|
||||||
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium ${
|
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium outline-none ${
|
||||||
tab.value === selectedTab
|
tab.value === selectedTab
|
||||||
? "border-custom-primary-100 text-custom-primary-100"
|
? "border-custom-primary-100 text-custom-primary-100"
|
||||||
: "border-transparent text-custom-text-500 hover:border-custom-border-300 hover:text-custom-text-200"
|
: "border-transparent text-custom-text-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.label}
|
|
||||||
{tab.unreadCount && tab.unreadCount > 0 ? (
|
{tab.unreadCount && tab.unreadCount > 0 ? (
|
||||||
<span className="ml-3 bg-custom-background-1000/5 rounded-full text-custom-text-100 text-xs px-1.5">
|
<span
|
||||||
|
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
|
||||||
|
tab.value === selectedTab
|
||||||
|
? "bg-custom-primary-100 text-white"
|
||||||
|
: "bg-custom-background-80 text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{getNumberCount(tab.unreadCount)}
|
{getNumberCount(tab.unreadCount)}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
@ -228,12 +226,18 @@ export const NotificationPopover = () => {
|
|||||||
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium ${
|
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium ${
|
||||||
tab.value === selectedTab
|
tab.value === selectedTab
|
||||||
? "border-custom-primary-100 text-custom-primary-100"
|
? "border-custom-primary-100 text-custom-primary-100"
|
||||||
: "border-transparent text-custom-text-500 hover:border-custom-border-300 hover:text-custom-text-200"
|
: "border-transparent text-custom-text-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
{tab.unreadCount && tab.unreadCount > 0 ? (
|
{tab.unreadCount && tab.unreadCount > 0 ? (
|
||||||
<span className="ml-3 bg-custom-background-1000/5 rounded-full text-custom-text-100 text-xs px-1.5">
|
<span
|
||||||
|
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
|
||||||
|
tab.value === selectedTab
|
||||||
|
? "bg-custom-primary-100 text-white"
|
||||||
|
: "bg-custom-background-80 text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{getNumberCount(tab.unreadCount)}
|
{getNumberCount(tab.unreadCount)}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
@ -241,16 +245,13 @@ export const NotificationPopover = () => {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full flex-1 overflow-y-auto">
|
|
||||||
{notifications ? (
|
{notifications ? (
|
||||||
notifications.filter(
|
notifications.length > 0 ? (
|
||||||
(notification) => notification.data.issue_activity.field !== "None"
|
<div className="divide-y divide-custom-border-100 overflow-y-auto">
|
||||||
).length > 0 ? (
|
{notifications.map((notification) => (
|
||||||
notifications.map((notification) => (
|
|
||||||
<NotificationCard
|
<NotificationCard
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
notification={notification}
|
notification={notification}
|
||||||
@ -259,30 +260,27 @@ export const NotificationPopover = () => {
|
|||||||
setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
|
setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
|
||||||
markSnoozeNotification={markSnoozeNotification}
|
markSnoozeNotification={markSnoozeNotification}
|
||||||
/>
|
/>
|
||||||
))
|
))}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col w-full h-full justify-center items-center">
|
<div className="grid h-full w-full place-items-center overflow-hidden">
|
||||||
<Image
|
<EmptyState
|
||||||
src="/empty-state/empty-notification.svg"
|
title="You're updated with all the notifications"
|
||||||
alt="Empty"
|
description="You have read all the notifications."
|
||||||
width={200}
|
image={emptyNotification}
|
||||||
height={200}
|
isFullScreen={false}
|
||||||
layout="fixed"
|
|
||||||
/>
|
/>
|
||||||
<h4 className="text-custom-text-300 text-lg font-semibold">
|
|
||||||
You{"'"}re updated with all the notifications
|
|
||||||
</h4>
|
|
||||||
<p className="text-custom-text-300 text-sm mt-2">
|
|
||||||
You have read all the notifications.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full h-full justify-center items-center">
|
<Loader className="p-5 space-y-4">
|
||||||
<Spinner />
|
<Loader.Item height="50px" />
|
||||||
</div>
|
<Loader.Item height="50px" />
|
||||||
|
<Loader.Item height="50px" />
|
||||||
|
<Loader.Item height="50px" />
|
||||||
|
<Loader.Item height="50px" />
|
||||||
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</>
|
</>
|
||||||
|
@ -9,13 +9,19 @@ import { useForm, Controller } from "react-hook-form";
|
|||||||
import { Transition, Dialog, Listbox } from "@headlessui/react";
|
import { Transition, Dialog, Listbox } from "@headlessui/react";
|
||||||
|
|
||||||
// date helper
|
// date helper
|
||||||
import { getDatesAfterCurrentDate, getTimestampAfterCurrentTime } from "helpers/date-time.helper";
|
import { getAllTimeIn30MinutesInterval } from "helpers/date-time.helper";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { PrimaryButton, SecondaryButton, Icon } from "components/ui";
|
import {
|
||||||
|
PrimaryButton,
|
||||||
|
SecondaryButton,
|
||||||
|
Icon,
|
||||||
|
CustomDatePicker,
|
||||||
|
CustomSelect,
|
||||||
|
} from "components/ui";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { IUserNotification } from "types";
|
import type { IUserNotification } from "types";
|
||||||
@ -28,14 +34,20 @@ type SnoozeModalProps = {
|
|||||||
onSubmit: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
|
onSubmit: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const dates = getDatesAfterCurrentDate();
|
type FormValues = {
|
||||||
const timeStamps = getTimestampAfterCurrentTime();
|
time: string | null;
|
||||||
|
date: Date | null;
|
||||||
|
period: "AM" | "PM";
|
||||||
|
};
|
||||||
|
|
||||||
const defaultValues = {
|
const defaultValues: FormValues = {
|
||||||
time: null,
|
time: null,
|
||||||
date: null,
|
date: null,
|
||||||
|
period: "AM",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const timeStamps = getAllTimeIn30MinutesInterval();
|
||||||
|
|
||||||
export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
|
export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
|
||||||
const { isOpen, onClose, notification, onSuccess, onSubmit: handleSubmitSnooze } = props;
|
const { isOpen, onClose, notification, onSuccess, onSubmit: handleSubmitSnooze } = props;
|
||||||
|
|
||||||
@ -49,19 +61,57 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
|
|||||||
reset,
|
reset,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
} = useForm<any>({
|
watch,
|
||||||
|
setValue,
|
||||||
|
} = useForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async (formData: any) => {
|
const getTimeStamp = () => {
|
||||||
if (!workspaceSlug || !notification) return;
|
const today = new Date();
|
||||||
|
const formDataDate = watch("date");
|
||||||
|
|
||||||
const dateTime = new Date(
|
if (!formDataDate) return timeStamps;
|
||||||
`${formData.date.toLocaleDateString()} ${formData.time.toLocaleTimeString()}`
|
|
||||||
|
const isToday = today.toDateString() === new Date(formDataDate).toDateString();
|
||||||
|
|
||||||
|
if (!isToday) return timeStamps;
|
||||||
|
|
||||||
|
const hours = today.getHours();
|
||||||
|
const minutes = today.getMinutes();
|
||||||
|
|
||||||
|
return timeStamps.filter((optionTime) => {
|
||||||
|
let optionHours = parseInt(optionTime.value.split(":")[0]);
|
||||||
|
const optionMinutes = parseInt(optionTime.value.split(":")[1]);
|
||||||
|
|
||||||
|
const period = watch("period");
|
||||||
|
|
||||||
|
if (period === "PM" && optionHours !== 12) optionHours += 12;
|
||||||
|
|
||||||
|
if (optionHours < hours) return false;
|
||||||
|
if (optionHours === hours && optionMinutes < minutes) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (formData: FormValues) => {
|
||||||
|
if (!workspaceSlug || !notification || !formData.date || !formData.time) return;
|
||||||
|
|
||||||
|
const period = formData.period;
|
||||||
|
|
||||||
|
const time = formData.time.split(":");
|
||||||
|
const hours = parseInt(
|
||||||
|
`${period === "AM" ? time[0] : parseInt(time[0]) + 12 === 24 ? "00" : parseInt(time[0]) + 12}`
|
||||||
);
|
);
|
||||||
|
const minutes = parseInt(time[1]);
|
||||||
|
|
||||||
|
const dateTime = new Date(formData.date);
|
||||||
|
dateTime.setHours(hours);
|
||||||
|
dateTime.setMinutes(minutes);
|
||||||
|
|
||||||
await handleSubmitSnooze(notification.id, dateTime).then(() => {
|
await handleSubmitSnooze(notification.id, dateTime).then(() => {
|
||||||
onClose();
|
handleClose();
|
||||||
onSuccess();
|
onSuccess();
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Notification snoozed",
|
title: "Notification snoozed",
|
||||||
@ -74,7 +124,7 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
|
|||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose();
|
onClose();
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
reset();
|
reset({ ...defaultValues });
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
@ -105,7 +155,7 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-100 bg-custom-background-80 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-100 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
@ -116,191 +166,104 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
|
|||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button type="button">
|
<button type="button" onClick={handleClose}>
|
||||||
<Icon iconName="close" className="w-5 h-5 text-custom-text-100" />
|
<Icon iconName="close" className="w-5 h-5 text-custom-text-100" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 flex gap-3">
|
<div className="mt-5 flex items-center gap-3">
|
||||||
<div className="flex-1">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="time"
|
|
||||||
rules={{ required: "Please select a time" }}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<Listbox value={value} onChange={onChange}>
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<div className="relative mt-2">
|
|
||||||
<Listbox.Button className="relative w-full cursor-default rounded-md border border-custom-border-100 bg-custom-background-100 py-1.5 pl-3 pr-10 text-left text-custom-text-100 shadow-sm focus:outline-none sm:text-sm sm:leading-6">
|
|
||||||
<span className="flex items-center">
|
|
||||||
<span className="ml-3 block truncate">
|
|
||||||
{value
|
|
||||||
? new Date(value)?.toLocaleTimeString([], {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
})
|
|
||||||
: "Select Time"}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2">
|
|
||||||
<Icon
|
|
||||||
iconName="expand_more"
|
|
||||||
className="h-5 w-5 text-custom-text-100"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Listbox.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
show={open}
|
|
||||||
as={Fragment}
|
|
||||||
leave="transition ease-in duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Listbox.Options className="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-custom-background-100 py-1 text-base shadow-lg focus:outline-none sm:text-sm">
|
|
||||||
{timeStamps.map((time, index) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={`${time.label}-${index}`}
|
|
||||||
className={({ active }) =>
|
|
||||||
`relative cursor-default select-none py-2 pl-3 pr-9 ${
|
|
||||||
active
|
|
||||||
? "bg-custom-primary-100/80 text-custom-text-100"
|
|
||||||
: "text-custom-text-700"
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
value={time.value}
|
|
||||||
>
|
|
||||||
{({ selected, active }) => (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span
|
|
||||||
className={`ml-3 block truncate ${
|
|
||||||
selected ? "font-semibold" : "font-normal"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{time.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selected ? (
|
|
||||||
<span
|
|
||||||
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
|
||||||
active
|
|
||||||
? "text-custom-text-100"
|
|
||||||
: "text-custom-primary-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
iconName="done"
|
|
||||||
className="h-5 w-5"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
<h6 className="block text-sm font-medium text-custom-text-400 mb-2">
|
||||||
|
Pick a date
|
||||||
|
</h6>
|
||||||
<Controller
|
<Controller
|
||||||
name="date"
|
name="date"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: "Please select a date" }}
|
rules={{ required: "Please select a date" }}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<Listbox value={value} onChange={onChange}>
|
<CustomDatePicker
|
||||||
{({ open }) => (
|
placeholder="Select date"
|
||||||
<>
|
value={value}
|
||||||
<div className="relative mt-2">
|
onChange={(val) => {
|
||||||
<Listbox.Button className="relative w-full cursor-default rounded-md border border-custom-border-100 bg-custom-background-100 py-1.5 pl-3 pr-10 text-left text-custom-text-100 shadow-sm focus:outline-none sm:text-sm sm:leading-6">
|
setValue("time", null);
|
||||||
<span className="flex items-center">
|
onChange(val);
|
||||||
<span className="ml-3 block truncate">
|
}}
|
||||||
{value
|
className="px-3 py-2 w-full rounded-md border border-custom-border-300 bg-custom-background-100 text-custom-text-100 focus:outline-none !text-sm"
|
||||||
? new Date(value)?.toLocaleDateString([], {
|
noBorder
|
||||||
day: "numeric",
|
minDate={new Date()}
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
})
|
|
||||||
: "Select Date"}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2">
|
|
||||||
<Icon
|
|
||||||
iconName="expand_more"
|
|
||||||
className="h-5 w-5 text-custom-text-100"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h6 className="block text-sm font-medium text-custom-text-400 mb-2">
|
||||||
|
Pick a time
|
||||||
|
</h6>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="time"
|
||||||
|
rules={{ required: "Please select a time" }}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<CustomSelect
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
label={
|
||||||
|
<div className="truncate">
|
||||||
|
{value ? (
|
||||||
|
<span>
|
||||||
|
{value} {watch("period").toLowerCase()}
|
||||||
</span>
|
</span>
|
||||||
</Listbox.Button>
|
) : (
|
||||||
|
<span className="text-custom-text-400 text-sm">
|
||||||
<Transition
|
Select a time
|
||||||
show={open}
|
</span>
|
||||||
as={Fragment}
|
)}
|
||||||
leave="transition ease-in duration-100"
|
</div>
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Listbox.Options className="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-custom-background-100 py-1 text-base shadow-lg focus:outline-none sm:text-sm">
|
|
||||||
{dates.map((date, index) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={`${date.label}-${index}`}
|
|
||||||
className={({ active }) =>
|
|
||||||
`relative cursor-default select-none py-2 pl-3 pr-9 ${
|
|
||||||
active
|
|
||||||
? "bg-custom-primary-100/80 text-custom-text-100"
|
|
||||||
: "text-custom-text-700"
|
|
||||||
}`
|
|
||||||
}
|
}
|
||||||
value={date.value}
|
width="w-full"
|
||||||
|
input
|
||||||
>
|
>
|
||||||
{({ selected, active }) => (
|
<div className="w-full rounded overflow-hidden h-9 mb-2 flex">
|
||||||
<>
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
setValue("period", "AM");
|
||||||
|
}}
|
||||||
|
className={`w-1/2 h-full cursor-pointer flex justify-center items-center text-center ${
|
||||||
|
watch("period") === "AM"
|
||||||
|
? "bg-custom-primary-100/90 text-custom-primary-0"
|
||||||
|
: "bg-custom-background-80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
AM
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
setValue("period", "PM");
|
||||||
|
}}
|
||||||
|
className={`w-1/2 h-full cursor-pointer flex justify-center items-center text-center ${
|
||||||
|
watch("period") === "PM"
|
||||||
|
? "bg-custom-primary-100/90 text-custom-primary-0"
|
||||||
|
: "bg-custom-background-80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
PM
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{getTimeStamp().length > 0 ? (
|
||||||
|
getTimeStamp().map((time, index) => (
|
||||||
|
<CustomSelect.Option key={`${time}-${index}`} value={time.value}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span
|
<span className="ml-3 block truncate">{time.label}</span>
|
||||||
className={`ml-3 block truncate ${
|
|
||||||
selected ? "font-semibold" : "font-normal"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{date.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</CustomSelect.Option>
|
||||||
{selected ? (
|
))
|
||||||
<span
|
) : (
|
||||||
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
<p className="text-custom-text-200 text-center p-3">
|
||||||
active
|
No available time for this date.
|
||||||
? "text-custom-text-100"
|
</p>
|
||||||
: "text-custom-primary-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
iconName="done"
|
|
||||||
className="h-5 w-5"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Listbox.Option>
|
</CustomSelect>
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,6 +15,7 @@ type Props = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
isClearable?: boolean;
|
isClearable?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
minDate?: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CustomDatePicker: React.FC<Props> = ({
|
export const CustomDatePicker: React.FC<Props> = ({
|
||||||
@ -28,6 +29,7 @@ export const CustomDatePicker: React.FC<Props> = ({
|
|||||||
className = "",
|
className = "",
|
||||||
isClearable = true,
|
isClearable = true,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
minDate,
|
||||||
}) => (
|
}) => (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
placeholderText={placeholder}
|
placeholderText={placeholder}
|
||||||
@ -52,5 +54,6 @@ export const CustomDatePicker: React.FC<Props> = ({
|
|||||||
dateFormat="MMM dd, yyyy"
|
dateFormat="MMM dd, yyyy"
|
||||||
isClearable={isClearable}
|
isClearable={isClearable}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
minDate={minDate}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -13,6 +13,7 @@ export type CustomMenuProps = DropdownProps & {
|
|||||||
ellipsis?: boolean;
|
ellipsis?: boolean;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
verticalEllipsis?: boolean;
|
verticalEllipsis?: boolean;
|
||||||
|
menuButtonOnClick?: (...args: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomMenu = ({
|
const CustomMenu = ({
|
||||||
@ -32,17 +33,21 @@ const CustomMenu = ({
|
|||||||
verticalEllipsis = false,
|
verticalEllipsis = false,
|
||||||
verticalPosition = "bottom",
|
verticalPosition = "bottom",
|
||||||
width = "auto",
|
width = "auto",
|
||||||
|
menuButtonOnClick,
|
||||||
}: CustomMenuProps) => (
|
}: CustomMenuProps) => (
|
||||||
<Menu as="div" className={`${selfPositioned ? "" : "relative"} w-min text-left ${className}`}>
|
<Menu as="div" className={`${selfPositioned ? "" : "relative"} w-min text-left ${className}`}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
{customButton ? (
|
{customButton ? (
|
||||||
<Menu.Button as="div">{customButton}</Menu.Button>
|
<Menu.Button as="button" onClick={menuButtonOnClick}>
|
||||||
|
{customButton}
|
||||||
|
</Menu.Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{ellipsis || verticalEllipsis ? (
|
{ellipsis || verticalEllipsis ? (
|
||||||
<Menu.Button
|
<Menu.Button
|
||||||
type="button"
|
type="button"
|
||||||
|
onClick={menuButtonOnClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={`relative grid place-items-center rounded p-1 text-custom-text-200 outline-none ${
|
className={`relative grid place-items-center rounded p-1 text-custom-text-200 outline-none ${
|
||||||
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
|
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
|
||||||
@ -59,7 +64,7 @@ const CustomMenu = ({
|
|||||||
className={`flex items-center justify-between gap-1 rounded-md px-2.5 py-1 text-xs whitespace-nowrap duration-300 ${
|
className={`flex items-center justify-between gap-1 rounded-md px-2.5 py-1 text-xs whitespace-nowrap duration-300 ${
|
||||||
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
|
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
|
||||||
} ${
|
} ${
|
||||||
noBorder ? "" : "border border-custom-border-100 shadow-sm focus:outline-none"
|
noBorder ? "" : "border border-custom-border-300 shadow-sm focus:outline-none"
|
||||||
} ${
|
} ${
|
||||||
disabled
|
disabled
|
||||||
? "cursor-not-allowed text-custom-text-200"
|
? "cursor-not-allowed text-custom-text-200"
|
||||||
@ -83,7 +88,7 @@ const CustomMenu = ({
|
|||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items
|
<Menu.Items
|
||||||
className={`absolute z-10 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-100 p-1 text-xs shadow-lg focus:outline-none bg-custom-background-90 ${
|
className={`absolute z-10 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 p-1 text-xs shadow-lg focus:outline-none bg-custom-background-90 ${
|
||||||
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
||||||
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
||||||
maxHeight === "lg"
|
maxHeight === "lg"
|
||||||
|
@ -69,7 +69,7 @@ export const CustomSearchSelect = ({
|
|||||||
<Combobox.Button as="div">{customButton}</Combobox.Button>
|
<Combobox.Button as="div">{customButton}</Combobox.Button>
|
||||||
) : (
|
) : (
|
||||||
<Combobox.Button
|
<Combobox.Button
|
||||||
className={`flex items-center justify-between gap-1 w-full rounded-md shadow-sm border border-custom-border-100 duration-300 focus:outline-none ${
|
className={`flex items-center justify-between gap-1 w-full rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
|
||||||
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
||||||
} ${
|
} ${
|
||||||
disabled
|
disabled
|
||||||
@ -94,7 +94,7 @@ export const CustomSearchSelect = ({
|
|||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Combobox.Options
|
<Combobox.Options
|
||||||
className={`absolute z-10 min-w-[10rem] border border-custom-border-100 p-2 rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
|
className={`absolute z-10 min-w-[10rem] border border-custom-border-300 p-2 rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
|
||||||
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
||||||
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
||||||
width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width
|
width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width
|
||||||
|
@ -44,7 +44,7 @@ const CustomSelect = ({
|
|||||||
<Listbox.Button as="div">{customButton}</Listbox.Button>
|
<Listbox.Button as="div">{customButton}</Listbox.Button>
|
||||||
) : (
|
) : (
|
||||||
<Listbox.Button
|
<Listbox.Button
|
||||||
className={`flex items-center justify-between gap-1 w-full rounded-md border border-custom-border-100 shadow-sm duration-300 focus:outline-none ${
|
className={`flex items-center justify-between gap-1 w-full rounded-md border border-custom-border-300 shadow-sm duration-300 focus:outline-none ${
|
||||||
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
||||||
} ${
|
} ${
|
||||||
disabled
|
disabled
|
||||||
@ -68,7 +68,7 @@ const CustomSelect = ({
|
|||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Listbox.Options
|
<Listbox.Options
|
||||||
className={`absolute z-10 border border-custom-border-100 mt-1 origin-top-right overflow-y-auto rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
|
className={`absolute z-10 border border-custom-border-300 mt-1 origin-top-right overflow-y-auto rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
|
||||||
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
||||||
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
||||||
maxHeight === "lg"
|
maxHeight === "lg"
|
||||||
|
@ -9,7 +9,7 @@ type Props = {
|
|||||||
title: string;
|
title: string;
|
||||||
description: React.ReactNode | string;
|
description: React.ReactNode | string;
|
||||||
image: any;
|
image: any;
|
||||||
buttonText: string;
|
buttonText?: string;
|
||||||
buttonIcon?: any;
|
buttonIcon?: any;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
isFullScreen?: boolean;
|
isFullScreen?: boolean;
|
||||||
@ -33,10 +33,12 @@ export const EmptyState: React.FC<Props> = ({
|
|||||||
<Image src={image} className="w-52 sm:w-60" alt={buttonText} />
|
<Image src={image} className="w-52 sm:w-60" alt={buttonText} />
|
||||||
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">{title}</h6>
|
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">{title}</h6>
|
||||||
<p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>
|
<p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>
|
||||||
|
{buttonText && (
|
||||||
<PrimaryButton className="flex items-center gap-1.5" onClick={onClick}>
|
<PrimaryButton className="flex items-center gap-1.5" onClick={onClick}>
|
||||||
{buttonIcon}
|
{buttonIcon}
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -36,7 +36,7 @@ export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => (
|
|||||||
activeInnerRadiusOffset={5}
|
activeInnerRadiusOffset={5}
|
||||||
colors={(datum) => datum.data.color}
|
colors={(datum) => datum.data.color}
|
||||||
tooltip={(datum) => (
|
tooltip={(datum) => (
|
||||||
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs">
|
||||||
<span className="text-custom-text-200 capitalize">{datum.datum.label} issues:</span>{" "}
|
<span className="text-custom-text-200 capitalize">{datum.datum.label} issues:</span>{" "}
|
||||||
{datum.datum.value}
|
{datum.datum.value}
|
||||||
</div>
|
</div>
|
||||||
@ -59,7 +59,7 @@ export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => (
|
|||||||
className="h-2 w-2"
|
className="h-2 w-2"
|
||||||
style={{ backgroundColor: STATE_GROUP_COLORS[cell.state_group] }}
|
style={{ backgroundColor: STATE_GROUP_COLORS[cell.state_group] }}
|
||||||
/>
|
/>
|
||||||
<div className="capitalize text-custom-text-200 text-xs">
|
<div className="capitalize text-custom-text-200 text-xs whitespace-nowrap">
|
||||||
{cell.state_group}- {cell.state_count}
|
{cell.state_group}- {cell.state_count}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -243,7 +243,7 @@ export const isDateGreaterThanToday = (dateStr: string) => {
|
|||||||
return date > today;
|
return date > today;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderLongDateFormat = (dateString: string) => {
|
export const renderLongDateFormat = (dateString: string | Date) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
const day = date.getDate();
|
const day = date.getDate();
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
@ -333,3 +333,44 @@ export const getDatesAfterCurrentDate = (): Array<{
|
|||||||
|
|
||||||
export const checkIfStringIsDate = (date: string): boolean =>
|
export const checkIfStringIsDate = (date: string): boolean =>
|
||||||
new Date(date).toString() !== "Invalid Date";
|
new Date(date).toString() !== "Invalid Date";
|
||||||
|
|
||||||
|
// return an array of dates starting from 12:00 to 23:30 with 30 minutes interval as dates
|
||||||
|
export const getDatesWith30MinutesInterval = (): Array<Date> => {
|
||||||
|
const dates = [];
|
||||||
|
const current = new Date();
|
||||||
|
for (let i = 0; i < 24; i++) {
|
||||||
|
const newDate = new Date(current.getTime() + i * 60 * 60 * 1000);
|
||||||
|
dates.push(newDate);
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllTimeIn30MinutesInterval = (): Array<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}> => [
|
||||||
|
{ label: "12:00", value: "12:00" },
|
||||||
|
{ label: "12:30", value: "12:30" },
|
||||||
|
{ label: "01:00", value: "01:00" },
|
||||||
|
{ label: "01:30", value: "01:30" },
|
||||||
|
{ label: "02:00", value: "02:00" },
|
||||||
|
{ label: "02:30", value: "02:30" },
|
||||||
|
{ label: "03:00", value: "03:00" },
|
||||||
|
{ label: "03:30", value: "03:30" },
|
||||||
|
{ label: "04:00", value: "04:00" },
|
||||||
|
{ label: "04:30", value: "04:30" },
|
||||||
|
{ label: "05:00", value: "05:00" },
|
||||||
|
{ label: "05:30", value: "05:30" },
|
||||||
|
{ label: "06:00", value: "06:00" },
|
||||||
|
{ label: "06:30", value: "06:30" },
|
||||||
|
{ label: "07:00", value: "07:00" },
|
||||||
|
{ label: "07:30", value: "07:30" },
|
||||||
|
{ label: "08:00", value: "08:00" },
|
||||||
|
{ label: "08:30", value: "08:30" },
|
||||||
|
{ label: "09:00", value: "09:00" },
|
||||||
|
{ label: "09:30", value: "09:30" },
|
||||||
|
{ label: "10:00", value: "10:00" },
|
||||||
|
{ label: "10:30", value: "10:30" },
|
||||||
|
{ label: "11:00", value: "11:00" },
|
||||||
|
{ label: "11:30", value: "11:30" },
|
||||||
|
];
|
||||||
|
@ -45,8 +45,6 @@ const useUserIssueNotificationSubscription = (
|
|||||||
}, [workspaceSlug, projectId, issueId, mutate]);
|
}, [workspaceSlug, projectId, issueId, mutate]);
|
||||||
|
|
||||||
const handleSubscribe = useCallback(() => {
|
const handleSubscribe = useCallback(() => {
|
||||||
console.log(workspaceSlug, projectId, issueId, user);
|
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId || !issueId || !user) return;
|
if (!workspaceSlug || !projectId || !issueId || !user) return;
|
||||||
|
|
||||||
userNotificationServices
|
userNotificationServices
|
||||||
|
@ -26,23 +26,17 @@ const useUserNotification = () => {
|
|||||||
);
|
);
|
||||||
const [selectedTab, setSelectedTab] = useState<NotificationType>("assigned");
|
const [selectedTab, setSelectedTab] = useState<NotificationType>("assigned");
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
type: snoozed || archived || readNotification ? undefined : selectedTab,
|
||||||
|
snoozed,
|
||||||
|
archived,
|
||||||
|
read: !readNotification ? undefined : false,
|
||||||
|
};
|
||||||
|
|
||||||
const { data: notifications, mutate: notificationsMutate } = useSWR(
|
const { data: notifications, mutate: notificationsMutate } = useSWR(
|
||||||
|
workspaceSlug ? USER_WORKSPACE_NOTIFICATIONS(workspaceSlug.toString(), params) : null,
|
||||||
workspaceSlug
|
workspaceSlug
|
||||||
? USER_WORKSPACE_NOTIFICATIONS(workspaceSlug.toString(), {
|
? () => userNotificationServices.getUserNotifications(workspaceSlug.toString(), params)
|
||||||
type: selectedTab,
|
|
||||||
snoozed,
|
|
||||||
archived,
|
|
||||||
read: selectedTab === null ? !readNotification : undefined,
|
|
||||||
})
|
|
||||||
: null,
|
|
||||||
workspaceSlug
|
|
||||||
? () =>
|
|
||||||
userNotificationServices.getUserNotifications(workspaceSlug.toString(), {
|
|
||||||
type: selectedTab,
|
|
||||||
snoozed,
|
|
||||||
archived,
|
|
||||||
read: selectedTab === null ? !readNotification : undefined,
|
|
||||||
})
|
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -69,13 +69,13 @@ const WorkspacePage: NextPage = () => {
|
|||||||
return (
|
return (
|
||||||
<WorkspaceAuthorizationLayout
|
<WorkspaceAuthorizationLayout
|
||||||
left={
|
left={
|
||||||
<div className="flex items-center gap-2 px-3">
|
<div className="flex items-center gap-2 pl-3">
|
||||||
<Icon iconName="grid_view" />
|
<Icon iconName="grid_view" />
|
||||||
Dashboard
|
Dashboard
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3 px-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsProductUpdatesModalOpen(true)}
|
onClick={() => setIsProductUpdatesModalOpen(true)}
|
||||||
className="flex items-center gap-1.5 bg-custom-background-80 text-xs font-medium py-1.5 px-3 rounded"
|
className="flex items-center gap-1.5 bg-custom-background-80 text-xs font-medium py-1.5 px-3 rounded"
|
||||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
Loading…
Reference in New Issue
Block a user