Rebase
This commit is contained in:
committed by
Jackson Harper
parent
f628f08d9b
commit
cec6dc5197
@ -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={() => {
|
||||
|
||||
@ -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
|
||||
|
||||
157
packages/web/components/templates/homeFeed/EditTitleModal.tsx
Normal file
157
packages/web/components/templates/homeFeed/EditTitleModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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`}
|
||||
|
||||
48
packages/web/lib/networking/mutations/updatePageMutation.ts
Normal file
48
packages/web/lib/networking/mutations/updatePageMutation.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
40
packages/web/stories/EditTitleModal.stories.tsx
Normal file
40
packages/web/stories/EditTitleModal.stories.tsx
Normal 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')}
|
||||
|
||||
/>
|
||||
)
|
||||
Reference in New Issue
Block a user