diff --git a/README.md b/README.md index 826146bcb..e8bda6e55 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/omnivore-app/omnivore/Run%20tests)](https://github.com/omnivore-app/omnivore/actions/workflows/run-tests.yaml) [![Discord](https://img.shields.io/discord/844965259462311966?label=Join%20our%20Discord)](https://discord.gg/h2z5rppzz9) +![Mastodon Follow](https://img.shields.io/mastodon/follow/109458738600914558?domain=https%3A%2F%2Fpkm.social) [![Twitter Follow](https://img.shields.io/twitter/follow/omnivoreapp)](https://twitter.com/OmnivoreApp) ![GitHub](https://img.shields.io/github/license/omnivore-app/omnivore) diff --git a/packages/api/src/services/popular_reads.ts b/packages/api/src/services/popular_reads.ts index 7f79406d5..89d067c1b 100644 --- a/packages/api/src/services/popular_reads.ts +++ b/packages/api/src/services/popular_reads.ts @@ -153,7 +153,7 @@ const popularReads = [ author: 'The Omnivore Team', description: 'Get the most out of Omnivore by learning how to use it.', previewImage: - 'https://proxy-prod.omnivore-image-cache.app/88x88,sBp_gMyIp8Y4Mje8lzL39vzrBQg5m9KbprssrGjCbbHw/https://substackcdn.com/image/fetch/w_1200,h_600,c_limit,f_jpg,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F658efff4-341a-4720-8cf6-9b2bdbedfaa7_800x668.gif', + 'https://proxy-prod.omnivore-image-cache.app/320x320,sxQnqya1QNApB7ZAGPj9K20AU6sw0UAnjmAIy2ub8hUU/https://substackcdn.com/image/fetch/w_1200,h_600,c_fill,f_jpg,q_auto:good,fl_progressive:steep,g_auto/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F658efff4-341a-4720-8cf6-9b2bdbedfaa7_800x668.gif', publishedAt: new Date('2021-10-13'), siteName: 'Omnivore Blog', }, @@ -164,7 +164,7 @@ const popularReads = [ author: 'Omnivore', description: 'Learn how to save articles on iOS.', previewImage: - 'https://proxy-prod.omnivore-image-cache.app/260x260,suM2fz_-6_1PDsQDursGPD2bQqnpgGH9Ymj-IVb5dUR4/https://substackcdn.com/image/youtube/w_728,c_limit/k6RkIqepAig', + 'https://proxy-prod.omnivore-image-cache.app/320x320,sWDfv7sARTIdAlx6Rw_6t-QwL3T9aniEJRa1-jVaglNg/https://substackcdn.com/image/youtube/w_728,c_limit/k6RkIqepAig', publishedAt: new Date('2021-10-19'), siteName: 'Omnivore Blog', }, @@ -175,7 +175,7 @@ const popularReads = [ author: 'The Omnivore Team', description: 'Use labels to organize your Omnivore library.', previewImage: - 'https://proxy-prod.omnivore-image-cache.app/88x88,sSLRtT7zJbaNFEUbqDe9jbr3nloPsdjaqQXUqISk_x7E/https://substackcdn.com/image/fetch/w_1200,h_600,c_limit,f_jpg,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4ec9f3c-baef-464b-8d3a-0b8a384874d3_960x711.gif', + 'https://proxy-prod.omnivore-image-cache.app/320x320,sTgJ5Q0XIg_EHdmPWcxtXFmkjn8T6hkJt7S9ziClagYo/https://substackcdn.com/image/fetch/w_1200,h_600,c_fill,f_jpg,q_auto:good,fl_progressive:steep,g_auto/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdaf07af7-5cdb-4ecc-aace-1a46de3e9c58_1827x1090.png', publishedAt: new Date('2022-04-18'), siteName: 'Omnivore Blog', }, @@ -199,7 +199,7 @@ const popularReads = [ description: 'At the end of 2014 I received an email informing me that I had read over a million words in the ‘read it later’ app Pocket', previewImage: - 'https://proxy-prod.omnivore-image-cache.app/88x88,sVITWrJo3Wdi5LY3qSXX9aGytwKKteF8bth4z1MNz-PI/https://i0.wp.com/fortelabs.co/wp-content/uploads/2015/11/1rPXwIczUJRCE54v8FfAHGw.jpeg?fit=2000%2C844&ssl=1', + 'https://proxy-prod.omnivore-image-cache.app/320x320,sGN5R34M5z068QMXDZD32CQD6mCbxc47hWXm__JVUePE/https://fortelabs.com/wp-content/uploads/2015/11/1rPXwIczUJRCE54v8FfAHGw.jpeg', publishedAt: new Date('2022-01-24'), siteName: 'Forte Labs', }, diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index 6a407e770..831fc58cc 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -20,7 +20,7 @@ export const Button = styled('button', { }, ctaDarkYellow: { border: '1px solid transparent', - fontSize: '13px', + fontSize: '14px', fontWeight: 500, fontFamily: 'Inter', borderRadius: '5px', @@ -77,8 +77,8 @@ export const Button = styled('button', { fontFamily: 'Inter', borderRadius: '8px', cursor: 'pointer', - color: 'white', p: '10px 12px', + color: '$thTextContrast2', bg: 'rgb(125, 125, 125, 0.3)', '&:hover': { bg: 'rgb(47, 47, 47, 0.1)', diff --git a/packages/web/components/elements/DropdownElements.tsx b/packages/web/components/elements/DropdownElements.tsx index b601b7f6b..1e42d61ba 100644 --- a/packages/web/components/elements/DropdownElements.tsx +++ b/packages/web/components/elements/DropdownElements.tsx @@ -53,7 +53,7 @@ export const DropdownContent = styled(Content, { borderRadius: '6px', outline: '1px solid #323232', border: '1px solid $grayBorder', - boxShadow: '$cardBoxShadow', + boxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05);', '--arrow-visibility': '', '&[data-side="top"]': { '--arrow-visibility': 'collapse', diff --git a/packages/web/components/elements/FeatureHelpBox.tsx b/packages/web/components/elements/FeatureHelpBox.tsx new file mode 100644 index 000000000..9390dddb3 --- /dev/null +++ b/packages/web/components/elements/FeatureHelpBox.tsx @@ -0,0 +1,131 @@ +import { HStack, SpanBox, VStack } from './LayoutPrimitives' +import { theme } from '../tokens/stitches.config' +import { Button } from './Button' +import { CloseIcon } from './icons/CloseIcon' +import { HelpfulSlothImage } from './images/HelpfulSlothImage' +import { ArrowSquareOut } from 'phosphor-react' +import { useEffect, useState } from 'react' + +type FeatureHelpBoxProps = { + helpTitle: string + helpMessage: string + + helpCTAText?: string + onClickCTA?: () => void + + docsMessage: string + docsDestination: string + + onDismiss: () => void +} + +export const FeatureHelpBox = (props: FeatureHelpBoxProps) => { + const [display, setDisplay] = useState(false) + + useEffect(() => { + setDisplay(true) + }, []) + + if (!display) { + return <> + } + + return ( + + + + + + + + + ) +} + +const HelpSection = (props: FeatureHelpBoxProps) => { + return ( + + + + {props.helpTitle} + + + + {props.helpMessage} + + {props.helpCTAText && props.onClickCTA && ( + + )} + + + + ) +} diff --git a/packages/web/components/elements/LabelColorDropdown.tsx b/packages/web/components/elements/LabelColorDropdown.tsx index 26e015bff..69d7a79e9 100644 --- a/packages/web/components/elements/LabelColorDropdown.tsx +++ b/packages/web/components/elements/LabelColorDropdown.tsx @@ -4,11 +4,8 @@ import { Box, HStack } from './LayoutPrimitives' import { StyledText } from './StyledText' import { LabelColorDropdownProps, - LabelColorObject, LabelOptionProps, } from '../../utils/settings-page/labels/types' -import { labelColorObjects } from '../../utils/settings-page/labels/labelColorObjects' -import { LabelColor } from '../../lib/networking/fragments/labelFragment' import { TwitterPicker } from 'react-color' import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' diff --git a/packages/web/components/elements/SuggestionBox.tsx b/packages/web/components/elements/SuggestionBox.tsx new file mode 100644 index 000000000..fd01bf44d --- /dev/null +++ b/packages/web/components/elements/SuggestionBox.tsx @@ -0,0 +1,127 @@ +import Link from 'next/link' +import { HStack, SpanBox, VStack } from './LayoutPrimitives' +import { ArrowRightIcon } from './icons/ArrowRightIcon' +import { theme } from '../tokens/stitches.config' +import { ReactNode } from 'react' +import { Button } from './Button' +import { CloseIcon } from './icons/CloseIcon' + +export type SuggestionAction = { + url: string + text: string +} + +type SuggestionBoxProps = { + helpMessage: string + suggestions: SuggestionAction[] + + size?: 'large' | 'small' + background?: string + + dismissible?: boolean + onDismiss?: () => void +} + +type InternalOrExternalLinkProps = { + link: string + children: ReactNode +} + +const InternalOrExternalLink = (props: InternalOrExternalLinkProps) => { + const isExternal = props.link.startsWith('https') + + return ( + + {!isExternal ? ( + {props.children} + ) : ( + + {props.children} + + )} + + ) +} + +export const SuggestionBox = (props: SuggestionBoxProps) => { + return ( + + + {props.dismissible && ( + + + + )} + {props.helpMessage} + {props.suggestions.map((suggestion, idx) => { + return ( + + + <>{suggestion.text} + + + + ) + })} + + + ) +} diff --git a/packages/web/components/elements/icons/CloseIcon.tsx b/packages/web/components/elements/icons/CloseIcon.tsx new file mode 100644 index 000000000..384f28150 --- /dev/null +++ b/packages/web/components/elements/icons/CloseIcon.tsx @@ -0,0 +1,39 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { IconProps } from './IconProps' + +import React from 'react' + +export class CloseIcon extends React.Component { + render() { + const size = (this.props.size || 26).toString() + const color = (this.props.color || '#2A2A2A').toString() + + return ( + + + + + + + ) + } +} diff --git a/packages/web/components/elements/images/HelpfulOwlImage.tsx b/packages/web/components/elements/images/HelpfulOwlImage.tsx new file mode 100644 index 000000000..140877c2e --- /dev/null +++ b/packages/web/components/elements/images/HelpfulOwlImage.tsx @@ -0,0 +1,12 @@ +import Image from 'next/image' + +export const HelpfulOwlImage = () => { + return ( + Picture of an owl reading + ) +} diff --git a/packages/web/components/elements/images/HelpfulSlothImage.tsx b/packages/web/components/elements/images/HelpfulSlothImage.tsx new file mode 100644 index 000000000..95c152a43 --- /dev/null +++ b/packages/web/components/elements/images/HelpfulSlothImage.tsx @@ -0,0 +1,12 @@ +import Image from 'next/image' + +export const HelpfulSlothImage = () => { + return ( + Picture of a sloth reading + ) +} diff --git a/packages/web/components/patterns/ArticleNotes.tsx b/packages/web/components/patterns/ArticleNotes.tsx index 438e834ff..a4a786b99 100644 --- a/packages/web/components/patterns/ArticleNotes.tsx +++ b/packages/web/components/patterns/ArticleNotes.tsx @@ -25,7 +25,6 @@ MdEditor.use(Plugins.TabInsert, { tabMapValue: 1, // note that 1 means a '\t' instead of ' '. }) -console.log() MdEditor.use(Counter) type NoteSectionProps = { diff --git a/packages/web/components/patterns/DropdownMenu.tsx b/packages/web/components/patterns/DropdownMenu.tsx deleted file mode 100644 index de09ce225..000000000 --- a/packages/web/components/patterns/DropdownMenu.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { ReactNode, useMemo, useState } from 'react' -import { HStack, VStack } from './../elements/LayoutPrimitives' -import { Dropdown, DropdownOption } from '../elements/DropdownElements' -import { StyledText } from '../elements/StyledText' -import { Button } from '../elements/Button' -import { currentThemeName } from '../../lib/themeUpdater' -import { Check } from 'phosphor-react' - -export type HeaderDropdownAction = - | 'apply-dark-theme' - | 'apply-light-theme' - | 'navigate-to-install' - | 'navigate-to-emails' - | 'navigate-to-labels' - | 'navigate-to-profile' - | 'navigate-to-subscriptions' - | 'navigate-to-api' - | 'navigate-to-integrations' - | 'increaseFontSize' - | 'decreaseFontSize' - | 'logout' - -type DropdownMenuProps = { - username?: string - triggerElement: ReactNode - actionHandler: (action: HeaderDropdownAction) => void -} - -export function DropdownMenu(props: DropdownMenuProps): JSX.Element { - const [currentTheme, setCurrentTheme] = useState(currentThemeName()) - - const isDark = useMemo(() => { - return currentTheme === 'Dark' || currentTheme === 'Darker' - }, [currentTheme]) - - return ( - - - Theme - - - - - - props.actionHandler('navigate-to-install')} - title="Install" - /> - props.actionHandler('navigate-to-emails')} - title="Emails" - /> - props.actionHandler('navigate-to-labels')} - title="Labels" - /> - props.actionHandler('navigate-to-api')} - title="API Keys" - /> - props.actionHandler('navigate-to-integrations')} - title="Integrations" - /> - window.open('https://docs.omnivore.app', '_blank')} - title="Documentation" - /> - window.Intercom('show')} - title="Feedback" - /> - props.actionHandler('logout')} - title="Logout" - /> - - ) -} diff --git a/packages/web/components/patterns/SettingsHeader.tsx b/packages/web/components/patterns/SettingsHeader.tsx index bd98428de..e359126ec 100644 --- a/packages/web/components/patterns/SettingsHeader.tsx +++ b/packages/web/components/patterns/SettingsHeader.tsx @@ -3,6 +3,7 @@ import { OmnivoreNameLogo } from '../elements/images/OmnivoreNameLogo' import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery' import { PrimaryDropdown } from '../templates/PrimaryDropdown' import { HEADER_HEIGHT } from '../templates/homeFeed/HeaderSpacer' +import { LogoBox } from '../elements/LogoBox' type HeaderProps = { user?: UserBasicData @@ -21,25 +22,15 @@ export function SettingsHeader(props: HeaderProps): JSX.Element { display: 'flex', position: 'fixed', width: '100%', - px: '25px', + pr: '25px', height: HEADER_HEIGHT, - bg: '$thBackground3', - borderBottom: '1px solid $thBorderColor', '@mdDown': { - px: '15px', + pr: '15px', }, + bg: '$thBackground', }} > - - - - + diff --git a/packages/web/components/templates/ErrorLayout.tsx b/packages/web/components/templates/ErrorLayout.tsx index b0837c883..b3370b928 100644 --- a/packages/web/components/templates/ErrorLayout.tsx +++ b/packages/web/components/templates/ErrorLayout.tsx @@ -13,7 +13,6 @@ type ErrorLayoutProps = { export function ErrorLayout(props: ErrorLayoutProps): JSX.Element { const { viewerData } = useGetViewerQuery() - console.log(viewerData?.me) return ( diff --git a/packages/web/components/templates/PrimaryDropdown.tsx b/packages/web/components/templates/PrimaryDropdown.tsx index cbd0283cd..e5679fe0f 100644 --- a/packages/web/components/templates/PrimaryDropdown.tsx +++ b/packages/web/components/templates/PrimaryDropdown.tsx @@ -25,11 +25,11 @@ type PrimaryDropdownProps = { updateLayout?: (layout: LayoutType) => void showAddLinkModal?: () => void - startSelectMultiple?: () => void } export type HeaderDropdownAction = | 'navigate-to-install' + | 'navigate-to-feeds' | 'navigate-to-emails' | 'navigate-to-labels' | 'navigate-to-profile' @@ -51,6 +51,9 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element { case 'navigate-to-install': router.push('/settings/installation') break + case 'navigate-to-feeds': + router.push('/settings/feeds') + break case 'navigate-to-emails': router.push('/settings/emails') break @@ -152,6 +155,10 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element { onSelect={() => headerDropdownActionHandler('navigate-to-install')} title="Install" /> + headerDropdownActionHandler('navigate-to-feeds')} + title="Feeds" + /> headerDropdownActionHandler('navigate-to-emails')} title="Emails" @@ -170,17 +177,6 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element { /> )} - {props.startSelectMultiple && ( - <> - - props.startSelectMultiple && props.startSelectMultiple() - } - title="Select Multiple" - /> - - - )} headerDropdownActionHandler('navigate-to-api')} title="API Keys" @@ -320,8 +316,7 @@ function ThemeSection(props: PrimaryDropdownProps): JSX.Element { props.updateLayout && props.updateLayout('LIST_LAYOUT') }} > - diff --git a/packages/web/components/templates/SettingsLayout.tsx b/packages/web/components/templates/SettingsLayout.tsx index 7ec179af3..eb5c291a7 100644 --- a/packages/web/components/templates/SettingsLayout.tsx +++ b/packages/web/components/templates/SettingsLayout.tsx @@ -49,7 +49,6 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element { {props.children} diff --git a/packages/web/components/templates/auth/EmailSignup.tsx b/packages/web/components/templates/auth/EmailSignup.tsx index e81531926..090c9f7ac 100644 --- a/packages/web/components/templates/auth/EmailSignup.tsx +++ b/packages/web/components/templates/auth/EmailSignup.tsx @@ -155,6 +155,17 @@ export function EmailSignup(): JSX.Element { {errorMessage && {errorMessage}} + + Omnivore will send you daily tips for your first week as a new user. + If you don't like them you can unsubscribe. + + { - switch (props.type) { - case 'library': - return ( - <> - You can add a link or read more about Omnivore's{' '} - - advanced search - - . - - ) - case 'feed': - return ( - <> - You can subscribe to RSS feeds using the{' '} - - feeds page - - . Learn more about feeds at 's{' '} - - docs.omnivore.app/using/feeds.html - - . - - ) - case 'newsletter': - return ( - <> - Create email addresses that can be used to subscribe to newsletters on - the{' '} - - emails page - - . Learn more about reading newsletters in Omnivore at 's{' '} - - docs.omnivore.app/using/inbox.html - - . - - ) - } - return <> -} - export const ErrorBox = (props: HelpMessageProps) => { const errorTitle = useMemo(() => { switch (props.type) { + case 'inbox': + return 'Your inbox is empty. The inbox will contain all your non-archived saved items.' + case 'continue': + return "No continue reading items. Continue Reading items are items you have started but haven't finished reading." + case 'non-feed': + return "No non-feed items found. Non-feed items are items you've add to the library using the mobile apps, browser extensions, or Add Link button. Not newsletter or feed items." + case 'highlight': + return 'No highlights found. Add highlights to your library by highlighting text in the reader view.' + case 'unlabeled': + return 'No unlabeled items found. Items without labels can be found here. Use this query to easily triage your library.' + case 'archive': + return 'You do not have any archived items.' + case 'files': + return 'No files found.' case 'feed': return 'You do not have any feed items matching this query.' + case 'subscription': + return 'You do not have any subscriptions.' case 'newsletter': - return 'You do not have any newsletter item matching this query.' + return 'You do not have any newsletter items matching this query.' } return 'No results found for this query.' }, [props.type]) @@ -116,70 +80,84 @@ export const ErrorBox = (props: HelpMessageProps) => { ) } -export const SuggestionBox = (props: HelpMessageProps) => { - const helpMessage = useMemo(() => { - switch (props.type) { - case 'feed': - return 'Want to add an RSS or Atom Subscription?' - case 'newsletter': - return 'Create an Omnivore email address and subscribe to newsletters.' - } - return "Add a link or read more about Omnivore's Advanced Search." - }, [props.type]) +type SuggestionMessage = { + message: string + actions: SuggestionAction[] +} - const helpTarget = useMemo(() => { +export const Suggestion = (props: HelpMessageProps) => { + const helpMessage = useMemo(() => { switch (props.type) { case 'feed': - return '/settings/feeds' + return { + message: 'Want to add an RSS or Atom Subscription?', + actions: [ + { text: 'Add an RSS or Atom feed', url: '/settings/feeds' }, + ], + } + case 'archive': + return { + message: + 'When you are done reading something archive it and it will be saved in Omnivore forever.', + actions: [ + { + text: 'Read the docs', + url: 'https://docs.omnivore.app/using/saving', + }, + ], + } + case 'files': + return { + message: + 'Drag PDFs into the library to add them to your Omnivore account.', + actions: [], + } case 'newsletter': - return '/settings/emails' + return { + message: + 'Create an Omnivore email address and subscribe to newsletters.', + actions: [ + { + text: 'Create an email address for newsletters', + url: '/settings/emails', + }, + ], + } + case 'subscription': + return { + message: + 'Create an Omnivore email address and subscribe to newsletters or add a feed from the Feeds page.', + actions: [ + { text: 'Add an RSS or Atom feed', url: '/settings/feeds' }, + { + text: 'Create an email address for newsletters', + url: '/settings/emails', + }, + ], + } + } + return { + message: "Add a link or read more about Omnivore's Advanced Search.", + actions: [ + { + text: 'Read the Docs', + url: 'https://docs.omnivore.app/using/search.html', + }, + ], } - return 'https://docs.omnivore.app/' }, [props.type]) return ( - - {helpMessage} - - - - <>Click Here - - - - - + <> + {helpMessage ? ( + + ) : ( + <> + )} + ) } @@ -187,8 +165,24 @@ export const EmptyLibrary = (props: EmptyLibraryProps) => { const type = useMemo(() => { if (props.searchTerm) { switch (props.searchTerm) { + case 'in:inbox': + return 'inbox' + case 'in:inbox sort:read-desc is:unread': + return 'continue' + case 'in:library': + return 'non-feed' + case 'has:highlights mode:highlights': + return 'highlight' + case 'no:label': + return 'unlabeled' + case 'type:file': + return 'files' + case 'in:archive': + return 'archive' case 'label:RSS': return 'feed' + case 'in:subscription': + return 'subscription' case 'label:Newsletter': return 'newsletter' } @@ -205,9 +199,7 @@ export const EmptyLibrary = (props: EmptyLibraryProps) => { pl: '0px', width: '100%', - '@media (max-width: 1300px)': { - flexDirection: 'column', - }, + flexDirection: 'column', '@media (max-width: 768px)': { p: '15px', @@ -232,7 +224,7 @@ export const EmptyLibrary = (props: EmptyLibraryProps) => { }} > - + ) } diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 65c1bb689..fae67ae68 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -1229,7 +1229,7 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element { data-testid="linkedItemCard" id={linkedItem.node.id} tabIndex={0} - key={linkedItem.node.id} + key={linkedItem.node.id + linkedItem.node.image} css={{ width: '100%', '&:focus-visible': { diff --git a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx index 9fafbc785..8825ff5ad 100644 --- a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx +++ b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx @@ -172,6 +172,7 @@ function Subscriptions(props: LibraryFilterMenuProps): JSX.Element { > {!collapsed ? ( <> + void - handleLinkSubmission: (link: string, timezone: string, locale:string) => Promise, + handleLinkSubmission: ( + link: string, + timezone: string, + locale: string + ) => Promise } export function LibraryHeader(props: LibraryHeaderProps): JSX.Element { @@ -220,15 +224,20 @@ export type SearchBoxProps = { compact?: boolean onClose?: () => void - handleLinkSubmission: (link: string, timezone: string, locale:string) => Promise, + handleLinkSubmission: ( + link: string, + timezone: string, + locale: string + ) => Promise } export function SearchBox(props: SearchBoxProps): JSX.Element { const inputRef = useRef(null) const [focused, setFocused] = useState(false) const [searchTerm, setSearchTerm] = useState(props.searchTerm ?? '') - const [isAddAction, setIsAddAction] = useState(false); - const IS_URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; + const [isAddAction, setIsAddAction] = useState(false) + const IS_URL_REGEX = + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/ useEffect(() => { setSearchTerm(props.searchTerm ?? '') @@ -284,22 +293,23 @@ export function SearchBox(props: SearchBoxProps): JSX.Element { e.preventDefault() }} > - { - (() => { - if (isAddAction) { - return { + if (isAddAction) { + return ( + + ) } - return - })() - } - + ) + })()}
{ @@ -308,9 +318,9 @@ export function SearchBox(props: SearchBoxProps): JSX.Element { if (!isAddAction) { props.applySearchQuery(searchTerm || '') } else { - await props.handleLinkSubmission(searchTerm, timeZone, locale) - setSearchTerm(props.searchTerm ?? "") - props.applySearchQuery(props.searchTerm ?? "") + await props.handleLinkSubmission(searchTerm, timeZone, locale) + setSearchTerm(props.searchTerm ?? '') + props.applySearchQuery(props.searchTerm ?? '') } inputRef.current?.blur() if (props.onClose) { @@ -408,7 +418,11 @@ type ControlButtonBoxProps = { searchTerm: string | undefined applySearchQuery: (searchQuery: string) => void - handleLinkSubmission: (link: string, timezone: string, locale:string) => Promise, + handleLinkSubmission: ( + link: string, + timezone: string, + locale: string + ) => Promise } function MultiSelectControls(props: ControlButtonBoxProps): JSX.Element { @@ -517,13 +531,6 @@ function SearchControlButtonBox( { - props.setMultiSelectMode('none') - } - : undefined - } showAddLinkModal={props.showAddLinkModal} /> @@ -702,9 +709,6 @@ function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element { layout={props.layout} updateLayout={props.updateLayout} showAddLinkModal={props.showAddLinkModal} - startSelectMultiple={() => { - props.setMultiSelectMode('none') - }} /> )} diff --git a/packages/web/components/templates/settings/SettingsTable.tsx b/packages/web/components/templates/settings/SettingsTable.tsx index 89c9f3d3f..ef3421651 100644 --- a/packages/web/components/templates/settings/SettingsTable.tsx +++ b/packages/web/components/templates/settings/SettingsTable.tsx @@ -8,6 +8,9 @@ import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { StyledText } from '../../elements/StyledText' import { theme } from '../../tokens/stitches.config' import { SettingsLayout } from '../SettingsLayout' +import { SuggestionBox } from '../../elements/SuggestionBox' +import { usePersistedState } from '../../../lib/hooks/usePersistedState' +import { FeatureHelpBox } from '../../elements/FeatureHelpBox' type SettingsTableProps = { pageId: string @@ -17,6 +20,8 @@ type SettingsTableProps = { createTitle?: string createAction?: () => void + suggestionInfo: SuggestionInfo + children: React.ReactNode } @@ -52,6 +57,16 @@ type MoreOptionsProps = { onEdit?: () => void } +type SuggestionInfo = { + title: string + message: string + docs: string + key: string + + CTAText?: string + onClickCTA?: () => void +} + const MoreOptions = (props: MoreOptionsProps) => ( { } export const SettingsTable = (props: SettingsTableProps): JSX.Element => { + const [showSuggestion, setShowSuggestion] = usePersistedState({ + key: props.suggestionInfo.key, + initialValue: !!props.suggestionInfo, + }) + return ( { }, }} > + {props.suggestionInfo && showSuggestion && ( + { + setShowSuggestion(false) + }} + helpCTAText={props.suggestionInfo.CTAText} + onClickCTA={props.suggestionInfo.onClickCTA} + /> + )} { break case 'setMarginWidth': { const value = Number(arg) - console.log('setMarginWidth: ', value) if (value >= 200 && value <= 560) { setMarginWidth(value) } diff --git a/packages/web/lib/networking/mutations/addPopularReadMutation.ts b/packages/web/lib/networking/mutations/addPopularReadMutation.ts index 2ab326d3b..c8914e7b5 100644 --- a/packages/web/lib/networking/mutations/addPopularReadMutation.ts +++ b/packages/web/lib/networking/mutations/addPopularReadMutation.ts @@ -26,11 +26,8 @@ export async function addPopularReadMutation( } ` - console.log('addPopularReadMutation', mutation) - try { const response = await gqlFetcher(mutation, { readName }) - console.log('response', response) const data = response as AddPopularReadResponse | undefined return data?.addPopularRead?.pageId } catch (error) { diff --git a/packages/web/lib/networking/mutations/bulkActionMutation.ts b/packages/web/lib/networking/mutations/bulkActionMutation.ts index c1070ccec..71d02a281 100644 --- a/packages/web/lib/networking/mutations/bulkActionMutation.ts +++ b/packages/web/lib/networking/mutations/bulkActionMutation.ts @@ -45,8 +45,6 @@ export async function bulkActionMutation( } ` - console.log('bulkActionbulkActionMutation', mutation) - try { const response = await gqlFetcher(mutation, { action, @@ -54,7 +52,6 @@ export async function bulkActionMutation( labelIds, expectedCount, }) - console.log('response', response) const data = response as BulkActionResponse | undefined return data?.bulkAction?.success ?? false } catch (error) { diff --git a/packages/web/lib/networking/mutations/createNewsletterEmailMutation.ts b/packages/web/lib/networking/mutations/createNewsletterEmailMutation.ts index e56d58427..ba7d46ab6 100644 --- a/packages/web/lib/networking/mutations/createNewsletterEmailMutation.ts +++ b/packages/web/lib/networking/mutations/createNewsletterEmailMutation.ts @@ -11,7 +11,9 @@ type CreateNewsletterEmail = { newsletterEmail: NewsletterEmail } -export async function createNewsletterEmailMutation(): Promise { +export async function createNewsletterEmailMutation(): Promise< + string | undefined +> { const mutation = gql` mutation createNewsletterEmailMutation { createNewsletterEmail { @@ -29,9 +31,10 @@ export async function createNewsletterEmailMutation(): Promise { + sendNotification: boolean +): Promise { const mutation = gql` mutation createReminderMutation($input: CreateReminderInput!) { createReminder(input: $input) { @@ -35,13 +36,12 @@ export async function createReminderMutation( reminderType, archiveUntil, sendNotification, - scheduledAt: new Date() + scheduledAt: new Date(), } const data = await gqlFetcher(mutation, { input }) - console.log('created reminder', data) return 'data' } catch (error) { console.log('createReminder error', error) return undefined } -} \ No newline at end of file +} diff --git a/packages/web/lib/networking/mutations/deleteAccountMutation.ts b/packages/web/lib/networking/mutations/deleteAccountMutation.ts index 15c6a6f6e..d65c3f4f7 100644 --- a/packages/web/lib/networking/mutations/deleteAccountMutation.ts +++ b/packages/web/lib/networking/mutations/deleteAccountMutation.ts @@ -26,8 +26,6 @@ export async function deleteAccountMutation( } ` - console.log('deleteAccountMutation', mutation) - try { const response = await gqlFetcher(mutation, { userId }) console.log('response', response) diff --git a/packages/web/lib/networking/mutations/deleteRuleMutation.ts b/packages/web/lib/networking/mutations/deleteRuleMutation.ts index 0e36bad2a..b41e4cc5b 100644 --- a/packages/web/lib/networking/mutations/deleteRuleMutation.ts +++ b/packages/web/lib/networking/mutations/deleteRuleMutation.ts @@ -35,7 +35,6 @@ export async function deleteRuleMutation(id: string): Promise { const data = (await gqlFetcher(mutation, { id })) as DeleteRuleResult const output = data as any const error = data.deleteRule?.errorCodes?.find(() => true) - console.log('DATA: ', output.deleteRule) if (error) { throw error } diff --git a/packages/web/lib/networking/mutations/importFromIntegrationMutation.ts b/packages/web/lib/networking/mutations/importFromIntegrationMutation.ts index e74b5429b..0d14dba25 100644 --- a/packages/web/lib/networking/mutations/importFromIntegrationMutation.ts +++ b/packages/web/lib/networking/mutations/importFromIntegrationMutation.ts @@ -25,7 +25,6 @@ export async function importFromIntegrationMutation( }` const data = await gqlFetcher(mutation, { integrationId }) - console.log('integrationId: ', data) const output = data as ImportFromIntegrationDataResponseData | undefined const error = output?.importFromIntegration?.errorCodes?.find(() => true) console.log('error: ', error) diff --git a/packages/web/lib/networking/mutations/joinGroupMutation.ts b/packages/web/lib/networking/mutations/joinGroupMutation.ts index 86c7b9000..30d18be36 100644 --- a/packages/web/lib/networking/mutations/joinGroupMutation.ts +++ b/packages/web/lib/networking/mutations/joinGroupMutation.ts @@ -38,10 +38,7 @@ export async function joinGroupMutation( } ` - console.log('JoinGroupMutation', mutation) - const response = await gqlFetcher(mutation, { inviteCode }) - console.log(' -- response', response) const data = response as JoinGroupResponse | undefined const error = data?.errorCodes?.find(() => true) if (error) { diff --git a/packages/web/lib/networking/mutations/markEmailAsItemMutation.ts b/packages/web/lib/networking/mutations/markEmailAsItemMutation.ts index fc6e60690..c54a12726 100644 --- a/packages/web/lib/networking/mutations/markEmailAsItemMutation.ts +++ b/packages/web/lib/networking/mutations/markEmailAsItemMutation.ts @@ -25,7 +25,6 @@ export async function markEmailAsItemMutation( }` const data = await gqlFetcher(mutation, { recentEmailId }) - console.log('recentEmailId: ', data) const output = data as MarkEmailAsItemDataResponseData | undefined const error = output?.markEmailAsItem?.errorCodes?.find(() => true) console.log('error: ', error) diff --git a/packages/web/lib/networking/mutations/setLabelsForHighlight.ts b/packages/web/lib/networking/mutations/setLabelsForHighlight.ts index a2c4521eb..b1f25f76a 100644 --- a/packages/web/lib/networking/mutations/setLabelsForHighlight.ts +++ b/packages/web/lib/networking/mutations/setLabelsForHighlight.ts @@ -31,18 +31,10 @@ export async function setLabelsForHighlight( ${labelFragment} ` - console.log( - 'setting label for highlight id: ', - highlightId, - 'labelIds', - labelIds - ) - try { const data = (await gqlFetcher(mutation, { input: { highlightId, labelIds }, })) as SetLabelsForHighlightResult - console.log(' -- errorCodes', data.setLabelsForHighlight.errorCodes) return data.setLabelsForHighlight.errorCodes ? undefined diff --git a/packages/web/lib/networking/mutations/updateLabelMutation.ts b/packages/web/lib/networking/mutations/updateLabelMutation.ts index d05f11b35..07e6cdbc8 100644 --- a/packages/web/lib/networking/mutations/updateLabelMutation.ts +++ b/packages/web/lib/networking/mutations/updateLabelMutation.ts @@ -3,8 +3,8 @@ import { gqlFetcher } from '../networkHelpers' export type UpdateLabelInput = { labelId: string - name: string, - color: string, + name: string + color: string description?: string } @@ -39,9 +39,7 @@ export async function updateLabelMutation( try { const data = await gqlFetcher(mutation) - console.log(input, data); const output = data as any - console.log(output) return output?.updatedLabel } catch (err) { return undefined diff --git a/packages/web/lib/networking/mutations/uploadImportFileMutation.ts b/packages/web/lib/networking/mutations/uploadImportFileMutation.ts index 055fe9078..aac8e9675 100644 --- a/packages/web/lib/networking/mutations/uploadImportFileMutation.ts +++ b/packages/web/lib/networking/mutations/uploadImportFileMutation.ts @@ -32,10 +32,8 @@ export async function uploadImportFileRequestMutation( }` const data = await gqlFetcher(mutation, { type, contentType }) - console.log('UploadImportFile: ', data) const output = data as UploadImportFileResponseData | undefined const error = output?.uploadImportFile?.errorCodes?.find(() => true) - console.log('error: ', error) if (error) { throw error } diff --git a/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx b/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx index c12443eec..edee9e662 100644 --- a/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx +++ b/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx @@ -53,8 +53,6 @@ export function useGetArticleOriginalHtmlQuery({ ) const resultData: ArticleData | undefined = data as ArticleData - console.log('RESULT', JSON.stringify(data)) - return resultData?.article.article.originalHtml } diff --git a/packages/web/lib/networking/queries/useGetIntegrationsQuery.tsx b/packages/web/lib/networking/queries/useGetIntegrationsQuery.tsx index 711079bae..9c28e2505 100644 --- a/packages/web/lib/networking/queries/useGetIntegrationsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetIntegrationsQuery.tsx @@ -53,8 +53,6 @@ export function useGetIntegrationsQuery(): IntegrationsQueryResponse { ` const { data, mutate, isValidating } = useSWR(query, publicGqlFetcher) - console.log('integrations data', data) - try { if (data) { const result = data as IntegrationsQueryResponseData diff --git a/packages/web/lib/networking/queries/useGetWebhooksQuery.tsx b/packages/web/lib/networking/queries/useGetWebhooksQuery.tsx index 2fa16675b..ea97cd71d 100644 --- a/packages/web/lib/networking/queries/useGetWebhooksQuery.tsx +++ b/packages/web/lib/networking/queries/useGetWebhooksQuery.tsx @@ -62,8 +62,6 @@ export function useGetWebhooksQuery(): WebhooksQueryResponse { ` const { data, mutate, isValidating } = useSWR(query, publicGqlFetcher) - console.log('webhooks data', data) - try { if (data) { const result = data as WebhooksQueryResponseData diff --git a/packages/web/next.config.js b/packages/web/next.config.js index e84e4370d..edac9f067 100644 --- a/packages/web/next.config.js +++ b/packages/web/next.config.js @@ -2,7 +2,7 @@ const ContentSecurityPolicy = ` default-src 'self'; base-uri 'self'; block-all-mixed-content; - connect-src 'self' ${process.env.NEXT_PUBLIC_SERVER_BASE_URL} https://proxy-prod.omnivore-image-cache.app https://accounts.google.com https://proxy-demo.omnivore-image-cache.app https://storage.googleapis.com https://api.segment.io https://cdn.segment.com https://widget.intercom.io https://api-iam.intercom.io https://static.intercomassets.com https://downloads.intercomcdn.com https://platform.twitter.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io wss://nexus-europe-websocket.intercom.io wss://nexus-australia-websocket.intercom.io; + connect-src 'self' ${process.env.NEXT_PUBLIC_SERVER_BASE_URL} https://proxy-prod.omnivore-image-cache.app https://accounts.google.com https://proxy-demo.omnivore-image-cache.app https://storage.googleapis.com https://api.segment.io https://cdn.segment.com https://widget.intercom.io https://api-iam.intercom.io https://static.intercomassets.com https://downloads.intercomcdn.com https://platform.twitter.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io wss://nexus-europe-websocket.intercom.io wss://nexus-australia-websocket.intercom.io https://tools.applemediaservices.com; font-src 'self' data: https://cdn.jsdelivr.net https://js.intercomcdn.com https://fonts.intercomcdn.com; form-action 'self' ${process.env.NEXT_PUBLIC_SERVER_BASE_URL} https://getpocket.com/auth/authorize https://intercom.help https://api-iam.intercom.io https://api-iam.eu.intercom.io https://api-iam.au.intercom.io; frame-ancestors 'none'; diff --git a/packages/web/pages/settings/api.tsx b/packages/web/pages/settings/api.tsx index 885a63a7a..69b51531a 100644 --- a/packages/web/pages/settings/api.tsx +++ b/packages/web/pages/settings/api.tsx @@ -65,7 +65,6 @@ export default function Api(): JSX.Element { name: 'expiredAt', required: true, onChange: (e) => { - console.log('onChange: ', e) let additionalDays = 0 switch (e.target.value) { case 'in 7 days': @@ -114,13 +113,28 @@ export default function Api(): JSX.Element { pageId="api-keys" pageInfoLink="https://docs.omnivore.app/integrations/api.html" headerTitle="API Keys" - createTitle="Generate API Key" + createTitle="Create an API Key" createAction={() => { onAdd() setName('') setExpiresAt(neverExpiresDate) setAddModalOpen(true) }} + suggestionInfo={{ + title: + 'Use API keys to Integrate Omnivore with other apps and services', + message: + 'Create API keys to connect Omnivore to other apps such as Logseq and Obsidian or to query the API. Check out the integrations documentation for more info on connecting to Omnivore via the API.', + docs: 'https://docs.omnivore.app/integrations/api.html', + key: '--settings-apikeys-show-help', + CTAText: 'Create an API Key', + onClickCTA: () => { + onAdd() + setName('') + setExpiresAt(neverExpiresDate) + setAddModalOpen(true) + }, + }} > {sortedApiKeys.length > 0 ? ( sortedApiKeys.map((apiKey, i) => { @@ -163,7 +177,7 @@ export default function Api(): JSX.Element { {addModalOpen && ( { + createEmail() + }, + }} > {sortedEmailAddresses.length > 0 ? ( sortedEmailAddresses.map((email, i) => { diff --git a/packages/web/pages/settings/emails/recent.tsx b/packages/web/pages/settings/emails/recent.tsx index 3dd1107b4..3a77c106e 100644 --- a/packages/web/pages/settings/emails/recent.tsx +++ b/packages/web/pages/settings/emails/recent.tsx @@ -168,9 +168,17 @@ export default function RecentEmails(): JSX.Element { return ( {sortedRecentEmails.length > 0 ? ( sortedRecentEmails.map((recentEmail: RecentEmail, i) => { diff --git a/packages/web/pages/settings/feeds/index.tsx b/packages/web/pages/settings/feeds/index.tsx index cbe310e8c..48f1d73cd 100644 --- a/packages/web/pages/settings/feeds/index.tsx +++ b/packages/web/pages/settings/feeds/index.tsx @@ -92,10 +92,21 @@ export default function Rss(): JSX.Element { pageId={'feeds'} pageInfoLink="https://docs.omnivore.app/using/feeds.html" headerTitle="Subscribed feeds" - createTitle="Add feed" + createTitle="Add a feed" createAction={() => { router.push('/settings/feeds/add') }} + suggestionInfo={{ + title: 'Add RSS and Atom feeds to your Omnivore account', + message: + 'When you add a new feed the last 24hrs of items, or at least one item will be added to your account. Feeds will be checked for updates every hour, and new items will be added to your library.', + docs: 'https://docs.omnivore.app/using/feeds.html', + key: '--settings-feeds-show-help', + CTAText: 'Add a feed', + onClickCTA: () => { + router.push('/settings/feeds/add') + }, + }} > {subscriptions.length === 0 ? ( diff --git a/packages/web/pages/settings/labels.tsx b/packages/web/pages/settings/labels.tsx index 572002fc2..03579e8bb 100644 --- a/packages/web/pages/settings/labels.tsx +++ b/packages/web/pages/settings/labels.tsx @@ -35,6 +35,9 @@ import { import { LabelChip } from '../../components/elements/LabelChip' import { ConfirmationModal } from '../../components/patterns/ConfirmationModal' import { InfoLink } from '../../components/elements/InfoLink' +import { SuggestionBox } from '../../components/elements/SuggestionBox' +import { usePersistedState } from '../../lib/hooks/usePersistedState' +import { FeatureHelpBox } from '../../components/elements/FeatureHelpBox' const HeaderWrapper = styled(Box, { width: '100%', @@ -153,6 +156,10 @@ export default function LabelsPage(): JSX.Element { const [confirmRemoveLabelId, setConfirmRemoveLabelId] = useState< string | null >(null) + const [showLabelPageHelp, setShowLabelPageHelp] = usePersistedState({ + key: `--settings-labels-show-help`, + initialValue: true, + }) const breakpoint = 768 applyStoredTheme(false) @@ -270,6 +277,23 @@ export default function LabelsPage(): JSX.Element { onOpenChange={() => setConfirmRemoveLabelId(null)} /> ) : null} + {showLabelPageHelp && ( + { + setShowLabelPageHelp(false) + }} + helpCTAText="Create a label" + onClickCTA={() => { + resetLabelState() + handleGenerateRandomColor() + setIsCreateMode(true) + }} + /> + )} - Add Label - - - + Create a label diff --git a/packages/web/pages/settings/subscriptions.tsx b/packages/web/pages/settings/subscriptions.tsx index 76b85b477..b6921e563 100644 --- a/packages/web/pages/settings/subscriptions.tsx +++ b/packages/web/pages/settings/subscriptions.tsx @@ -44,8 +44,15 @@ export default function SubscriptionsPage(): JSX.Element { return ( <> {sortedSubscriptions.length > 0 ? ( diff --git a/packages/web/pages/tools/bulk.tsx b/packages/web/pages/tools/bulk.tsx new file mode 100644 index 000000000..ab74aed27 --- /dev/null +++ b/packages/web/pages/tools/bulk.tsx @@ -0,0 +1,222 @@ +import { useCallback, useEffect, useState } from 'react' +import { applyStoredTheme } from '../../lib/themeUpdater' + +import { VStack } from '../../components/elements/LayoutPrimitives' + +import { StyledText } from '../../components/elements/StyledText' +import { ProfileLayout } from '../../components/templates/ProfileLayout' +import { + BulkAction, + bulkActionMutation, +} from '../../lib/networking/mutations/bulkActionMutation' +import { Button } from '../../components/elements/Button' +import { theme } from '../../components/tokens/stitches.config' +import { ConfirmationModal } from '../../components/patterns/ConfirmationModal' +import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' +import { useRouter } from 'next/router' +import { useGetLibraryItemsQuery } from '../../lib/networking/queries/useGetLibraryItemsQuery' +import { + BorderedFormInput, + FormLabel, +} from '../../components/elements/FormElements' + +type RunningState = 'none' | 'confirming' | 'running' | 'completed' + +export default function BulkPerformer(): JSX.Element { + const router = useRouter() + + applyStoredTheme(false) + + const [action, setAction] = useState() + const [query, setQuery] = useState('in:all') + const [expectedCount, setExpectedCount] = useState() + const [errorMessage, setErrorMessage] = useState() + const [runningState, setRunningState] = useState('none') + + const { itemsPages, isValidating } = useGetLibraryItemsQuery({ + searchQuery: query, + limit: 1, + sortDescending: false, + }) + + useEffect(() => { + console.log('itemsPages: ', itemsPages) + setExpectedCount(itemsPages?.find(() => true)?.search.pageInfo.totalCount) + }, [itemsPages]) + + const performAction = useCallback(() => { + ;(async () => { + console.log('performing action: ', action) + if (isValidating) { + showErrorToast('Query still being validated.') + return + } + if (!action) { + showErrorToast('Unable to run action, no action set.') + return + } + if (!expectedCount) { + showErrorToast('No items matching this query or query still running.') + return + } + if (!action) { + showErrorToast('No action selected') + return + } + try { + const success = await bulkActionMutation(action, query, expectedCount) + if (!success) { + throw 'Success not returned' + } + showSuccessToast('Bulk action is being performed.') + setRunningState('completed') + } catch (err) { + showErrorToast('Error performing bulk action.') + } + })() + }, [action, query, expectedCount]) + + return ( + + + + Perform a Bulk Action + + + Use this tool to perform a bulk operation on all the items in your + library.

+
+ + Note: This operation can not be undone. + + + {runningState == 'completed' ? ( + + Your bulk action has started. Please note that it can take some + time for these actions to complete. During this time, we recommend + not modifying your library as new items could be updated by the + action. + + ) : ( + <> + + Search Query + setQuery(e.target.value)} + required + /> + + Matches {expectedCount} items. + + + Action + + + + + + )} + + {runningState == 'confirming' && ( + setRunningState('none')} + /> + )} + {runningState == 'completed' && ( + + + + )} + + {errorMessage && ( + {errorMessage} + )} + +
+
+ ) +} diff --git a/packages/web/public/static/images/helpful-owl@1x.png b/packages/web/public/static/images/helpful-owl@1x.png new file mode 100644 index 000000000..70fcb7926 Binary files /dev/null and b/packages/web/public/static/images/helpful-owl@1x.png differ diff --git a/packages/web/public/static/images/helpful-owl@2x.png b/packages/web/public/static/images/helpful-owl@2x.png new file mode 100644 index 000000000..98692a53a Binary files /dev/null and b/packages/web/public/static/images/helpful-owl@2x.png differ diff --git a/packages/web/public/static/images/helpful-sloth@2x.png b/packages/web/public/static/images/helpful-sloth@2x.png new file mode 100644 index 000000000..13859597d Binary files /dev/null and b/packages/web/public/static/images/helpful-sloth@2x.png differ