feat: approve hold and check out from hold
This commit is contained in:
parent
27c29949da
commit
c5e07e7c82
@ -14,9 +14,9 @@ const eslintConfig = [
|
|||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
'@next/next/no-img-element': 'off',
|
'@next/next/no-img-element': 'off',
|
||||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||||
'@typescript-eslint/no-explicit-any': 'warn',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'warn',
|
'warn',
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { HoldRequest } from "@/payload-types";
|
|
||||||
import { CollectionConfig } from "payload";
|
import { CollectionConfig } from "payload";
|
||||||
|
|
||||||
const HoldRequests: CollectionConfig = {
|
const HoldRequests: CollectionConfig = {
|
||||||
@ -56,8 +55,8 @@ const HoldRequests: CollectionConfig = {
|
|||||||
hooks:
|
hooks:
|
||||||
{
|
{
|
||||||
beforeValidate: [({ data, originalDoc }) => {
|
beforeValidate: [({ data, originalDoc }) => {
|
||||||
if (data?.isCheckedOut && !data.copy) return originalDoc
|
if (originalDoc.isCheckedOut) return originalDoc
|
||||||
|
if (data?.isCheckedOut && !originalDoc.copy) return originalDoc
|
||||||
if (originalDoc.isCheckedOut && !data?.isCheckedOut) return originalDoc
|
if (originalDoc.isCheckedOut && !data?.isCheckedOut) return originalDoc
|
||||||
}],
|
}],
|
||||||
afterChange: [({ value, data, req }) => {
|
afterChange: [({ value, data, req }) => {
|
||||||
@ -69,6 +68,8 @@ const HoldRequests: CollectionConfig = {
|
|||||||
copy: data?.copy,
|
copy: data?.copy,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,8 @@ import { z } from 'zod'
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { Button } from '../ui/button'
|
import { Button } from '../ui/button'
|
||||||
import { Select } from '@headlessui/react'
|
import { Select } from '@headlessui/react'
|
||||||
|
import approveHoldRequest from '@/serverActions/ApproveHoldRequests'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
@ -90,18 +92,21 @@ const ApproveHoldRequestModal = (props: Props) => {
|
|||||||
setAvailableCopies(availableCopies)
|
setAvailableCopies(availableCopies)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
availableCopies
|
|
||||||
console.error(err)
|
console.error(err)
|
||||||
})
|
})
|
||||||
.finally(() => {})
|
.finally(() => {})
|
||||||
}, [availableCopies, setAvailableCopies, isOpen])
|
}, [availableCopies, setAvailableCopies, isOpen, book.id, repository.id])
|
||||||
|
|
||||||
const onSubmit = (values: z.infer<typeof formSchema>) => {
|
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
const acceptHoldPayload = {
|
const updateRequest = await approveHoldRequest({
|
||||||
|
holdRequestId: hold.id,
|
||||||
copyId: parseInt(values.copyId, 10),
|
copyId: parseInt(values.copyId, 10),
|
||||||
untilDate: values.untilDate,
|
untilDate: values.untilDate,
|
||||||
}
|
})
|
||||||
console.log(acceptHoldPayload)
|
|
||||||
|
if (!updateRequest) toast('There was an issue approving that request.')
|
||||||
|
else toast('Approved')
|
||||||
|
|
||||||
onOpenChange()
|
onOpenChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +167,7 @@ const ApproveHoldRequestModal = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="mt-4 inline-flex items-center justify-center self-end rounded-lg bg-black px-4 py-2 text-sm font-medium text-zinc-50 dark:bg-white dark:text-zinc-900"
|
className="mt-4 cursor-pointer inline-flex items-center justify-center self-end rounded-lg bg-black px-4 py-2 text-sm font-medium text-zinc-50 dark:bg-white dark:text-zinc-900"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Approve Hold
|
Approve Hold
|
||||||
|
|||||||
89
src/components/Manage/CheckoutFromHoldModal.tsx
Normal file
89
src/components/Manage/CheckoutFromHoldModal.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogDescription,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogClose,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Book, HoldRequest, User } from '@/payload-types'
|
||||||
|
import { Variants, Transition } from 'motion/react'
|
||||||
|
import { Button } from '../ui/button'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import checkoutFromHoldRequest from '@/serverActions/CheckoutFromHoldRequests'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
holdRequest: HoldRequest
|
||||||
|
isOpen: boolean
|
||||||
|
onOpenChange: () => void
|
||||||
|
}
|
||||||
|
const CheckoutFromHoldModal = (props: Props) => {
|
||||||
|
const { holdRequest, isOpen, onOpenChange } = props
|
||||||
|
|
||||||
|
const customVariants: Variants = {
|
||||||
|
initial: {
|
||||||
|
scale: 0.9,
|
||||||
|
filter: 'blur(10px)',
|
||||||
|
y: '100%',
|
||||||
|
},
|
||||||
|
animate: {
|
||||||
|
scale: 1,
|
||||||
|
filter: 'blur(0px)',
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const customTransition: Transition = {
|
||||||
|
type: 'spring',
|
||||||
|
bounce: 0,
|
||||||
|
duration: 0.4,
|
||||||
|
}
|
||||||
|
|
||||||
|
const hold = holdRequest as HoldRequest
|
||||||
|
const book = hold.book as Book
|
||||||
|
const userRequested = hold.userRequested as User
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
const updateRequest = await checkoutFromHoldRequest({
|
||||||
|
holdRequestId: hold.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!updateRequest) toast('There was an issue checking out that hold request.')
|
||||||
|
else toast('Checked out')
|
||||||
|
|
||||||
|
onOpenChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
variants={customVariants}
|
||||||
|
transition={customTransition}
|
||||||
|
>
|
||||||
|
<DialogContent className="w-full max-w-md bg-white p-6 dark:bg-zinc-900">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-zinc-900 dark:text-white">
|
||||||
|
Checkout held for for {`${userRequested.firstName} ${userRequested.lastName}`}?
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-zinc-600 dark:text-zinc-400">
|
||||||
|
{book.title}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="mt-4 cursor-pointer inline-flex items-center justify-center self-end rounded-lg bg-black px-4 py-2 text-sm font-medium text-zinc-50 dark:bg-white dark:text-zinc-900"
|
||||||
|
type="button"
|
||||||
|
onClick={onSubmit}
|
||||||
|
>
|
||||||
|
Checkout Held Copy
|
||||||
|
</Button>
|
||||||
|
<DialogClose />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CheckoutFromHoldModal
|
||||||
@ -3,8 +3,8 @@ import { Author, Book, HoldRequest, Repository, User } from '@/payload-types'
|
|||||||
import { Button } from '../ui/button'
|
import { Button } from '../ui/button'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import ApproveHoldRequestModal from './ApproveHoldRequestModal'
|
import ApproveHoldRequestModal from './ApproveHoldRequestModal'
|
||||||
import { DialogTrigger } from '../ui/dialog'
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import CheckoutFromHoldModal from './CheckoutFromHoldModal'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repos: PaginatedDocs<Repository> | null
|
repos: PaginatedDocs<Repository> | null
|
||||||
@ -24,7 +24,11 @@ const HoldRequestNotifications = (props: Props) => {
|
|||||||
const book = hold.book as Book
|
const book = hold.book as Book
|
||||||
const authors = book.authors as Author[]
|
const authors = book.authors as Author[]
|
||||||
const dateRequested = hold.dateRequested ? new Date(hold.dateRequested) : new Date()
|
const dateRequested = hold.dateRequested ? new Date(hold.dateRequested) : new Date()
|
||||||
|
const holdingUntilDate = hold.holdingUntilDate
|
||||||
|
? new Date(hold.holdingUntilDate)
|
||||||
|
: new Date()
|
||||||
const userRequested = hold.userRequested as User
|
const userRequested = hold.userRequested as User
|
||||||
|
const userName = `${userRequested.firstName} ${userRequested.lastName}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={hold.id} className="col-span-1 rounded-lg shadow-sm border border-accent">
|
<li key={hold.id} className="col-span-1 rounded-lg shadow-sm border border-accent">
|
||||||
@ -37,10 +41,10 @@ const HoldRequestNotifications = (props: Props) => {
|
|||||||
{authors.map((a) => a.lf).join(' | ')}
|
{authors.map((a) => a.lf).join(' | ')}
|
||||||
</p>
|
</p>
|
||||||
{hold.isHolding ? (
|
{hold.isHolding ? (
|
||||||
<span>
|
<span className="text-wrap">
|
||||||
<span className="mr-0.5 text-xs">Hold Until</span>
|
<span className="mr-0.5 text-xs">{userName} Until</span>
|
||||||
<time className="inline-flex shrink-0 items-center rounded-full bg-background/20 px-1.5 py-0.5 text-xs font-medium text-emerald-600 ring-1 ring-emerald-600/20 ring-inset">
|
<time className="inline-flex shrink-0 items-center rounded-full bg-background/20 px-1.5 py-0.5 text-xs font-medium text-emerald-600 ring-1 ring-emerald-600/20 ring-inset">
|
||||||
{hold.holdingUntilDate}
|
{holdingUntilDate.toLocaleDateString()}
|
||||||
</time>
|
</time>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@ -48,9 +52,7 @@ const HoldRequestNotifications = (props: Props) => {
|
|||||||
<time className="inline-flex shrink-0 items-center rounded-full bg-background/20 px-1.5 py-0.5 text-xs font-medium text-amber-500 ring-1 ring-amber-600/20 ring-inset">
|
<time className="inline-flex shrink-0 items-center rounded-full bg-background/20 px-1.5 py-0.5 text-xs font-medium text-amber-500 ring-1 ring-amber-600/20 ring-inset">
|
||||||
{dateRequested.toLocaleDateString()}
|
{dateRequested.toLocaleDateString()}
|
||||||
</time>
|
</time>
|
||||||
<span className="text-xs">
|
<span className="text-xs">{userName}</span>
|
||||||
{`${userRequested.firstName} ${userRequested.lastName}`}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -75,14 +77,22 @@ const HoldRequestNotifications = (props: Props) => {
|
|||||||
onClick={() => setOpenedModalId(hold.id)}
|
onClick={() => setOpenedModalId(hold.id)}
|
||||||
>
|
>
|
||||||
<Image width={24} height={24} src="/images/approve.svg" alt="approve hold" />
|
<Image width={24} height={24} src="/images/approve.svg" alt="approve hold" />
|
||||||
<span>Approve</span>
|
<span>{hold.isHolding ? 'Checkout' : 'Approve'}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<ApproveHoldRequestModal
|
{hold.isHolding ? (
|
||||||
isOpen={openedModalId === hold.id}
|
<CheckoutFromHoldModal
|
||||||
onOpenChange={setOpenedModalId}
|
isOpen={openedModalId === hold.id}
|
||||||
holdRequest={hold}
|
onOpenChange={() => setOpenedModalId(null)}
|
||||||
/>
|
holdRequest={hold}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ApproveHoldRequestModal
|
||||||
|
isOpen={openedModalId === hold.id}
|
||||||
|
onOpenChange={() => setOpenedModalId(null)}
|
||||||
|
holdRequest={hold}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { useEffect, useLayoutEffect } from 'react';
|
import { useEffect, useLayoutEffect } from 'react'
|
||||||
|
|
||||||
function isMac(): boolean | undefined {
|
function isMac(): boolean | undefined {
|
||||||
return testPlatform(/^Mac/);
|
return testPlatform(/^Mac/)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIPhone(): boolean | undefined {
|
function isIPhone(): boolean | undefined {
|
||||||
return testPlatform(/^iPhone/);
|
return testPlatform(/^iPhone/)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIPad(): boolean | undefined {
|
function isIPad(): boolean | undefined {
|
||||||
@ -13,60 +13,57 @@ function isIPad(): boolean | undefined {
|
|||||||
testPlatform(/^iPad/) ||
|
testPlatform(/^iPad/) ||
|
||||||
// iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
|
// iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
|
||||||
(isMac() && navigator.maxTouchPoints > 1)
|
(isMac() && navigator.maxTouchPoints > 1)
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIOS(): boolean | undefined {
|
function isIOS(): boolean | undefined {
|
||||||
return isIPhone() || isIPad();
|
return isIPhone() || isIPad()
|
||||||
}
|
}
|
||||||
|
|
||||||
function testPlatform(re: RegExp): boolean | undefined {
|
function testPlatform(re: RegExp): boolean | undefined {
|
||||||
return typeof window !== 'undefined' && window.navigator != null
|
return typeof window !== 'undefined' && window.navigator != null
|
||||||
? re.test(window.navigator.platform)
|
? re.test(window.navigator.platform)
|
||||||
: undefined;
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const KEYBOARD_BUFFER = 24;
|
const KEYBOARD_BUFFER = 24
|
||||||
|
|
||||||
export const useIsomorphicLayoutEffect =
|
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
|
||||||
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
||||||
|
|
||||||
interface PreventScrollOptions {
|
interface PreventScrollOptions {
|
||||||
/** Whether the scroll lock is disabled. */
|
/** Whether the scroll lock is disabled. */
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean
|
||||||
focusCallback?: () => void;
|
focusCallback?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function chain(...callbacks: any[]): (...args: any[]) => void {
|
function chain(...callbacks: any[]): (...args: any[]) => void {
|
||||||
return (...args: any[]) => {
|
return (...args: any[]) => {
|
||||||
for (let callback of callbacks) {
|
for (const callback of callbacks) {
|
||||||
if (typeof callback === 'function') {
|
if (typeof callback === 'function') {
|
||||||
callback(...args);
|
callback(...args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const visualViewport = typeof document !== 'undefined' && window.visualViewport;
|
const visualViewport = typeof document !== 'undefined' && window.visualViewport
|
||||||
|
|
||||||
export function isScrollable(node: Element): boolean {
|
export function isScrollable(node: Element): boolean {
|
||||||
let style = window.getComputedStyle(node);
|
const style = window.getComputedStyle(node)
|
||||||
return /(auto|scroll)/.test(
|
return /(auto|scroll)/.test(style.overflow + style.overflowX + style.overflowY)
|
||||||
style.overflow + style.overflowX + style.overflowY
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getScrollParent(node: Element): Element {
|
export function getScrollParent(node: Element): Element {
|
||||||
if (isScrollable(node)) {
|
if (isScrollable(node)) {
|
||||||
node = node.parentElement as HTMLElement;
|
node = node.parentElement as HTMLElement
|
||||||
}
|
}
|
||||||
|
|
||||||
while (node && !isScrollable(node)) {
|
while (node && !isScrollable(node)) {
|
||||||
node = node.parentElement as HTMLElement;
|
node = node.parentElement as HTMLElement
|
||||||
}
|
}
|
||||||
|
|
||||||
return node || document.scrollingElement || document.documentElement;
|
return node || document.scrollingElement || document.documentElement
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML input types that do not cause the software keyboard to appear.
|
// HTML input types that do not cause the software keyboard to appear.
|
||||||
@ -80,11 +77,11 @@ const nonTextInputTypes = new Set([
|
|||||||
'button',
|
'button',
|
||||||
'submit',
|
'submit',
|
||||||
'reset',
|
'reset',
|
||||||
]);
|
])
|
||||||
|
|
||||||
// The number of active usePreventScroll calls. Used to determine whether to revert back to the original page style/scroll position
|
// The number of active usePreventScroll calls. Used to determine whether to revert back to the original page style/scroll position
|
||||||
let preventScrollCount = 0;
|
let preventScrollCount = 0
|
||||||
let restore: () => void;
|
let restore: () => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevents scrolling on the document body on mount, and
|
* Prevents scrolling on the document body on mount, and
|
||||||
@ -92,27 +89,27 @@ let restore: () => void;
|
|||||||
* shift due to the scrollbars disappearing.
|
* shift due to the scrollbars disappearing.
|
||||||
*/
|
*/
|
||||||
export function usePreventScroll(options: PreventScrollOptions = {}) {
|
export function usePreventScroll(options: PreventScrollOptions = {}) {
|
||||||
let { isDisabled } = options;
|
const { isDisabled } = options
|
||||||
|
|
||||||
useIsomorphicLayoutEffect(() => {
|
useIsomorphicLayoutEffect(() => {
|
||||||
if (isDisabled) {
|
if (isDisabled) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
preventScrollCount++;
|
preventScrollCount++
|
||||||
if (preventScrollCount === 1) {
|
if (preventScrollCount === 1) {
|
||||||
if (isIOS()) {
|
if (isIOS()) {
|
||||||
restore = preventScrollMobileSafari();
|
restore = preventScrollMobileSafari()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
preventScrollCount--;
|
preventScrollCount--
|
||||||
if (preventScrollCount === 0) {
|
if (preventScrollCount === 0) {
|
||||||
restore?.();
|
restore?.()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}, [isDisabled]);
|
}, [isDisabled])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mobile Safari is a whole different beast. Even with overflow: hidden,
|
// Mobile Safari is a whole different beast. Even with overflow: hidden,
|
||||||
@ -142,79 +139,72 @@ export function usePreventScroll(options: PreventScrollOptions = {}) {
|
|||||||
// 6. As a last resort, handle window scroll events, and scroll back to the top. This can happen when attempting
|
// 6. As a last resort, handle window scroll events, and scroll back to the top. This can happen when attempting
|
||||||
// to navigate to an input with the next/previous buttons that's outside a modal.
|
// to navigate to an input with the next/previous buttons that's outside a modal.
|
||||||
function preventScrollMobileSafari() {
|
function preventScrollMobileSafari() {
|
||||||
let scrollable: Element;
|
let scrollable: Element
|
||||||
let lastY = 0;
|
let lastY = 0
|
||||||
let onTouchStart = (e: TouchEvent) => {
|
const onTouchStart = (e: TouchEvent) => {
|
||||||
// Store the nearest scrollable parent element from the element that the user touched.
|
// Store the nearest scrollable parent element from the element that the user touched.
|
||||||
scrollable = getScrollParent(e.target as Element);
|
scrollable = getScrollParent(e.target as Element)
|
||||||
if (
|
if (scrollable === document.documentElement && scrollable === document.body) {
|
||||||
scrollable === document.documentElement &&
|
return
|
||||||
scrollable === document.body
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastY = e.changedTouches[0].pageY;
|
lastY = e.changedTouches[0].pageY
|
||||||
};
|
}
|
||||||
|
|
||||||
let onTouchMove = (e: TouchEvent) => {
|
const onTouchMove = (e: TouchEvent) => {
|
||||||
// Prevent scrolling the window.
|
// Prevent scrolling the window.
|
||||||
if (
|
if (!scrollable || scrollable === document.documentElement || scrollable === document.body) {
|
||||||
!scrollable ||
|
e.preventDefault()
|
||||||
scrollable === document.documentElement ||
|
return
|
||||||
scrollable === document.body
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent scrolling up when at the top and scrolling down when at the bottom
|
// Prevent scrolling up when at the top and scrolling down when at the bottom
|
||||||
// of a nested scrollable area, otherwise mobile Safari will start scrolling
|
// of a nested scrollable area, otherwise mobile Safari will start scrolling
|
||||||
// the window instead. Unfortunately, this disables bounce scrolling when at
|
// the window instead. Unfortunately, this disables bounce scrolling when at
|
||||||
// the top but it's the best we can do.
|
// the top but it's the best we can do.
|
||||||
let y = e.changedTouches[0].pageY;
|
const y = e.changedTouches[0].pageY
|
||||||
let scrollTop = scrollable.scrollTop;
|
const scrollTop = scrollable.scrollTop
|
||||||
let bottom = scrollable.scrollHeight - scrollable.clientHeight;
|
const bottom = scrollable.scrollHeight - scrollable.clientHeight
|
||||||
|
|
||||||
if (bottom === 0) {
|
if (bottom === 0) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((scrollTop <= 0 && y > lastY) || (scrollTop >= bottom && y < lastY)) {
|
if ((scrollTop <= 0 && y > lastY) || (scrollTop >= bottom && y < lastY)) {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
lastY = y;
|
lastY = y
|
||||||
};
|
}
|
||||||
|
|
||||||
let onTouchEnd = (e: TouchEvent) => {
|
const onTouchEnd = (e: TouchEvent) => {
|
||||||
let target = e.target as HTMLElement;
|
const target = e.target as HTMLElement
|
||||||
|
|
||||||
// Apply this change if we're not already focused on the target element
|
// Apply this change if we're not already focused on the target element
|
||||||
if (isInput(target) && target !== document.activeElement) {
|
if (isInput(target) && target !== document.activeElement) {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
// Apply a transform to trick Safari into thinking the input is at the top of the page
|
// Apply a transform to trick Safari into thinking the input is at the top of the page
|
||||||
// so it doesn't try to scroll it into view. When tapping on an input, this needs to
|
// so it doesn't try to scroll it into view. When tapping on an input, this needs to
|
||||||
// be done before the "focus" event, so we have to focus the element ourselves.
|
// be done before the "focus" event, so we have to focus the element ourselves.
|
||||||
target.style.transform = 'translateY(-2000px)';
|
target.style.transform = 'translateY(-2000px)'
|
||||||
target.focus();
|
target.focus()
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
target.style.transform = '';
|
target.style.transform = ''
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let onFocus = (e: FocusEvent) => {
|
const onFocus = (e: FocusEvent) => {
|
||||||
let target = e.target as HTMLElement;
|
const target = e.target as HTMLElement
|
||||||
if (isInput(target)) {
|
if (isInput(target)) {
|
||||||
// Transform also needs to be applied in the focus event in cases where focus moves
|
// Transform also needs to be applied in the focus event in cases where focus moves
|
||||||
// other than tapping on an input directly, e.g. the next/previous buttons in the
|
// other than tapping on an input directly, e.g. the next/previous buttons in the
|
||||||
// software keyboard. In these cases, it seems applying the transform in the focus event
|
// software keyboard. In these cases, it seems applying the transform in the focus event
|
||||||
// is good enough, whereas when tapping an input, it must be done before the focus event. 🤷♂️
|
// is good enough, whereas when tapping an input, it must be done before the focus event. 🤷♂️
|
||||||
target.style.transform = 'translateY(-2000px)';
|
target.style.transform = 'translateY(-2000px)'
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
target.style.transform = '';
|
target.style.transform = ''
|
||||||
|
|
||||||
// This will have prevented the browser from scrolling the focused element into view,
|
// This will have prevented the browser from scrolling the focused element into view,
|
||||||
// so we need to do this ourselves in a way that doesn't cause the whole page to scroll.
|
// so we need to do this ourselves in a way that doesn't cause the whole page to scroll.
|
||||||
@ -223,48 +213,44 @@ function preventScrollMobileSafari() {
|
|||||||
// If the keyboard is already visible, do this after one additional frame
|
// If the keyboard is already visible, do this after one additional frame
|
||||||
// to wait for the transform to be removed.
|
// to wait for the transform to be removed.
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
scrollIntoView(target);
|
scrollIntoView(target)
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, wait for the visual viewport to resize before scrolling so we can
|
// Otherwise, wait for the visual viewport to resize before scrolling so we can
|
||||||
// measure the correct position to scroll to.
|
// measure the correct position to scroll to.
|
||||||
visualViewport.addEventListener(
|
visualViewport.addEventListener('resize', () => scrollIntoView(target), { once: true })
|
||||||
'resize',
|
|
||||||
() => scrollIntoView(target),
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let onWindowScroll = () => {
|
const onWindowScroll = () => {
|
||||||
// Last resort. If the window scrolled, scroll it back to the top.
|
// Last resort. If the window scrolled, scroll it back to the top.
|
||||||
// It should always be at the top because the body will have a negative margin (see below).
|
// It should always be at the top because the body will have a negative margin (see below).
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0)
|
||||||
};
|
}
|
||||||
|
|
||||||
// Record the original scroll position so we can restore it.
|
// Record the original scroll position so we can restore it.
|
||||||
// Then apply a negative margin to the body to offset it by the scroll position. This will
|
// Then apply a negative margin to the body to offset it by the scroll position. This will
|
||||||
// enable us to scroll the window to the top, which is required for the rest of this to work.
|
// enable us to scroll the window to the top, which is required for the rest of this to work.
|
||||||
let scrollX = window.pageXOffset;
|
const scrollX = window.pageXOffset
|
||||||
let scrollY = window.pageYOffset;
|
const scrollY = window.pageYOffset
|
||||||
|
|
||||||
let restoreStyles = chain(
|
const restoreStyles = chain(
|
||||||
setStyle(
|
setStyle(
|
||||||
document.documentElement,
|
document.documentElement,
|
||||||
'paddingRight',
|
'paddingRight',
|
||||||
`${window.innerWidth - document.documentElement.clientWidth}px`
|
`${window.innerWidth - document.documentElement.clientWidth}px`,
|
||||||
)
|
),
|
||||||
// setStyle(document.documentElement, 'overflow', 'hidden'),
|
// setStyle(document.documentElement, 'overflow', 'hidden'),
|
||||||
// setStyle(document.body, 'marginTop', `-${scrollY}px`),
|
// setStyle(document.body, 'marginTop', `-${scrollY}px`),
|
||||||
);
|
)
|
||||||
|
|
||||||
// Scroll to the top. The negative margin on the body will make this appear the same.
|
// Scroll to the top. The negative margin on the body will make this appear the same.
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0)
|
||||||
|
|
||||||
let removeEvents = chain(
|
const removeEvents = chain(
|
||||||
addEvent(document, 'touchstart', onTouchStart, {
|
addEvent(document, 'touchstart', onTouchStart, {
|
||||||
passive: false,
|
passive: false,
|
||||||
capture: true,
|
capture: true,
|
||||||
@ -278,33 +264,29 @@ function preventScrollMobileSafari() {
|
|||||||
capture: true,
|
capture: true,
|
||||||
}),
|
}),
|
||||||
addEvent(document, 'focus', onFocus, true),
|
addEvent(document, 'focus', onFocus, true),
|
||||||
addEvent(window, 'scroll', onWindowScroll)
|
addEvent(window, 'scroll', onWindowScroll),
|
||||||
);
|
)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Restore styles and scroll the page back to where it was.
|
// Restore styles and scroll the page back to where it was.
|
||||||
restoreStyles();
|
restoreStyles()
|
||||||
removeEvents();
|
removeEvents()
|
||||||
window.scrollTo(scrollX, scrollY);
|
window.scrollTo(scrollX, scrollY)
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets a CSS property on an element, and returns a function to revert it to the previous value.
|
// Sets a CSS property on an element, and returns a function to revert it to the previous value.
|
||||||
function setStyle(
|
function setStyle(element: HTMLElement, style: keyof React.CSSProperties, value: string) {
|
||||||
element: HTMLElement,
|
|
||||||
style: keyof React.CSSProperties,
|
|
||||||
value: string
|
|
||||||
) {
|
|
||||||
// https://github.com/microsoft/TypeScript/issues/17827#issuecomment-391663310
|
// https://github.com/microsoft/TypeScript/issues/17827#issuecomment-391663310
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let cur = element.style[style];
|
const cur = element.style[style]
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
element.style[style] = value;
|
element.style[style] = value
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
element.style[style] = cur;
|
element.style[style] = cur
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds an event listener to an element, and returns a function to remove it.
|
// Adds an event listener to an element, and returns a function to remove it.
|
||||||
@ -312,49 +294,47 @@ function addEvent<K extends keyof GlobalEventHandlersEventMap>(
|
|||||||
target: EventTarget,
|
target: EventTarget,
|
||||||
event: K,
|
event: K,
|
||||||
handler: (this: Document, ev: GlobalEventHandlersEventMap[K]) => any,
|
handler: (this: Document, ev: GlobalEventHandlersEventMap[K]) => any,
|
||||||
options?: boolean | AddEventListenerOptions
|
options?: boolean | AddEventListenerOptions,
|
||||||
) {
|
) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
target.addEventListener(event, handler, options);
|
target.addEventListener(event, handler, options)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
target.removeEventListener(event, handler, options);
|
target.removeEventListener(event, handler, options)
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollIntoView(target: Element) {
|
function scrollIntoView(target: Element) {
|
||||||
let root = document.scrollingElement || document.documentElement;
|
const root = document.scrollingElement || document.documentElement
|
||||||
while (target && target !== root) {
|
while (target && target !== root) {
|
||||||
// Find the parent scrollable element and adjust the scroll position if the target is not already in view.
|
// Find the parent scrollable element and adjust the scroll position if the target is not already in view.
|
||||||
let scrollable = getScrollParent(target);
|
const scrollable = getScrollParent(target)
|
||||||
if (
|
if (
|
||||||
scrollable !== document.documentElement &&
|
scrollable !== document.documentElement &&
|
||||||
scrollable !== document.body &&
|
scrollable !== document.body &&
|
||||||
scrollable !== target
|
scrollable !== target
|
||||||
) {
|
) {
|
||||||
let scrollableTop = scrollable.getBoundingClientRect().top;
|
const scrollableTop = scrollable.getBoundingClientRect().top
|
||||||
let targetTop = target.getBoundingClientRect().top;
|
const targetTop = target.getBoundingClientRect().top
|
||||||
let targetBottom = target.getBoundingClientRect().bottom;
|
const targetBottom = target.getBoundingClientRect().bottom
|
||||||
// Buffer is needed for some edge cases
|
// Buffer is needed for some edge cases
|
||||||
const keyboardHeight =
|
const keyboardHeight = scrollable.getBoundingClientRect().bottom + KEYBOARD_BUFFER
|
||||||
scrollable.getBoundingClientRect().bottom + KEYBOARD_BUFFER;
|
|
||||||
|
|
||||||
if (targetBottom > keyboardHeight) {
|
if (targetBottom > keyboardHeight) {
|
||||||
scrollable.scrollTop += targetTop - scrollableTop;
|
scrollable.scrollTop += targetTop - scrollableTop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
target = scrollable.parentElement;
|
target = scrollable.parentElement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isInput(target: Element) {
|
export function isInput(target: Element) {
|
||||||
return (
|
return (
|
||||||
(target instanceof HTMLInputElement &&
|
(target instanceof HTMLInputElement && !nonTextInputTypes.has(target.type)) ||
|
||||||
!nonTextInputTypes.has(target.type)) ||
|
|
||||||
target instanceof HTMLTextAreaElement ||
|
target instanceof HTMLTextAreaElement ||
|
||||||
(target instanceof HTMLElement && target.isContentEditable)
|
(target instanceof HTMLElement && target.isContentEditable)
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/serverActions/ApproveHoldRequests.ts
Normal file
34
src/serverActions/ApproveHoldRequests.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@/payload.config'
|
||||||
|
import { HoldRequest } from '@/payload-types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
holdRequestId: number,
|
||||||
|
copyId: number,
|
||||||
|
untilDate: string,
|
||||||
|
}
|
||||||
|
export const approveHoldRequest = async (props: Props): Promise<HoldRequest | null> => {
|
||||||
|
const { holdRequestId, copyId, untilDate } = props
|
||||||
|
|
||||||
|
const payloadConfig = await config
|
||||||
|
const payload = await getPayload({ config: payloadConfig })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedHold = await payload.update({
|
||||||
|
collection: 'holdRequests',
|
||||||
|
id: holdRequestId,
|
||||||
|
data: {
|
||||||
|
copy: copyId,
|
||||||
|
holdingUntilDate: untilDate,
|
||||||
|
isHolding: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return updatedHold
|
||||||
|
} catch (_) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default approveHoldRequest
|
||||||
30
src/serverActions/CheckoutFromHoldRequests.ts
Normal file
30
src/serverActions/CheckoutFromHoldRequests.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@/payload.config'
|
||||||
|
import { HoldRequest } from '@/payload-types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
holdRequestId: number
|
||||||
|
}
|
||||||
|
export const checkoutFromHoldRequest = async (props: Props): Promise<HoldRequest | null> => {
|
||||||
|
const { holdRequestId } = props
|
||||||
|
|
||||||
|
const payloadConfig = await config
|
||||||
|
const payload = await getPayload({ config: payloadConfig })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedHold = await payload.update({
|
||||||
|
collection: 'holdRequests',
|
||||||
|
id: holdRequestId,
|
||||||
|
data: {
|
||||||
|
isCheckedOut: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return updatedHold
|
||||||
|
} catch (_) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default checkoutFromHoldRequest
|
||||||
Loading…
x
Reference in New Issue
Block a user