Merge pull request #2596 from omnivore-app/feat/web-undo-delete

Dont require confirmation on delete, show undo action
This commit is contained in:
Jackson Harper 2023-08-07 13:48:44 +08:00 committed by GitHub
commit 1e7019c657
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 135 additions and 103 deletions

View file

@ -77,9 +77,9 @@ export const Button = styled('button', {
fontFamily: 'Inter',
borderRadius: '8px',
cursor: 'pointer',
color: '$grayTextContrast',
color: 'white',
p: '10px 12px',
bg: 'rgb(125, 125, 125, 0.1)',
bg: 'rgb(125, 125, 125, 0.3)',
'&:hover': {
bg: 'rgb(47, 47, 47, 0.1)',
'.ctaButtonIcon': {

View file

@ -1,14 +1,7 @@
import { Action, createAction, useKBar, useRegisterActions } from 'kbar'
import debounce from 'lodash/debounce'
import { useRouter } from 'next/router'
import {
useCallback,
useEffect,
useMemo,
useReducer,
useRef,
useState,
} from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Toaster } from 'react-hot-toast'
import TopBarProgress from 'react-topbar-progress-indicator'
import { useFetchMore } from '../../../lib/hooks/useFetchMoreScroll'
@ -48,10 +41,13 @@ import { LibraryHeader, MultiSelectMode } from './LibraryHeader'
import { UploadModal } from '../UploadModal'
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
import { bulkActionMutation } from '../../../lib/networking/mutations/bulkActionMutation'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import {
showErrorToast,
showSuccessToast,
showSuccessToastWithUndo,
} from '../../../lib/toastHelpers'
import { SetPageLabelsModalPresenter } from '../article/SetLabelsModalPresenter'
import { NotebookPresenter } from '../article/NotebookPresenter'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
export type LibraryMode = 'reads' | 'highlights'
@ -94,7 +90,6 @@ export function HomeFeedContainer(): JSX.Element {
const [showAddLinkModal, setShowAddLinkModal] = useState(false)
const [showEditTitleModal, setShowEditTitleModal] = useState(false)
const [linkToRemove, setLinkToRemove] = useState<LibraryItem>()
const [linkToEdit, setLinkToEdit] = useState<LibraryItem>()
const [linkToUnsubscribe, setLinkToUnsubscribe] = useState<LibraryItem>()
@ -110,6 +105,19 @@ export function HomeFeedContainer(): JSX.Element {
mutate,
} = useGetLibraryItemsQuery(queryInputs)
useEffect(() => {
const handleRevalidate = () => {
;(async () => {
console.log('revalidating library')
await mutate()
})()
}
document.addEventListener('revalidateLibrary', handleRevalidate)
return () => {
document.removeEventListener('revalidateLibrary', handleRevalidate)
}
}, [mutate])
useEffect(() => {
if (queryValue.startsWith('#')) {
debouncedFetchSearchResults(
@ -367,8 +375,8 @@ export function HomeFeedContainer(): JSX.Element {
}
const modalTargetItem = useMemo(() => {
return labelsTarget || linkToEdit || linkToRemove || linkToUnsubscribe
}, [labelsTarget, linkToEdit, linkToRemove, linkToUnsubscribe])
return labelsTarget || linkToEdit || linkToUnsubscribe
}, [labelsTarget, linkToEdit, linkToUnsubscribe])
const [checkedItems, setCheckedItems] = useState<string[]>([])
const [multiSelectMode, setMultiSelectMode] = useState<MultiSelectMode>('off')
@ -758,8 +766,6 @@ export function HomeFeedContainer(): JSX.Element {
setActiveItem={(item: LibraryItem) => {
activateCard(item.node.id)
}}
linkToRemove={linkToRemove}
setLinkToRemove={setLinkToRemove}
linkToEdit={linkToEdit}
setLinkToEdit={setLinkToEdit}
linkToUnsubscribe={linkToUnsubscribe}
@ -796,8 +802,6 @@ type HomeFeedContentProps = {
setShowEditTitleModal: (show: boolean) => void
setActiveItem: (item: LibraryItem) => void
linkToRemove: LibraryItem | undefined
setLinkToRemove: (set: LibraryItem | undefined) => void
linkToEdit: LibraryItem | undefined
setLinkToEdit: (set: LibraryItem | undefined) => void
linkToUnsubscribe: LibraryItem | undefined
@ -911,23 +915,11 @@ type LibraryItemsLayoutProps = {
} & HomeFeedContentProps
function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
const [showRemoveLinkConfirmation, setShowRemoveLinkConfirmation] =
useState(false)
const [showUnsubscribeConfirmation, setShowUnsubscribeConfirmation] =
useState(false)
const [showUploadModal, setShowUploadModal] = useState(false)
const [, updateState] = useState({})
const removeItem = () => {
if (!props.linkToRemove) {
return
}
props.actionHandler('delete', props.linkToRemove)
props.setLinkToRemove(undefined)
setShowRemoveLinkConfirmation(false)
}
const unsubscribe = () => {
if (!props.linkToUnsubscribe) {
return
@ -977,9 +969,7 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
setShowEditTitleModal={props.setShowEditTitleModal}
setLinkToEdit={props.setLinkToEdit}
setShowUnsubscribeConfirmation={setShowUnsubscribeConfirmation}
setLinkToRemove={props.setLinkToRemove}
setLinkToUnsubscribe={props.setLinkToUnsubscribe}
setShowRemoveLinkConfirmation={setShowRemoveLinkConfirmation}
actionHandler={props.actionHandler}
multiSelectMode={props.multiSelectMode}
/>
@ -1014,43 +1004,6 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
item={props.linkToEdit as LibraryItem}
/>
)}
{showRemoveLinkConfirmation && (
<ConfirmationModal
richMessage={
<VStack alignment="center" distribution="center">
<StyledText style="modalTitle" css={{ margin: '0px 8px' }}>
Are you sure you want to delete this item? All associated notes
and highlights will be deleted.
</StyledText>
{props.linkToRemove?.node && props.viewer && (
<Box
css={{
transform: 'scale(0.6)',
opacity: 0.8,
pointerEvents: 'none',
filter: 'grayscale(1)',
}}
>
<LinkedItemCard
item={props.linkToRemove?.node}
viewer={props.viewer}
layout="GRID_LAYOUT"
multiSelectMode={props.multiSelectMode}
isChecked={false}
// eslint-disable-next-line @typescript-eslint/no-empty-function
setIsChecked={() => {}}
// eslint-disable-next-line @typescript-eslint/no-empty-function
handleAction={() => {}}
/>
</Box>
)}
</VStack>
}
onAccept={removeItem}
acceptButtonLabel="Delete Item"
onOpenChange={() => setShowRemoveLinkConfirmation(false)}
/>
)}
{showUnsubscribeConfirmation && (
<ConfirmationModal
message={'Are you sure you want to unsubscribe?'}
@ -1102,9 +1055,7 @@ type LibraryItemsProps = {
setShowEditTitleModal: (show: boolean) => void
setLinkToEdit: (set: LibraryItem | undefined) => void
setShowUnsubscribeConfirmation: (show: true) => void
setLinkToRemove: (set: LibraryItem | undefined) => void
setLinkToUnsubscribe: (set: LibraryItem | undefined) => void
setShowRemoveLinkConfirmation: (show: true) => void
isChecked: (itemId: string) => boolean
setIsChecked: (itemId: string, set: boolean) => void
@ -1199,10 +1150,7 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element {
setIsChecked={props.setIsChecked}
multiSelectMode={props.multiSelectMode}
handleAction={(action: LinkedItemCardAction) => {
if (action === 'delete') {
props.setShowRemoveLinkConfirmation(true)
props.setLinkToRemove(linkedItem)
} else if (action === 'editTitle') {
if (action === 'editTitle') {
props.setShowEditTitleModal(true)
props.setLinkToEdit(linkedItem)
} else if (action == 'unsubscribe') {

View file

@ -15,11 +15,9 @@ export type ReaderSettings = {
setMarginWidth: (newMarginWidth: number) => void
showSetLabelsModal: boolean
showDeleteConfirmation: boolean
showEditDisplaySettingsModal: boolean
setShowSetLabelsModal: (showSetLabelsModal: boolean) => void
setShowDeleteConfirmation: (showDeleteConfirmation: boolean) => void
setShowEditDisplaySettingsModal: (
showEditDisplaySettingsModal: boolean
) => void
@ -70,7 +68,6 @@ export const useReaderSettings = (): ReaderSettings => {
const [showSetLabelsModal, setShowSetLabelsModal] = useState(false)
const [showEditDisplaySettingsModal, setShowEditDisplaySettingsModal] =
useState(false)
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false)
const updateFontSize = useCallback(
(newFontSize: number) => {
@ -209,12 +206,10 @@ export const useReaderSettings = (): ReaderSettings => {
setFontSize,
setLineHeight,
setMarginWidth,
showDeleteConfirmation,
showSetLabelsModal,
showEditDisplaySettingsModal,
setShowSetLabelsModal,
setShowEditDisplaySettingsModal,
setShowDeleteConfirmation,
actionHandler,
setFontFamily,
fontFamily,

View file

@ -1,13 +1,15 @@
import { gql } from 'graphql-request'
import { gqlFetcher } from '../networkHelpers'
import { State } from '../fragments/articleFragment'
export type UpdatePageInput = {
pageId: string
title: string
title?: string
byline?: string | undefined
description: string
description?: string
savedAt?: string
publishedAt?: string
state?: State
}
export async function updatePageMutation(

View file

@ -1,15 +1,20 @@
import { gql } from 'graphql-request'
import useSWRInfinite from 'swr/infinite'
import { gqlFetcher } from '../networkHelpers'
import type { PageType, State } from '../fragments/articleFragment'
import { PageType, State } from '../fragments/articleFragment'
import { ContentReader } from '../fragments/articleFragment'
import { setLinkArchivedMutation } from '../mutations/setLinkArchivedMutation'
import { deleteLinkMutation } from '../mutations/deleteLinkMutation'
import { unsubscribeMutation } from '../mutations/unsubscribeMutation'
import { articleReadingProgressMutation } from '../mutations/articleReadingProgressMutation'
import { Label } from './../fragments/labelFragment'
import { showErrorToast, showSuccessToast } from '../../toastHelpers'
import {
showErrorToast,
showSuccessToast,
showSuccessToastWithUndo,
} from '../../toastHelpers'
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
import { updatePageMutation } from '../mutations/updatePageMutation'
export interface ReadableItem {
id: string
@ -343,9 +348,26 @@ export function useGetLibraryItemsQuery({
break
case 'delete':
updateData(undefined)
deleteLinkMutation(item.node.id).then((res) => {
const pageId = item.node.id
deleteLinkMutation(pageId).then((res) => {
if (res) {
showSuccessToast('Link removed', { position: 'bottom-right' })
showSuccessToastWithUndo('Page deleted', async () => {
const result = await updatePageMutation({
pageId: pageId,
state: State.SUCCEEDED,
})
mutate()
if (result) {
showSuccessToast('Page recovered')
} else {
showErrorToast(
'Error recovering page, check your deleted items'
)
}
})
} else {
showErrorToast('Error removing link', { position: 'bottom-right' })
}

View file

@ -2,6 +2,7 @@ import { toast, ToastOptions } from 'react-hot-toast'
import { CheckCircle, WarningCircle, X } from 'phosphor-react'
import { Box, HStack } from '../components/elements/LayoutPrimitives'
import { styled } from '@stitches/react'
import { Button } from '../components/elements/Button'
const toastStyles = {
minWidth: 265,
@ -67,10 +68,64 @@ const showToast = (
)
}
const showToastWithUndo = (
message: string,
background: string,
undoAction: () => Promise<void>,
options?: ToastOptions
) => {
return toast(
({ id }) => (
<FullWidthContainer alignment="center">
<CheckCircle size={24} color="white" />
<MessageContainer>{message}</MessageContainer>
<HStack distribution="end" css={{ marginLeft: 16 }}>
<Button
style="ctaLightGray"
onClick={(event) => {
event.preventDefault()
toast.dismiss(id)
;(async () => {
await undoAction()
})()
}}
>
Undo
</Button>
</HStack>
</FullWidthContainer>
),
{
style: {
...toastStyles,
background: background,
},
duration: 3500,
...options,
}
)
}
export const showSuccessToast = (message: string, options?: ToastOptions) => {
return showToast(message, '#55B938', 'success', options)
return showToast(message, '#55B938', 'success', {
position: 'bottom-right',
...options,
})
}
export const showErrorToast = (message: string, options?: ToastOptions) => {
return showToast(message, '#cc0000', 'error', options)
return showToast(message, '#cc0000', 'error', {
position: 'bottom-right',
...options,
})
}
export const showSuccessToastWithUndo = (
message: string,
undoAction: () => Promise<void>
) => {
return showToastWithUndo(message, '#55B938', undoAction, {
position: 'bottom-right',
})
}

View file

@ -27,7 +27,11 @@ import { ArticleActionsMenu } from '../../../components/templates/article/Articl
import { setLinkArchivedMutation } from '../../../lib/networking/mutations/setLinkArchivedMutation'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { useSWRConfig } from 'swr'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import {
showErrorToast,
showSuccessToast,
showSuccessToastWithUndo,
} from '../../../lib/toastHelpers'
import { SetLabelsModal } from '../../../components/templates/article/SetLabelsModal'
import { DisplaySettingsModal } from '../../../components/templates/article/DisplaySettingsModal'
import { useReaderSettings } from '../../../lib/hooks/useReaderSettings'
@ -41,6 +45,8 @@ import { VerticalArticleActionsMenu } from '../../../components/templates/articl
import { PdfHeaderSpacer } from '../../../components/templates/article/PdfHeaderSpacer'
import { EpubContainerProps } from '../../../components/templates/article/EpubContainer'
import { useSetPageLabels } from '../../../lib/hooks/useSetPageLabels'
import { updatePageMutation } from '../../../lib/networking/mutations/updatePageMutation'
import { State } from '../../../lib/networking/fragments/articleFragment'
const PdfArticleContainerNoSSR = dynamic<PdfArticleContainerProps>(
() => import('./../../../components/templates/article/PdfArticleContainer'),
@ -138,7 +144,7 @@ export default function Home(): JSX.Element {
}
break
case 'delete':
readerSettings.setShowDeleteConfirmation(true)
await deleteCurrentItem()
break
case 'openOriginalArticle':
const url = article?.url
@ -206,10 +212,23 @@ export default function Home(): JSX.Element {
const deleteCurrentItem = useCallback(async () => {
if (article) {
removeItemFromCache(cache, mutate, article.id)
await deleteLinkMutation(article.id).then((res) => {
const pageId = article.id
removeItemFromCache(cache, mutate, pageId)
await deleteLinkMutation(pageId).then((res) => {
if (res) {
showSuccessToast('Page deleted', { position: 'bottom-right' })
showSuccessToastWithUndo('Page deleted', async () => {
const result = await updatePageMutation({
pageId: pageId,
state: State.SUCCEEDED,
})
document.dispatchEvent(new Event('revalidateLibrary'))
if (result) {
showSuccessToast('Page recovered')
} else {
showErrorToast('Error recovering page, check your deleted items')
}
})
} else {
// todo: revalidate or put back in cache?
showErrorToast('Error deleting page', { position: 'bottom-right' })
@ -253,8 +272,6 @@ export default function Home(): JSX.Element {
perform: () => {
if (
readerSettings.showSetLabelsModal ||
readerSettings.showDeleteConfirmation ||
readerSettings.showDeleteConfirmation ||
readerSettings.showEditDisplaySettingsModal
) {
return
@ -550,13 +567,6 @@ export default function Home(): JSX.Element {
}}
/>
)}
{readerSettings.showDeleteConfirmation && (
<ConfirmationModal
message={'Are you sure you want to delete this page?'}
onAccept={deleteCurrentItem}
onOpenChange={() => readerSettings.setShowDeleteConfirmation(false)}
/>
)}
{article && showEditModal && (
<EditArticleModal
article={article}