feat(frontend): cancel movie request modal

also includes tons of performance fixes for the modals
pull/132/head
sct 4 years ago
parent 61b6152e89
commit 1f9cbbfdf1

@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 326 B

@ -14,10 +14,10 @@ const Badge: React.FC<BadgeProps> = ({ badgeType = 'default', children }) => {
badgeStyle.push('bg-red-600 text-red-100');
break;
case 'warning':
badgeStyle.push('bg-orange-400 text-orange-100');
badgeStyle.push('bg-orange-500 text-orange-100');
break;
case 'success':
badgeStyle.push('bg-green-500 text-green-100');
badgeStyle.push('bg-green-400 text-green-100');
break;
default:
badgeStyle.push('bg-indigo-500 text-indigo-100');

@ -6,16 +6,23 @@ import { useLockBodyScroll } from '../../../hooks/useLockBodyScroll';
import LoadingSpinner from '../LoadingSpinner';
import useClickOutside from '../../../hooks/useClickOutside';
interface ModalProps {
interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
title?: string;
onCancel?: (e?: MouseEvent<HTMLElement>) => void;
onOk?: (e: MouseEvent<HTMLButtonElement>) => void;
onOk?: (e?: MouseEvent<HTMLButtonElement>) => void;
onSecondary?: (e?: MouseEvent<HTMLButtonElement>) => void;
onTertiary?: (e?: MouseEvent<HTMLButtonElement>) => void;
cancelText?: string;
okText?: string;
secondaryText?: string;
tertiaryText?: string;
okDisabled?: boolean;
cancelButtonType?: ButtonType;
okButtonType?: ButtonType;
visible?: boolean;
secondaryButtonType?: ButtonType;
secondaryDisabled?: boolean;
tertiaryDisabled?: boolean;
tertiaryButtonType?: ButtonType;
disableScrollLock?: boolean;
backgroundClickable?: boolean;
iconSvg?: ReactNode;
@ -29,14 +36,22 @@ const Modal: React.FC<ModalProps> = ({
cancelText,
okText,
okDisabled = false,
cancelButtonType,
okButtonType,
cancelButtonType = 'default',
okButtonType = 'primary',
children,
visible,
disableScrollLock,
backgroundClickable = true,
iconSvg,
loading = false,
secondaryButtonType = 'default',
secondaryDisabled = false,
onSecondary,
secondaryText,
tertiaryButtonType = 'default',
tertiaryDisabled = false,
tertiaryText,
onTertiary,
...props
}) => {
const modalRef = useRef<HTMLDivElement>(null);
useClickOutside(modalRef, () => {
@ -44,123 +59,129 @@ const Modal: React.FC<ModalProps> = ({
? onCancel()
: undefined;
});
useLockBodyScroll(!!visible, disableScrollLock);
const transitions = useTransition(visible, null, {
from: { opacity: 0, backdropFilter: 'blur(0px)' },
enter: { opacity: 1, backdropFilter: 'blur(3px)' },
leave: { opacity: 0, backdropFilter: 'blur(0px)' },
config: { tension: 500, velocity: 40, friction: 60 },
});
const containerTransitions = useTransition(visible && !loading, null, {
useLockBodyScroll(true, disableScrollLock);
const containerTransitions = useTransition(!loading, null, {
from: { opacity: 0, transform: 'scale(0.5)' },
enter: { opacity: 1, transform: 'scale(1)' },
leave: { opacity: 0, transform: 'scale(0.5)' },
config: { tension: 500, velocity: 40, friction: 60 },
});
const loadingTransitions = useTransition(visible && loading, null, {
const loadingTransitions = useTransition(loading, null, {
from: { opacity: 0, transform: 'scale(0.5)' },
enter: { opacity: 1, transform: 'scale(1)' },
leave: { opacity: 0, transform: 'scale(0.5)' },
config: { tension: 500, velocity: 40, friction: 60 },
});
const cancelType = cancelButtonType ?? 'default';
const okType = okButtonType ?? 'primary';
return (
<>
{transitions.map(
({ props, item, key }) =>
item &&
ReactDOM.createPortal(
<animated.div
className="fixed top-0 left-0 right-0 bottom-0 bg-cool-gray-800 bg-opacity-50 w-full h-full z-50 flex justify-center items-center"
style={props}
key={key}
onKeyDown={(e) => {
if (e.key === 'Escape') {
typeof onCancel === 'function' && backgroundClickable
? onCancel()
: undefined;
}
}}
>
{loadingTransitions.map(
({ props, item, key }) =>
item && (
<animated.div
style={{ ...props, position: 'absolute' }}
key={key}
>
<LoadingSpinner />
</animated.div>
)
)}
{containerTransitions.map(
({ props, item, key }) =>
item && (
<animated.div
style={props}
className="inline-block align-bottom bg-cool-gray-700 sm:rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full sm:p-6"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
key={key}
ref={modalRef}
>
<div className="sm:flex sm:items-center">
{iconSvg && (
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-cool-gray-600 text-white sm:mx-0 sm:h-10 sm:w-10">
{iconSvg}
</div>
)}
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
{title && (
<h3
className="text-lg leading-6 font-medium text-white"
id="modal-headline"
>
{title}
</h3>
)}
</div>
{ReactDOM.createPortal(
<animated.div
className="fixed top-0 left-0 right-0 bottom-0 bg-cool-gray-800 bg-opacity-50 w-full h-full z-50 flex justify-center items-center"
style={props.style}
onKeyDown={(e) => {
if (e.key === 'Escape') {
typeof onCancel === 'function' && backgroundClickable
? onCancel()
: undefined;
}
}}
>
{loadingTransitions.map(
({ props, item, key }) =>
item && (
<animated.div
style={{ ...props, position: 'absolute' }}
key={key}
>
<LoadingSpinner />
</animated.div>
)
)}
{containerTransitions.map(
({ props, item, key }) =>
item && (
<animated.div
style={props}
className="inline-block align-bottom bg-cool-gray-700 sm:rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full sm:p-6"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
key={key}
ref={modalRef}
>
<div className="sm:flex sm:items-center">
{iconSvg && (
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-cool-gray-600 text-white sm:mx-0 sm:h-10 sm:w-10">
{iconSvg}
</div>
{children && (
<div className="mt-4">
<p className="text-sm leading-5 text-cool-gray-300">
{children}
</p>
</div>
)}
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
{title && (
<h3
className="text-lg leading-6 font-medium text-white"
id="modal-headline"
>
{title}
</h3>
)}
</div>
</div>
{children && (
<div className="mt-4">
<p className="text-sm leading-5 text-cool-gray-300">
{children}
</p>
</div>
)}
{(onCancel || onOk || onSecondary || onTertiary) && (
<div className="mt-5 sm:mt-4 flex justify-center sm:justify-start flex-row-reverse">
{typeof onOk === 'function' && (
<Button
buttonType={okButtonType}
onClick={onOk}
className="ml-3"
disabled={okDisabled}
>
{okText ? okText : 'Ok'}
</Button>
)}
{typeof onSecondary === 'function' && secondaryText && (
<Button
buttonType={secondaryButtonType}
onClick={onSecondary}
className="ml-3"
disabled={secondaryDisabled}
>
{secondaryText}
</Button>
)}
{typeof onTertiary === 'function' && tertiaryText && (
<Button
buttonType={tertiaryButtonType}
onClick={onTertiary}
className="ml-3"
disabled={tertiaryDisabled}
>
{tertiaryText}
</Button>
)}
{(onCancel || onOk) && (
<div className="mt-5 sm:mt-4 flex justify-center sm:justify-start flex-row-reverse">
{typeof onOk === 'function' && (
<Button
buttonType={okType}
onClick={onOk}
className="ml-3"
disabled={okDisabled}
>
{okText ? okText : 'Ok'}
</Button>
)}
{typeof onCancel === 'function' && (
<Button
buttonType={cancelType}
onClick={onCancel}
className="ml-3 sm:ml-0 sm:px-4"
>
{cancelText ? cancelText : 'Cancel'}
</Button>
)}
</div>
{typeof onCancel === 'function' && (
<Button
buttonType={cancelButtonType}
onClick={onCancel}
className="ml-3 sm:ml-0 sm:px-4"
>
{cancelText ? cancelText : 'Cancel'}
</Button>
)}
</animated.div>
)
)}
</animated.div>,
document.body
)
</div>
)}
</animated.div>
)
)}
</animated.div>,
document.body
)}
</>
);

@ -38,6 +38,7 @@ const messages = defineMessages({
available: 'Available',
unavailable: 'Unavailable',
request: 'Request',
viewrequest: 'View Request',
pending: 'Pending',
overviewunavailable: 'Overview unavailable',
});
@ -53,13 +54,6 @@ interface SearchResult {
results: MovieResult[];
}
enum MediaRequestStatus {
PENDING = 1,
APPROVED,
DECLINED,
AVAILABLE,
}
const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
const { hasPermission } = useUser();
const router = useRouter();
@ -87,6 +81,11 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
return <div>Broken?</div>;
}
console.log(MediaStatus);
console.log(data);
const activeRequest = data?.mediaInfo?.requests?.[0];
return (
<div
className="bg-cover bg-center -mx-4 -mt-2 px-4 sm:px-8 pt-4 "
@ -144,102 +143,46 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</div>
<div className="flex-1 flex justify-end mt-4 md:mt-0">
{(!data.mediaInfo ||
data.mediaInfo?.status === MediaStatus.UNKNOWN) && (
data.mediaInfo?.status === MediaStatus.UNKNOWN ||
activeRequest) && (
<Button
buttonType="primary"
onClick={() => setShowRequestModal(true)}
>
<svg
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
<FormattedMessage {...messages.request} />
</Button>
)}
{data.mediaInfo?.status === MediaStatus.PENDING && (
<Button buttonType="warning">
<svg
className="w-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
<FormattedMessage {...messages.pending} />
</Button>
)}
{data.mediaInfo?.status === MediaStatus.PROCESSING && (
<Button buttonType="danger">
<svg
className="w-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<FormattedMessage {...messages.unavailable} />
</Button>
)}
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
<Button buttonType="success">
<svg
className="w-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<FormattedMessage {...messages.available} />
{activeRequest ? (
<svg
className="w-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
) : (
<svg
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
)}
<FormattedMessage
{...(activeRequest ? messages.viewrequest : messages.request)}
/>
</Button>
)}
<Button buttonType="danger" className="ml-2">
<svg
className="w-5"
style={{ height: 20 }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</Button>
{hasPermission(Permission.MANAGE_REQUESTS) && (
<Button buttonType="default" className="ml-2">
<svg

@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import Modal from '../Common/Modal';
import { useUser } from '../../hooks/useUser';
import { Permission } from '../../../server/lib/permissions';
@ -8,7 +8,11 @@ import useSWR from 'swr';
import { MovieDetails } from '../../../server/models/Movie';
import { useToasts } from 'react-toast-notifications';
import axios from 'axios';
import type { MediaStatus } from '../../../server/constants/media';
import {
MediaStatus,
MediaRequestStatus,
} from '../../../server/constants/media';
import DownloadIcon from '../../assets/download.svg';
const messages = defineMessages({
requestadmin:
@ -17,29 +21,28 @@ const messages = defineMessages({
'This will remove your request. Are you sure you want to continue?',
});
interface RequestModalProps {
request?: MediaRequest;
interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
tmdbId: number;
visible?: boolean;
onCancel?: () => void;
onComplete?: (newStatus: MediaStatus) => void;
onUpdating?: (isUpdating: boolean) => void;
}
const MovieRequestModal: React.FC<RequestModalProps> = ({
visible,
onCancel,
onComplete,
request,
tmdbId,
onUpdating,
...props
}) => {
const { addToast } = useToasts();
const { data, error } = useSWR<MovieDetails>(`/api/v1/movie/${tmdbId}`);
const { data, error } = useSWR<MovieDetails>(`/api/v1/movie/${tmdbId}`, {
revalidateOnMount: true,
});
const intl = useIntl();
const { hasPermission } = useUser();
const { user, hasPermission } = useUser();
const sendRequest = async () => {
const sendRequest = useCallback(async () => {
if (onUpdating) {
onUpdating(true);
}
@ -62,59 +65,76 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
onUpdating(false);
}
}
}, [data, onComplete, onUpdating, addToast]);
const activeRequest = data?.mediaInfo?.requests?.[0];
console.log(activeRequest);
const cancelRequest = async () => {
if (onUpdating) {
onUpdating(true);
}
const response = await axios.delete<MediaRequest>(
`/api/v1/request/${activeRequest?.id}`
);
if (response.data) {
if (onComplete) {
onComplete(MediaStatus.UNKNOWN);
}
addToast(
<span>
<strong>{data?.title}</strong> request cancelled!
</span>,
{ appearance: 'success', autoDismiss: true }
);
if (onUpdating) {
onUpdating(false);
}
}
};
let text = hasPermission(Permission.MANAGE_REQUESTS)
const isOwner = activeRequest
? activeRequest.requestedBy.id === user?.id ||
hasPermission(Permission.MANAGE_REQUESTS)
: false;
const text = hasPermission(Permission.MANAGE_REQUESTS)
? intl.formatMessage(messages.requestadmin)
: undefined;
if (request) {
text = intl.formatMessage(messages.cancelrequest);
if (activeRequest?.status === MediaRequestStatus.PENDING) {
return (
<Modal
loading={!data && !error}
backgroundClickable
onCancel={onCancel}
onOk={isOwner ? () => cancelRequest() : undefined}
title={`Pending request for ${data?.title}`}
okText={'Cancel Request'}
okButtonType={'danger'}
cancelText="Close"
iconSvg={<DownloadIcon className="w-6 h-6" />}
{...props}
>
There is currently a pending request from{' '}
<strong>{activeRequest.requestedBy.username}</strong>.
</Modal>
);
}
return (
<Modal
visible={visible}
loading={!data && !error}
backgroundClickable
onCancel={onCancel}
onOk={() => sendRequest()}
title={!request ? `Request ${data?.title}` : 'Cancel Request'}
okText={!request ? 'Request' : 'Cancel Request'}
okButtonType={!!request ? 'danger' : 'primary'}
iconSvg={
!request ? (
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
) : (
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
)
}
onOk={sendRequest}
title={`Request ${data?.title}`}
okText={'Request'}
okButtonType={'primary'}
iconSvg={<DownloadIcon className="w-6 h-6" />}
{...props}
>
{text}
</Modal>

@ -21,27 +21,22 @@ const messages = defineMessages({
'This will remove your request. Are you sure you want to continue?',
});
interface RequestModalProps {
request?: MediaRequest;
interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
tmdbId: number;
visible?: boolean;
onCancel?: () => void;
onComplete?: (newStatus: MediaStatus) => void;
onUpdating?: (isUpdating: boolean) => void;
}
const TvRequestModal: React.FC<RequestModalProps> = ({
visible,
onCancel,
onComplete,
request,
tmdbId,
onUpdating,
...props
}) => {
const { addToast } = useToasts();
const { data, error } = useSWR<TvDetails>(
visible ? `/api/v1/tv/${tmdbId}` : null
);
const { data, error } = useSWR<TvDetails>(`/api/v1/tv/${tmdbId}`);
const [selectedSeasons, setSelectedSeasons] = useState<number[]>([]);
const intl = useIntl();
const { hasPermission } = useUser();
@ -164,7 +159,6 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
return (
<Modal
visible={visible}
loading={!data && !error}
backgroundClickable
onCancel={onCancel}
@ -193,6 +187,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
/>
</svg>
}
{...props}
>
<div className="flex flex-col">
<div className="-mx-4 sm:mx-0 overflow-auto max-h-96">

@ -4,6 +4,7 @@ import MovieRequestModal from './MovieRequestModal';
import type { MediaRequest } from '../../../server/entity/MediaRequest';
import type { MediaStatus } from '../../../server/constants/media';
import TvRequestModal from './TvRequestModal';
import { useTransition, animated } from 'react-spring';
interface RequestModalProps {
show: boolean;
@ -24,26 +25,49 @@ const RequestModal: React.FC<RequestModalProps> = ({
onUpdating,
onCancel,
}) => {
const transitions = useTransition(show, null, {
from: { opacity: 0, backdropFilter: 'blur(0px)' },
enter: { opacity: 1, backdropFilter: 'blur(3px)' },
leave: { opacity: 0, backdropFilter: 'blur(0px)' },
config: { tension: 500, velocity: 40, friction: 60 },
});
if (type === 'tv') {
return (
<TvRequestModal
onComplete={onComplete}
onCancel={onCancel}
visible={show}
tmdbId={tmdbId}
onUpdating={onUpdating}
/>
<>
{transitions.map(
({ props, item, key }) =>
item && (
<TvRequestModal
onComplete={onComplete}
onCancel={onCancel}
tmdbId={tmdbId}
onUpdating={onUpdating}
style={props}
key={key}
/>
)
)}
</>
);
}
return (
<MovieRequestModal
onComplete={onComplete}
onCancel={onCancel}
visible={show}
tmdbId={tmdbId}
onUpdating={onUpdating}
/>
<>
{transitions.map(
({ props, item, key }) =>
item && (
<MovieRequestModal
onComplete={onComplete}
onCancel={onCancel}
tmdbId={tmdbId}
onUpdating={onUpdating}
style={props}
key={key}
/>
)
)}
</>
);
};

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useCallback } from 'react';
import type { MediaType } from '../../../server/models/Search';
import Available from '../../assets/available.svg';
import Requested from '../../assets/requested.svg';
@ -42,18 +42,27 @@ const TitleCard: React.FC<TitleCardProps> = ({
year = year.slice(0, 4);
}
const requestComplete = useCallback((newStatus: MediaStatus) => {
setCurrentStatus(newStatus);
setShowRequestModal(false);
}, []);
const requestUpdating = useCallback(
(status: boolean) => setIsUpdating(status),
[]
);
const closeModal = useCallback(() => setShowRequestModal(false), []);
return (
<div className="w-36 sm:w-36 md:w-44">
<RequestModal
tmdbId={id}
show={showRequestModal}
type={mediaType === 'movie' ? 'movie' : 'tv'}
onComplete={(newStatus) => {
setCurrentStatus(newStatus);
setShowRequestModal(false);
}}
onUpdating={(status) => setIsUpdating(status)}
onCancel={() => setShowRequestModal(false)}
onComplete={requestComplete}
onUpdating={requestUpdating}
onCancel={closeModal}
/>
<div
className="titleCard outline-none cursor-default"

@ -150,23 +150,6 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
<FormattedMessage {...messages.request} />
</Button>
)}
<Button buttonType="danger" className="ml-2">
<svg
className="w-5"
style={{ height: 20 }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</Button>
{hasPermission(Permission.MANAGE_REQUESTS) && (
<Button buttonType="default" className="ml-2">
<svg

Loading…
Cancel
Save