mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
16a7bd3bda
* 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
---------
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
326 lines
15 KiB
TypeScript
326 lines
15 KiB
TypeScript
import React, { Fragment } from "react";
|
|
|
|
// next
|
|
import { useRouter } from "next/router";
|
|
|
|
// react hook form
|
|
import { useForm, Controller } from "react-hook-form";
|
|
|
|
import { Transition, Dialog, Listbox } from "@headlessui/react";
|
|
|
|
// date helper
|
|
import { getDatesAfterCurrentDate, getTimestampAfterCurrentTime } from "helpers/date-time.helper";
|
|
|
|
// hooks
|
|
import useToast from "hooks/use-toast";
|
|
|
|
// components
|
|
import { PrimaryButton, SecondaryButton, Icon } from "components/ui";
|
|
|
|
// types
|
|
import type { IUserNotification } from "types";
|
|
|
|
type SnoozeModalProps = {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSuccess: () => void;
|
|
notification: IUserNotification | null;
|
|
onSubmit: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
|
|
};
|
|
|
|
const dates = getDatesAfterCurrentDate();
|
|
const timeStamps = getTimestampAfterCurrentTime();
|
|
|
|
const defaultValues = {
|
|
time: null,
|
|
date: null,
|
|
};
|
|
|
|
export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
|
|
const { isOpen, onClose, notification, onSuccess, onSubmit: handleSubmitSnooze } = props;
|
|
|
|
const router = useRouter();
|
|
const { workspaceSlug } = router.query;
|
|
|
|
const { setToastAlert } = useToast();
|
|
|
|
const {
|
|
formState: { isSubmitting },
|
|
reset,
|
|
handleSubmit,
|
|
control,
|
|
} = useForm<any>({
|
|
defaultValues,
|
|
});
|
|
|
|
const onSubmit = async (formData: any) => {
|
|
if (!workspaceSlug || !notification) return;
|
|
|
|
const dateTime = new Date(
|
|
`${formData.date.toLocaleDateString()} ${formData.time.toLocaleTimeString()}`
|
|
);
|
|
|
|
await handleSubmitSnooze(notification.id, dateTime).then(() => {
|
|
onClose();
|
|
onSuccess();
|
|
setToastAlert({
|
|
title: "Notification snoozed",
|
|
message: "Notification snoozed successfully",
|
|
type: "success",
|
|
});
|
|
});
|
|
};
|
|
|
|
const handleClose = () => {
|
|
onClose();
|
|
const timeout = setTimeout(() => {
|
|
reset();
|
|
clearTimeout(timeout);
|
|
}, 500);
|
|
};
|
|
|
|
return (
|
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
|
<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-custom-backdrop bg-opacity-50 transition-opacity" />
|
|
</Transition.Child>
|
|
|
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
|
<div className="flex items-center justify-center min-h-full p-4 text-center">
|
|
<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 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">
|
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
<div className="flex justify-between items-center">
|
|
<Dialog.Title
|
|
as="h3"
|
|
className="text-lg font-medium leading-6 text-custom-text-100"
|
|
>
|
|
Customize Snooze Time
|
|
</Dialog.Title>
|
|
|
|
<div>
|
|
<button type="button">
|
|
<Icon iconName="close" className="w-5 h-5 text-custom-text-100" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-5 flex 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">
|
|
<Controller
|
|
name="date"
|
|
control={control}
|
|
rules={{ required: "Please select a date" }}
|
|
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)?.toLocaleDateString([], {
|
|
day: "numeric",
|
|
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"
|
|
/>
|
|
</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">
|
|
{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}
|
|
>
|
|
{({ selected, active }) => (
|
|
<>
|
|
<div className="flex items-center">
|
|
<span
|
|
className={`ml-3 block truncate ${
|
|
selected ? "font-semibold" : "font-normal"
|
|
}`}
|
|
>
|
|
{date.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>
|
|
|
|
<div className="mt-5 flex items-center justify-between gap-2">
|
|
<div className="w-full flex items-center gap-2 justify-end">
|
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
|
{isSubmitting ? "Submitting..." : "Submit"}
|
|
</PrimaryButton>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</Dialog.Panel>
|
|
</Transition.Child>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</Transition.Root>
|
|
);
|
|
};
|