Merge Main
This commit is contained in:
@ -2,6 +2,7 @@
|
||||
|
||||
[](https://github.com/omnivore-app/omnivore/actions/workflows/run-tests.yaml)
|
||||
[](https://discord.gg/h2z5rppzz9)
|
||||

|
||||
[](https://twitter.com/OmnivoreApp)
|
||||

|
||||
|
||||
|
||||
@ -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',
|
||||
},
|
||||
|
||||
@ -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)',
|
||||
|
||||
@ -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',
|
||||
|
||||
131
packages/web/components/elements/FeatureHelpBox.tsx
Normal file
131
packages/web/components/elements/FeatureHelpBox.tsx
Normal file
@ -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 (
|
||||
<HStack
|
||||
css={{
|
||||
gap: '10px',
|
||||
my: '40px',
|
||||
display: 'flex',
|
||||
width: 'fit-content',
|
||||
borderRadius: '5px',
|
||||
background: '$thBackground5',
|
||||
fontSize: '15px',
|
||||
fontFamily: '$inter',
|
||||
fontWeight: '500',
|
||||
color: '$grayText',
|
||||
px: '20px',
|
||||
py: '20px',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
<HStack css={{ gap: '30px' }}>
|
||||
<SpanBox
|
||||
css={{
|
||||
pt: '7px',
|
||||
alignSelf: 'center',
|
||||
'@smDown': { display: 'none' },
|
||||
}}
|
||||
>
|
||||
<HelpfulSlothImage />
|
||||
</SpanBox>
|
||||
<HelpSection {...props} />
|
||||
</HStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const HelpSection = (props: FeatureHelpBoxProps) => {
|
||||
return (
|
||||
<VStack css={{ gap: '20px' }}>
|
||||
<HStack css={{ width: '100%', gap: '20px' }} distribution="between">
|
||||
<SpanBox
|
||||
css={{
|
||||
fontSize: '22px',
|
||||
fontFamily: '$display',
|
||||
color: '$thTextContrast2',
|
||||
}}
|
||||
>
|
||||
{props.helpTitle}
|
||||
</SpanBox>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
title="Hide this tip"
|
||||
css={{ pt: '7px' }}
|
||||
onClick={(event) => {
|
||||
props.onDismiss()
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<CloseIcon
|
||||
size={25}
|
||||
color={theme.colors.thTextContrast2.toString()}
|
||||
/>
|
||||
</Button>
|
||||
</HStack>
|
||||
<SpanBox>{props.helpMessage}</SpanBox>
|
||||
<HStack css={{ gap: '20px' }}>
|
||||
{props.helpCTAText && props.onClickCTA && (
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
onClick={(event) => {
|
||||
if (props.onClickCTA) {
|
||||
props.onClickCTA()
|
||||
event.preventDefault()
|
||||
}
|
||||
}}
|
||||
css={{ '@smDown': { display: 'none' } }}
|
||||
>
|
||||
{props.helpCTAText}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
style="ctaLightGray"
|
||||
onClick={(event) => {
|
||||
window.open(props.docsDestination, '_blank', 'noreferrer')
|
||||
event.preventDefault()
|
||||
}}
|
||||
css={{ display: 'flex', flexDirection: 'row', gap: '10px' }}
|
||||
>
|
||||
{props.docsMessage}
|
||||
<SpanBox css={{ alignSelf: 'center' }}>
|
||||
<ArrowSquareOut
|
||||
size={12}
|
||||
color={theme.colors.thTextContrast2.toString()}
|
||||
/>
|
||||
</SpanBox>
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
@ -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'
|
||||
|
||||
|
||||
127
packages/web/components/elements/SuggestionBox.tsx
Normal file
127
packages/web/components/elements/SuggestionBox.tsx
Normal file
@ -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 (
|
||||
<SpanBox
|
||||
css={{
|
||||
cursor: 'pointer',
|
||||
a: {
|
||||
color: '$omnivoreCtaYellow',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!isExternal ? (
|
||||
<Link href={props.link}>{props.children}</Link>
|
||||
) : (
|
||||
<a href={props.link} target="_blank" rel="noreferrer">
|
||||
{props.children}
|
||||
</a>
|
||||
)}
|
||||
</SpanBox>
|
||||
)
|
||||
}
|
||||
|
||||
export const SuggestionBox = (props: SuggestionBoxProps) => {
|
||||
return (
|
||||
<HStack
|
||||
css={{
|
||||
gap: '10px',
|
||||
display: 'flex',
|
||||
flexDirection: props.size == 'large' ? 'column' : 'row',
|
||||
width: 'fit-content',
|
||||
borderRadius: '5px',
|
||||
background: props.background ?? '$thBackground3',
|
||||
fontSize: '15px',
|
||||
fontFamily: '$inter',
|
||||
fontWeight: '500',
|
||||
color: '$thTextContrast',
|
||||
px: '15px',
|
||||
py: props.size == 'large' ? '15px' : '10px',
|
||||
justifyContent: 'flex-start',
|
||||
'@smDown': {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VStack>
|
||||
{props.dismissible && (
|
||||
<SpanBox
|
||||
css={{
|
||||
marginLeft: 'auto',
|
||||
lineHeight: '2',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
fontSize: '10',
|
||||
fontWeight: '600',
|
||||
}}
|
||||
>
|
||||
<CloseIcon size={2} color="white" />
|
||||
</Button>
|
||||
</SpanBox>
|
||||
)}
|
||||
{props.helpMessage}
|
||||
{props.suggestions.map((suggestion, idx) => {
|
||||
return (
|
||||
<InternalOrExternalLink
|
||||
key={`suggestions-${idx}`}
|
||||
link={suggestion.url}
|
||||
>
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: '$omnivoreCtaYellow',
|
||||
pt: '15px',
|
||||
gap: '2px',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<>{suggestion.text}</>
|
||||
<ArrowRightIcon
|
||||
size={25}
|
||||
color={theme.colors.omnivoreCtaYellow.toString()}
|
||||
/>
|
||||
</SpanBox>
|
||||
</InternalOrExternalLink>
|
||||
)
|
||||
})}
|
||||
</VStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
39
packages/web/components/elements/icons/CloseIcon.tsx
Normal file
39
packages/web/components/elements/icons/CloseIcon.tsx
Normal file
@ -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<IconProps> {
|
||||
render() {
|
||||
const size = (this.props.size || 26).toString()
|
||||
const color = (this.props.color || '#2A2A2A').toString()
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 26 26`}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M10.7441 10.9399L14.9108 15.1066M14.9108 10.9399L10.7441 15.1066"
|
||||
stroke={color}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12.8281 3.64844C20.3281 3.64844 22.2031 5.52344 22.2031 13.0234C22.2031 20.5234 20.3281 22.3984 12.8281 22.3984C5.32813 22.3984 3.45312 20.5234 3.45312 13.0234C3.45312 5.52344 5.32813 3.64844 12.8281 3.64844Z"
|
||||
stroke={color}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
12
packages/web/components/elements/images/HelpfulOwlImage.tsx
Normal file
12
packages/web/components/elements/images/HelpfulOwlImage.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
export const HelpfulOwlImage = () => {
|
||||
return (
|
||||
<Image
|
||||
src="/static/images/helpful-owl@2x.png"
|
||||
width={200}
|
||||
height={200}
|
||||
alt="Picture of an owl reading"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
export const HelpfulSlothImage = () => {
|
||||
return (
|
||||
<Image
|
||||
src="/static/images/helpful-sloth@2x.png"
|
||||
width={200}
|
||||
height={200}
|
||||
alt="Picture of a sloth reading"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -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 = {
|
||||
|
||||
@ -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 (
|
||||
<Dropdown triggerElement={props.triggerElement}>
|
||||
<VStack css={{ py: '12px', px: '24px' }}>
|
||||
<StyledText style="menuTitle">Theme</StyledText>
|
||||
<HStack css={{ py: '8px', width: '100%', gap: '25px' }}>
|
||||
<Button
|
||||
style="themeSwitch"
|
||||
css={{ background: '#FFFFFF' }}
|
||||
data-state={isDark ? 'unselected' : 'selected'}
|
||||
onClick={() => {
|
||||
props.actionHandler('apply-light-theme')
|
||||
setCurrentTheme(currentThemeName())
|
||||
}}
|
||||
>
|
||||
{!isDark && <Check color="#F9D354" size={32} />}
|
||||
</Button>
|
||||
<Button
|
||||
style="themeSwitch"
|
||||
css={{ background: '#3D3D3D' }}
|
||||
data-state={isDark ? 'selected' : 'unselected'}
|
||||
onClick={() => {
|
||||
props.actionHandler('apply-dark-theme')
|
||||
setCurrentTheme(currentThemeName())
|
||||
}}
|
||||
>
|
||||
{isDark && <Check color="#F9D354" size={32} />}
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<DropdownOption
|
||||
onSelect={() => props.actionHandler('navigate-to-install')}
|
||||
title="Install"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => props.actionHandler('navigate-to-emails')}
|
||||
title="Emails"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => props.actionHandler('navigate-to-labels')}
|
||||
title="Labels"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => props.actionHandler('navigate-to-api')}
|
||||
title="API Keys"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => props.actionHandler('navigate-to-integrations')}
|
||||
title="Integrations"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => window.open('https://docs.omnivore.app', '_blank')}
|
||||
title="Documentation"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => window.Intercom('show')}
|
||||
title="Feedback"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => props.actionHandler('logout')}
|
||||
title="Logout"
|
||||
/>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
@ -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',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingRight: '10px',
|
||||
}}
|
||||
>
|
||||
<OmnivoreNameLogo href={props.user ? '/home' : '/login'} />
|
||||
</Box>
|
||||
|
||||
<LogoBox />
|
||||
<HStack css={{ ml: 'auto' }}>
|
||||
<PrimaryDropdown showThemeSection={true} />
|
||||
</HStack>
|
||||
|
||||
@ -13,7 +13,6 @@ type ErrorLayoutProps = {
|
||||
|
||||
export function ErrorLayout(props: ErrorLayoutProps): JSX.Element {
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
console.log(viewerData?.me)
|
||||
|
||||
return (
|
||||
<VStack alignment="center" distribution="start" css={{ height: '100%' }}>
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => headerDropdownActionHandler('navigate-to-feeds')}
|
||||
title="Feeds"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => headerDropdownActionHandler('navigate-to-emails')}
|
||||
title="Emails"
|
||||
@ -170,17 +177,6 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{props.startSelectMultiple && (
|
||||
<>
|
||||
<DropdownOption
|
||||
onSelect={() =>
|
||||
props.startSelectMultiple && props.startSelectMultiple()
|
||||
}
|
||||
title="Select Multiple"
|
||||
/>
|
||||
<DropdownSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownOption
|
||||
onSelect={() => headerDropdownActionHandler('navigate-to-api')}
|
||||
title="API Keys"
|
||||
@ -320,8 +316,7 @@ function ThemeSection(props: PrimaryDropdownProps): JSX.Element {
|
||||
props.updateLayout && props.updateLayout('LIST_LAYOUT')
|
||||
}}
|
||||
>
|
||||
<ListViewIcon
|
||||
size={30}
|
||||
<ListLayoutIcon
|
||||
color={theme.colors.thTextContrast2.toString()}
|
||||
/>
|
||||
</StyledToggleButton>
|
||||
|
||||
@ -49,7 +49,6 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element {
|
||||
<Box
|
||||
css={{
|
||||
height: HEADER_HEIGHT,
|
||||
bg: '$grayBase',
|
||||
}}
|
||||
></Box>
|
||||
{props.children}
|
||||
|
||||
@ -155,6 +155,17 @@ export function EmailSignup(): JSX.Element {
|
||||
|
||||
{errorMessage && <StyledText style="error">{errorMessage}</StyledText>}
|
||||
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{
|
||||
p: '0px',
|
||||
color: '$omnivoreLightGray',
|
||||
}}
|
||||
>
|
||||
Omnivore will send you daily tips for your first week as a new user.
|
||||
If you don't like them you can unsubscribe.
|
||||
</StyledText>
|
||||
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="end"
|
||||
|
||||
@ -1,14 +1,8 @@
|
||||
import Link from 'next/link'
|
||||
import { Book } from 'phosphor-react'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { Box } from '../../elements/LayoutPrimitives'
|
||||
import { useMemo } from 'react'
|
||||
import { searchQuery } from '../../../lib/networking/queries/search'
|
||||
import { LIBRARY_LEFT_MENU_WIDTH } from './LibraryFilterMenu'
|
||||
import { LayoutType } from './HomeFeedContainer'
|
||||
import { ArrowRightIcon } from '../../elements/icons/ArrowRightIcon'
|
||||
import { SuggestionBox, SuggestionAction } from '../../elements/SuggestionBox'
|
||||
|
||||
type EmptyLibraryProps = {
|
||||
searchTerm: string | undefined
|
||||
@ -17,76 +11,46 @@ type EmptyLibraryProps = {
|
||||
layoutType: LayoutType
|
||||
}
|
||||
|
||||
type MessageType = 'feed' | 'newsletter' | 'library'
|
||||
type MessageType =
|
||||
| 'inbox'
|
||||
| 'continue'
|
||||
| 'non-feed'
|
||||
| 'highlight'
|
||||
| 'unlabeled'
|
||||
| 'files'
|
||||
| 'archive'
|
||||
| 'feed'
|
||||
| 'subscription'
|
||||
| 'newsletter'
|
||||
| 'library'
|
||||
|
||||
type HelpMessageProps = {
|
||||
type: MessageType
|
||||
}
|
||||
|
||||
const HelpMessage = (props: HelpMessageProps) => {
|
||||
switch (props.type) {
|
||||
case 'library':
|
||||
return (
|
||||
<>
|
||||
You can add a link or read more about Omnivore's{' '}
|
||||
<a
|
||||
href="https://docs.omnivore.app/using/search.html"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
advanced search
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
)
|
||||
case 'feed':
|
||||
return (
|
||||
<>
|
||||
You can subscribe to RSS feeds using the{' '}
|
||||
<Link href="/settings/feeds" passHref>
|
||||
feeds page
|
||||
</Link>
|
||||
. Learn more about feeds at 's{' '}
|
||||
<a
|
||||
href="https://docs.omnivore.app/using/feeds.html"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
docs.omnivore.app/using/feeds.html
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
)
|
||||
case 'newsletter':
|
||||
return (
|
||||
<>
|
||||
Create email addresses that can be used to subscribe to newsletters on
|
||||
the{' '}
|
||||
<Link href="/settings/emails" passHref>
|
||||
emails page
|
||||
</Link>
|
||||
. Learn more about reading newsletters in Omnivore at 's{' '}
|
||||
<a
|
||||
href="https://docs.omnivore.app/using/inbox.html"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
docs.omnivore.app/using/inbox.html
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
)
|
||||
}
|
||||
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<SuggestionMessage>(() => {
|
||||
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 (
|
||||
<HStack
|
||||
css={{
|
||||
gap: '10px',
|
||||
width: 'fit-content',
|
||||
borderRadius: '5px',
|
||||
background: '$thBackground3',
|
||||
fontSize: '15px',
|
||||
fontFamily: '$inter',
|
||||
fontWeight: '500',
|
||||
color: '$thTextContrast',
|
||||
padding: '10px',
|
||||
justifyContent: 'flex-start',
|
||||
'@smDown': {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{helpMessage}
|
||||
<SpanBox css={{ cursor: 'pointer' }}>
|
||||
<Link href={helpTarget} passHref>
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: '$omnivoreCtaYellow',
|
||||
gap: '2px',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<>Click Here</>
|
||||
<ArrowRightIcon
|
||||
size={25}
|
||||
color={theme.colors.omnivoreCtaYellow.toString()}
|
||||
/>
|
||||
</SpanBox>
|
||||
</Link>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
<>
|
||||
{helpMessage ? (
|
||||
<SuggestionBox
|
||||
helpMessage={helpMessage.message}
|
||||
suggestions={helpMessage.actions}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -187,8 +165,24 @@ export const EmptyLibrary = (props: EmptyLibraryProps) => {
|
||||
const type = useMemo<MessageType>(() => {
|
||||
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) => {
|
||||
}}
|
||||
>
|
||||
<ErrorBox type={type} />
|
||||
<SuggestionBox type={type} />
|
||||
<Suggestion type={type} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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': {
|
||||
|
||||
@ -172,6 +172,7 @@ function Subscriptions(props: LibraryFilterMenuProps): JSX.Element {
|
||||
>
|
||||
{!collapsed ? (
|
||||
<>
|
||||
<FilterButton filterTerm="in:subscription" text="All" {...props} />
|
||||
<FilterButton filterTerm={`label:RSS`} text="Feeds" {...props} />
|
||||
<FilterButton
|
||||
filterTerm={`label:Newsletter`}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
@ -54,7 +54,11 @@ type LibraryHeaderProps = {
|
||||
|
||||
performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => void
|
||||
|
||||
handleLinkSubmission: (link: string, timezone: string, locale:string) => Promise<void>,
|
||||
handleLinkSubmission: (
|
||||
link: string,
|
||||
timezone: string,
|
||||
locale: string
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
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<void>,
|
||||
handleLinkSubmission: (
|
||||
link: string,
|
||||
timezone: string,
|
||||
locale: string
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
export function SearchBox(props: SearchBoxProps): JSX.Element {
|
||||
const inputRef = useRef<HTMLInputElement | null>(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 <Plus
|
||||
{(() => {
|
||||
if (isAddAction) {
|
||||
return (
|
||||
<Plus
|
||||
size={props.compact ? 15 : 20}
|
||||
color={theme.colors.graySolid.toString()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <MagnifyingGlass
|
||||
return (
|
||||
<MagnifyingGlass
|
||||
size={props.compact ? 15 : 20}
|
||||
color={theme.colors.graySolid.toString()}
|
||||
/>
|
||||
})()
|
||||
}
|
||||
|
||||
)
|
||||
})()}
|
||||
</HStack>
|
||||
<form
|
||||
onSubmit={async (event) => {
|
||||
@ -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<void>,
|
||||
handleLinkSubmission: (
|
||||
link: string,
|
||||
timezone: string,
|
||||
locale: string
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
function MultiSelectControls(props: ControlButtonBoxProps): JSX.Element {
|
||||
@ -517,13 +531,6 @@ function SearchControlButtonBox(
|
||||
</Button>
|
||||
<PrimaryDropdown
|
||||
showThemeSection={true}
|
||||
startSelectMultiple={
|
||||
props.allowSelectMultiple
|
||||
? () => {
|
||||
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')
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
@ -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) => (
|
||||
<Dropdown
|
||||
align={'end'}
|
||||
@ -273,6 +288,11 @@ const CreateButton = (props: CreateButtonProps): JSX.Element => {
|
||||
}
|
||||
|
||||
export const SettingsTable = (props: SettingsTableProps): JSX.Element => {
|
||||
const [showSuggestion, setShowSuggestion] = usePersistedState<boolean>({
|
||||
key: props.suggestionInfo.key,
|
||||
initialValue: !!props.suggestionInfo,
|
||||
})
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<Toaster
|
||||
@ -299,6 +319,19 @@ export const SettingsTable = (props: SettingsTableProps): JSX.Element => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.suggestionInfo && showSuggestion && (
|
||||
<FeatureHelpBox
|
||||
helpTitle={props.suggestionInfo.title}
|
||||
helpMessage={props.suggestionInfo.message}
|
||||
docsMessage={'Read the Docs'}
|
||||
docsDestination={props.suggestionInfo.docs}
|
||||
onDismiss={() => {
|
||||
setShowSuggestion(false)
|
||||
}}
|
||||
helpCTAText={props.suggestionInfo.CTAText}
|
||||
onClickCTA={props.suggestionInfo.onClickCTA}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
|
||||
@ -103,8 +103,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
|
||||
borderWidths: {},
|
||||
borderStyles: {},
|
||||
shadows: {
|
||||
panelShadow: '0px 4px 18px rgba(120, 123, 134, 0.12)',
|
||||
cardBoxShadow: '0px 16px 25px 16px rgba(32, 31, 29, 0.1)',
|
||||
cardBoxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05);',
|
||||
},
|
||||
zIndices: {},
|
||||
transitions: {},
|
||||
|
||||
@ -99,7 +99,6 @@ export const useReaderSettings = (): ReaderSettings => {
|
||||
break
|
||||
case 'setMarginWidth': {
|
||||
const value = Number(arg)
|
||||
console.log('setMarginWidth: ', value)
|
||||
if (value >= 200 && value <= 560) {
|
||||
setMarginWidth(value)
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -11,7 +11,9 @@ type CreateNewsletterEmail = {
|
||||
newsletterEmail: NewsletterEmail
|
||||
}
|
||||
|
||||
export async function createNewsletterEmailMutation(): Promise<string | undefined> {
|
||||
export async function createNewsletterEmailMutation(): Promise<
|
||||
string | undefined
|
||||
> {
|
||||
const mutation = gql`
|
||||
mutation createNewsletterEmailMutation {
|
||||
createNewsletterEmail {
|
||||
@ -29,9 +31,10 @@ export async function createNewsletterEmailMutation(): Promise<string | undefine
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation) as CreateNewsletterEmailResult
|
||||
console.log('created email', data)
|
||||
return data.errorCodes ? undefined : data.createNewsletterEmail.newsletterEmail.id
|
||||
const data = (await gqlFetcher(mutation)) as CreateNewsletterEmailResult
|
||||
return data.errorCodes
|
||||
? undefined
|
||||
: data.createNewsletterEmail.newsletterEmail.id
|
||||
} catch (error) {
|
||||
console.log('createNewsletterEmailMutation error', error)
|
||||
return undefined
|
||||
|
||||
@ -12,7 +12,8 @@ export async function createReminderMutation(
|
||||
linkId: string,
|
||||
reminderType: ReminderType,
|
||||
archiveUntil: boolean,
|
||||
sendNotification: boolean): Promise<string | undefined> {
|
||||
sendNotification: boolean
|
||||
): Promise<string | undefined> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,8 +26,6 @@ export async function deleteAccountMutation(
|
||||
}
|
||||
`
|
||||
|
||||
console.log('deleteAccountMutation', mutation)
|
||||
|
||||
try {
|
||||
const response = await gqlFetcher(mutation, { userId })
|
||||
console.log('response', response)
|
||||
|
||||
@ -35,7 +35,6 @@ export async function deleteRuleMutation(id: string): Promise<Rule> {
|
||||
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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 && (
|
||||
<FormModal
|
||||
title={'Generate API Key'}
|
||||
title={'Create an API Key'}
|
||||
onSubmit={onCreate}
|
||||
onOpenChange={setAddModalOpen}
|
||||
inputs={formInputs}
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
SettingsTableRow,
|
||||
} from '../../../components/templates/settings/SettingsTable'
|
||||
import { ConfirmationModal } from '../../../components/patterns/ConfirmationModal'
|
||||
import { SuggestionBox } from '../../../components/elements/SuggestionBox'
|
||||
|
||||
enum TextType {
|
||||
EmailAddress,
|
||||
@ -121,6 +122,17 @@ export default function EmailsPage(): JSX.Element {
|
||||
headerTitle="Address"
|
||||
createTitle="Create a new email address"
|
||||
createAction={createEmail}
|
||||
suggestionInfo={{
|
||||
title: 'Subscribe to newsletters with an Omnivore Email Address',
|
||||
message:
|
||||
'Create an Omnivore email address and use it to subscribe to newsletters or send yourself documents. Newsletters and documents will be categorized and added to your library when we receive a message. View all received emails with the "Recently Received Emails" link at the bottom of this page.',
|
||||
docs: 'https://docs.omnivore.app/using/inbox.html',
|
||||
key: '--settings-emails-show-help',
|
||||
CTAText: 'Create an email address',
|
||||
onClickCTA: () => {
|
||||
createEmail()
|
||||
},
|
||||
}}
|
||||
>
|
||||
{sortedEmailAddresses.length > 0 ? (
|
||||
sortedEmailAddresses.map((email, i) => {
|
||||
|
||||
@ -168,9 +168,17 @@ export default function RecentEmails(): JSX.Element {
|
||||
|
||||
return (
|
||||
<SettingsTable
|
||||
pageId="api-keys"
|
||||
pageId="recent-emails"
|
||||
pageInfoLink="https://docs.omnivore.app/using/inbox.html"
|
||||
headerTitle="Recently Received Emails"
|
||||
suggestionInfo={{
|
||||
title:
|
||||
'View original emails that have been recently received in your Omnivore inbox.',
|
||||
message:
|
||||
"Your 30 most recent emails are stored below. You can click on each email to view its original content or it's text content. If an email was not correctly classified as an article you can mark it as an article and it will be added to your library.",
|
||||
docs: 'https://docs.omnivore.app/using/inbox.html',
|
||||
key: '--settings-recent-emails-show-help',
|
||||
}}
|
||||
>
|
||||
{sortedRecentEmails.length > 0 ? (
|
||||
sortedRecentEmails.map((recentEmail: RecentEmail, i) => {
|
||||
|
||||
@ -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 ? (
|
||||
<EmptySettingsRow text={isValidating ? '-' : 'No feeds subscribed'} />
|
||||
|
||||
@ -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<boolean>({
|
||||
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 && (
|
||||
<FeatureHelpBox
|
||||
helpTitle="Use labels to organize your library and optimize your workflow."
|
||||
helpMessage="Use this page to view and edit all your labels. Labels can be attached to individual library items, or your highlights, and are used to keep your library organized."
|
||||
docsMessage={'Read the Docs'}
|
||||
docsDestination="https://docs.omnivore.app/using/organizing.html#labels"
|
||||
onDismiss={() => {
|
||||
setShowLabelPageHelp(false)
|
||||
}}
|
||||
helpCTAText="Create a label"
|
||||
onClickCTA={() => {
|
||||
resetLabelState()
|
||||
handleGenerateRandomColor()
|
||||
setIsCreateMode(true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<HeaderWrapper>
|
||||
<Box
|
||||
style={{
|
||||
@ -305,24 +329,11 @@ export default function LabelsPage(): JSX.Element {
|
||||
>
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'none',
|
||||
'@md': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SpanBox>Add Label</SpanBox>
|
||||
</SpanBox>
|
||||
<SpanBox
|
||||
css={{
|
||||
p: '0',
|
||||
display: 'flex',
|
||||
'@md': {
|
||||
display: 'none',
|
||||
},
|
||||
'@md': {},
|
||||
}}
|
||||
>
|
||||
<Plus size={24} />
|
||||
<SpanBox>Create a label</SpanBox>
|
||||
</SpanBox>
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@ -44,8 +44,15 @@ export default function SubscriptionsPage(): JSX.Element {
|
||||
return (
|
||||
<SettingsTable
|
||||
pageId="settings-subscriptions-tag"
|
||||
pageInfoLink="https://docs.omnivore.app/using/inbox.html"
|
||||
pageInfoLink="https://docs.omnivore.app/using/feeds.html"
|
||||
headerTitle="Subscriptions"
|
||||
suggestionInfo={{
|
||||
title: 'View and manage all your Feed and Newsletter subscriptions',
|
||||
message:
|
||||
'Use this page to view and manage all the Feeds (RSS & Atom) and Newsletters you have subscribed to.',
|
||||
docs: 'https://docs.omnivore.app/using/inbox.html',
|
||||
key: '--settings-recent-subscriptions-show-help',
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{sortedSubscriptions.length > 0 ? (
|
||||
|
||||
222
packages/web/pages/tools/bulk.tsx
Normal file
222
packages/web/pages/tools/bulk.tsx
Normal file
@ -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<BulkAction | undefined>()
|
||||
const [query, setQuery] = useState<string>('in:all')
|
||||
const [expectedCount, setExpectedCount] = useState<number | undefined>()
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>()
|
||||
const [runningState, setRunningState] = useState<RunningState>('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 (
|
||||
<ProfileLayout logoDestination="/home">
|
||||
<VStack
|
||||
alignment="start"
|
||||
css={{
|
||||
padding: '16px',
|
||||
background: 'white',
|
||||
minWidth: '340px',
|
||||
width: '70vw',
|
||||
maxWidth: '576px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #3D3D3D',
|
||||
boxShadow: '#B1B1B1 9px 9px 9px -9px',
|
||||
}}
|
||||
>
|
||||
<StyledText
|
||||
style="modalHeadline"
|
||||
css={{
|
||||
color: theme.colors.omnivoreGray.toString(),
|
||||
}}
|
||||
>
|
||||
Perform a Bulk Action
|
||||
</StyledText>
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{ pt: '10px', color: theme.colors.omnivoreGray.toString() }}
|
||||
>
|
||||
Use this tool to perform a bulk operation on all the items in your
|
||||
library.<br></br>
|
||||
</StyledText>
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{ pt: '0px', color: theme.colors.omnivoreGray.toString() }}
|
||||
>
|
||||
<b>Note:</b> This operation can not be undone.
|
||||
</StyledText>
|
||||
<VStack css={{ pt: '36px', width: '100%' }}>
|
||||
{runningState == 'completed' ? (
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{
|
||||
pt: '10px',
|
||||
pb: '20px',
|
||||
color: theme.colors.omnivoreGray.toString(),
|
||||
}}
|
||||
>
|
||||
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.
|
||||
</StyledText>
|
||||
) : (
|
||||
<>
|
||||
<VStack css={{ width: '100%', gap: '15px' }}>
|
||||
<FormLabel className="required">Search Query</FormLabel>
|
||||
<BorderedFormInput
|
||||
key="fullname"
|
||||
type="text"
|
||||
name="name"
|
||||
defaultValue={query}
|
||||
placeholder="Enter your query"
|
||||
css={{ bg: 'white', color: 'black' }}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<StyledText style="footnote" css={{ mt: '5px' }}>
|
||||
Matches {expectedCount} items.
|
||||
</StyledText>
|
||||
|
||||
<FormLabel className="required">Action</FormLabel>
|
||||
<select
|
||||
disabled={runningState == 'running'}
|
||||
onChange={(event) => {
|
||||
const updatedAction: BulkAction =
|
||||
BulkAction[
|
||||
event.currentTarget.value as keyof typeof BulkAction
|
||||
]
|
||||
setAction(updatedAction)
|
||||
}}
|
||||
style={{
|
||||
margin: '0px',
|
||||
padding: '8px',
|
||||
height: '38px',
|
||||
borderRadius: '6px',
|
||||
minWidth: '196px',
|
||||
color: theme.colors.omnivoreGray.toString(),
|
||||
}}
|
||||
>
|
||||
<option value="none">Choose bulk action</option>
|
||||
<option value="ARCHIVE">Archive All</option>
|
||||
<option value="DELETE">Delete All</option>
|
||||
</select>
|
||||
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
if (!expectedCount) {
|
||||
alert(
|
||||
'No items matching this query or query still running.'
|
||||
)
|
||||
return
|
||||
}
|
||||
if (!action) {
|
||||
alert('No action selected')
|
||||
return
|
||||
}
|
||||
setRunningState('confirming')
|
||||
}}
|
||||
style="ctaDarkYellow"
|
||||
>
|
||||
Perform Action
|
||||
</Button>
|
||||
</VStack>
|
||||
</>
|
||||
)}
|
||||
|
||||
{runningState == 'confirming' && (
|
||||
<ConfirmationModal
|
||||
message={`Are you sure you want to ${action} the ${expectedCount} items matching this query? This operation can not be undone.`}
|
||||
onAccept={performAction}
|
||||
onOpenChange={() => setRunningState('none')}
|
||||
/>
|
||||
)}
|
||||
{runningState == 'completed' && (
|
||||
<VStack css={{ width: '100%' }} alignment="center">
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
window.location.href = '/home'
|
||||
e.preventDefault()
|
||||
}}
|
||||
style="ctaDarkYellow"
|
||||
>
|
||||
Return to Library
|
||||
</Button>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<StyledText style="error">{errorMessage}</StyledText>
|
||||
)}
|
||||
</VStack>
|
||||
</VStack>
|
||||
</ProfileLayout>
|
||||
)
|
||||
}
|
||||
BIN
packages/web/public/static/images/helpful-owl@1x.png
Normal file
BIN
packages/web/public/static/images/helpful-owl@1x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
BIN
packages/web/public/static/images/helpful-owl@2x.png
Normal file
BIN
packages/web/public/static/images/helpful-owl@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
BIN
packages/web/public/static/images/helpful-sloth@2x.png
Normal file
BIN
packages/web/public/static/images/helpful-sloth@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
Reference in New Issue
Block a user