This commit is contained in:
gitstart-omnivore
2022-06-07 08:40:29 +00:00
committed by Jackson Harper
parent f628f08d9b
commit cec6dc5197
7 changed files with 295 additions and 8 deletions

View File

@ -15,6 +15,7 @@ export type CardMenuDropdownAction =
| 'set-labels'
| 'showOriginal'
| 'unsubscribe'
| 'editTitle'
type CardMenuProps = {
item: LibraryItemNode
@ -47,6 +48,10 @@ export function CardMenu(props: CardMenuProps): JSX.Element {
onSelect={() => props.actionHandler('showOriginal')}
title="Open Original"
/>
<DropdownOption
onSelect={() => props.actionHandler('editTitle')}
title="Edit Title"
/>
{isVipUser(props.viewer) && (
<DropdownOption
onSelect={() => {

View File

@ -5,6 +5,7 @@ import type { LibraryItemNode } from '../../../lib/networking/queries/useGetLibr
export type LinkedItemCardAction =
| 'showDetail'
| 'showOriginal'
| 'editTitle'
| 'archive'
| 'unarchive'
| 'delete'
@ -14,6 +15,7 @@ export type LinkedItemCardAction =
| 'snooze'
| 'set-labels'
| 'unsubscribe'
| 'update-item'
export type LinkedItemCardProps = {
item: LibraryItemNode

View File

@ -0,0 +1,157 @@
import {
ModalRoot,
ModalContent,
ModalOverlay,
} from '../../elements/ModalPrimitives'
import { VStack, HStack, Box } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { StyledText } from '../../elements/StyledText'
import { CrossIcon } from '../../elements/images/CrossIcon'
import { theme } from '../../tokens/stitches.config'
import { FormInput } from '../../elements/FormElements'
import { useState, useCallback } from 'react'
import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { StyledTextArea } from '../../elements/StyledTextArea'
import { updatePageMutation } from '../../../lib/networking/mutations/updatePageMutation'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
type EditTitleModalProps = {
onOpenChange: (open: boolean) => void
item: LibraryItem
updateItem: (item: LibraryItem) => Promise<void>,
}
export function EditTitleModal(props: EditTitleModalProps): JSX.Element {
const [title, setTitle] = useState(props.item.node.title)
const [description, setDescription] = useState(props.item.node.description)
const handleUpdateTitle = async () => {
if (title !== '') {
const res = await updatePageMutation({
pageId: props.item.node.id,
title,
description,
})
if (res) {
await props.updateItem({
cursor: props.item.cursor,
node: {
...props.item.node,
title: title,
description: description,
},
})
showSuccessToast('Link updated succesfully', {
position: 'bottom-right',
})
props.onOpenChange(false)
} else {
showErrorToast('There was an error updating your link', {
position: 'bottom-right',
})
}
} else {
showErrorToast('Title can\'t be empty', {
position: 'bottom-right',
})
}
}
return (
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
<ModalOverlay />
<ModalContent
css={{ bg: '$grayBg', maxWidth: '20em', pt: '0px' }}
onInteractOutside={() => {
// remove focus from modal
(document.activeElement as HTMLElement).blur()
}}
>
<VStack distribution="start" css={{ p: '$2' }}>
<HStack
distribution="between"
alignment="center"
css={{ width: '100%', mt: '4px' }}
>
<StyledText style="modalHeadline">
Edit Title or Description
</StyledText>
<Button
css={{ p: '10px', cursor: 'pointer', pt: '2px' }}
style="ghost"
onClick={() => {
props.onOpenChange(false)
}}
>
<CrossIcon
size={11}
strokeColor={theme.colors.grayTextContrast.toString()}
/>
</Button>
</HStack>
<StyledText css={{ mt: '22px', mb: '6px' }}>Title</StyledText>
<Box css={{ width: '100%' }}>
<form
onSubmit={(event) => {
event.preventDefault()
}}
>
<FormInput
type="text"
value={title}
autoFocus
placeholder="Edit Title"
onChange={(event) => setTitle(event.target.value)}
css={{
borderRadius: '8px',
border: '1px solid $grayTextContrast',
width: '100%',
p: '$2',
}}
/>
<StyledText css={{ mt: '22px', mb: '6px' }}>
Description
</StyledText>
<Box
css={{
border: '1px solid $grayTextContrast',
borderRadius: '8px',
}}
>
<StyledTextArea
css={{
mt: '$2',
width: '95%',
p: '$1',
height: '$6',
}}
placeholder="Edit Description"
value={description}
onChange={(event) => setDescription(event.target.value)}
maxLength={4000}
/>
</Box>
<HStack distribution="end" css={{ mt: '12px', width: '100%' }}>
<Button
onClick={() => props.onOpenChange(false)}
style="ctaGray"
css={{ mr: '16px' }}
>
Cancel
</Button>
<Button
onClick={handleUpdateTitle}
style="ctaDarkYellow"
css={{ mb: '0px' }}
>
Save
</Button>
</HStack>
</form>
</Box>
</VStack>
</ModalContent>
</ModalRoot>
)
}

View File

@ -39,7 +39,11 @@ import { SetLabelsModal } from '../article/SetLabelsModal'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { EmptyLibrary } from './EmptyLibrary'
import TopBarProgress from 'react-topbar-progress-indicator'
import { State, PageType } from '../../../lib/networking/fragments/articleFragment'
import {
State,
PageType,
} from '../../../lib/networking/fragments/articleFragment'
import { EditTitleModal } from './EditTitleModal'
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
@ -85,6 +89,7 @@ export function HomeFeedContainer(props: HomeFeedContainerProps): JSX.Element {
)
const [showAddLinkModal, setShowAddLinkModal] = useState(false)
const [showEditTitleModal, setShowEditTitleModal] = useState(false)
const [queryInputs, setQueryInputs] =
useState<LibraryItemsQueryInput>(defaultQuery)
@ -97,6 +102,9 @@ export function HomeFeedContainer(props: HomeFeedContainerProps): JSX.Element {
})
)
const { itemsPages, size, setSize, isValidating, performActionOnItem } =
useGetLibraryItemsQuery(queryInputs)
useEffect(() => {
if (!router.isReady) return
const q = router.query['q']
@ -106,13 +114,11 @@ export function HomeFeedContainer(props: HomeFeedContainerProps): JSX.Element {
}
if (qs !== (queryInputs.searchQuery || '')) {
setQueryInputs({ ...queryInputs, searchQuery: qs })
performActionOnItem('refresh', undefined as unknown as any)
}
// intentionally not watching queryInputs here to prevent infinite looping
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setQueryInputs, router.isReady, router.query])
const { itemsPages, size, setSize, isValidating, performActionOnItem } =
useGetLibraryItemsQuery(queryInputs)
}, [setQueryInputs, router.isReady, router.query, performActionOnItem])
const hasMore = useMemo(() => {
if (!itemsPages) {
@ -261,7 +267,10 @@ export function HomeFeedContainer(props: HomeFeedContainerProps): JSX.Element {
if (item.node.state === State.PROCESSING) {
router.push(`/${username}/links/${item.node.id}`)
} else {
const dl = item.node.pageType === PageType.HIGHLIGHTS ? `#${item.node.id}` : ''
const dl =
item.node.pageType === PageType.HIGHLIGHTS
? `#${item.node.id}`
: ''
router.push(`/${username}/${item.node.slug}` + dl)
}
}
@ -298,6 +307,8 @@ export function HomeFeedContainer(props: HomeFeedContainerProps): JSX.Element {
break
case 'unsubscribe':
performActionOnItem('unsubscribe', item)
case 'update-item':
performActionOnItem('update-item', item)
break
}
}
@ -438,6 +449,7 @@ export function HomeFeedContainer(props: HomeFeedContainerProps): JSX.Element {
}
const href = `${window.location.pathname}?${qp.toString()}`
router.push(href, href, { shallow: true })
performActionOnItem('refresh', undefined as unknown as any)
}}
loadMore={() => {
if (isValidating) {
@ -457,6 +469,8 @@ export function HomeFeedContainer(props: HomeFeedContainerProps): JSX.Element {
setLabelsTarget={setLabelsTarget}
showAddLinkModal={showAddLinkModal}
setShowAddLinkModal={setShowAddLinkModal}
showEditTitleModal={showEditTitleModal}
setShowEditTitleModal={setShowEditTitleModal}
setActiveItem={(item: LibraryItem) => {
activateCard(item.node.id)
}}
@ -482,6 +496,8 @@ type HomeFeedContentProps = {
setLabelsTarget: (target: LibraryItem | undefined) => void
showAddLinkModal: boolean
setShowAddLinkModal: (show: boolean) => void
showEditTitleModal: boolean
setShowEditTitleModal: (show: boolean) => void
setActiveItem: (item: LibraryItem) => void
actionHandler: (
action: LinkedItemCardAction,
@ -500,6 +516,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
const [showRemoveLinkConfirmation, setShowRemoveLinkConfirmation] =
useState(false)
const [linkToRemove, setLinkToRemove] = useState<LibraryItem>()
const [linkToEdit, setLinkToEdit] = useState<LibraryItem>()
const updateLayout = useCallback(
async (newLayout: LayoutType) => {
@ -723,6 +740,9 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
if (action === 'delete') {
setShowRemoveLinkConfirmation(true)
setLinkToRemove(linkedItem)
} else if (action === 'editTitle') {
props.setShowEditTitleModal(true)
setLinkToEdit(linkedItem)
} else {
props.actionHandler(action, linkedItem)
}
@ -756,6 +776,13 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
{props.showAddLinkModal && (
<AddLinkModal onOpenChange={() => props.setShowAddLinkModal(false)} />
)}
{props.showEditTitleModal && (
<EditTitleModal
updateItem={(item: LibraryItem) => props.actionHandler('update-item', item)}
onOpenChange={() => props.setShowEditTitleModal(false)}
item={linkToEdit as LibraryItem}
/>
)}
{props.shareTarget && viewerData?.me?.profile.username && (
<ShareArticleModal
url={`${webBaseURL}${viewerData?.me?.profile.username}/${props.shareTarget.node.slug}/highlights?r=true`}

View File

@ -0,0 +1,48 @@
import { gql } from 'graphql-request'
import { gqlFetcher } from '../networkHelpers'
export type UpdatePageInput = {
pageId: string
title: string,
description: string,
}
export async function updatePageMutation(
input: UpdatePageInput
): Promise<string | undefined> {
const mutation = gql`
mutation {
updatePage(
input: {
pageId: "${input.pageId}"
title: "${input.title}"
description: "${input.description}"
}
) {
... on UpdatePageSuccess {
updatedPage {
id
title
url
createdAt
author
image
description
publishedAt
}
}
... on UpdatePageError {
errorCodes
}
}
}
`
try {
const data = await gqlFetcher(mutation)
const output = data as any
return output.updatePage
} catch (err) {
return undefined
}
}

View File

@ -37,6 +37,7 @@ type LibraryItemAction =
| 'mark-unread'
| 'refresh'
| 'unsubscribe'
| 'update-item'
export type LibraryItemsData = {
search: LibraryItems
@ -195,6 +196,10 @@ export function useGetLibraryItemsQuery({
}
}
const getIndexOf = (page: LibraryItems, item: LibraryItem) => {
return page.edges.findIndex(i => i.node.id === item.node.id)
}
const performActionOnItem = async (
action: LibraryItemAction,
item: LibraryItem
@ -207,13 +212,14 @@ export function useGetLibraryItemsQuery({
if (!responsePages) {
return
}
for (const searchResults of responsePages) {
const itemIndex = searchResults.search.edges.indexOf(item)
const itemIndex = getIndexOf(searchResults.search, item)
if (itemIndex !== -1) {
if (typeof mutatedItem === 'undefined') {
searchResults.search.edges.splice(itemIndex, 1)
} else {
searchResults.search.edges[itemIndex] = mutatedItem
searchResults.search.edges.splice(itemIndex, 1, mutatedItem)
}
break
}
@ -330,6 +336,8 @@ export function useGetLibraryItemsQuery({
}
})
}
case 'update-item':
updateData(item)
break
case 'refresh':
await mutate()

View File

@ -0,0 +1,40 @@
import { ComponentStory, ComponentMeta } from '@storybook/react'
import { EditTitleModal } from '../components/templates/homeFeed/EditTitleModal'
import { LibraryItem } from '../lib/networking/queries/useGetLibraryItemsQuery'
export default {
title: 'Components/EditTitleModal',
component: EditTitleModal,
argTypes: {
onOpenChange: {
description:
'This is the function that changes the open and closed state of the modal',
},
item: {
description: 'The article whose title or description is to be changed.',
},
},
parameters: {
docs: {
page: null,
},
previewTabs: {
'storybook/docs/panel': { hidden: true },
},
viewMode: 'canvas',
},
} as ComponentMeta<typeof EditTitleModal>
export const EditTitleModalStory: ComponentStory<typeof EditTitleModal> = (
args
) => (
<EditTitleModal
onOpenChange={() => {}}
item={{
cursor: '',
node: { title: '', description: '' } as LibraryItem['node'],
}}
updateItem={async () => console.log('update item')}
/>
)