mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #2596 from omnivore-app/feat/web-undo-delete
Dont require confirmation on delete, show undo action
This commit is contained in:
commit
1e7019c657
7 changed files with 135 additions and 103 deletions
|
|
@ -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': {
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue