mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Update labels to use react-query
This commit is contained in:
parent
11e336735f
commit
1fc73bd59e
17 changed files with 343 additions and 340 deletions
|
|
@ -2,16 +2,17 @@ import AutosizeInput_, { AutosizeInputProps } from 'react-input-autosize'
|
|||
import { Box, SpanBox } from './LayoutPrimitives'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Label } from '../../lib/networking/fragments/labelFragment'
|
||||
import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { isTouchScreenDevice } from '../../lib/deviceType'
|
||||
import { EditLabelChip } from './EditLabelChip'
|
||||
import { LabelsDispatcher } from '../../lib/hooks/useSetPageLabels'
|
||||
import { EditLabelChipStack } from './EditLabelChipStack'
|
||||
import { useGetLabels } from '../../lib/networking/labels/useLabels'
|
||||
|
||||
// AutosizeInput is a Class component, but the types are broken in React 18.
|
||||
// TODO: Maybe move away from this component, since it hasn't been updated for 3 years.
|
||||
// https://github.com/JedWatson/react-input-autosize/issues
|
||||
const AutosizeInput = AutosizeInput_ as unknown as React.FunctionComponent<AutosizeInputProps>
|
||||
const AutosizeInput =
|
||||
AutosizeInput_ as unknown as React.FunctionComponent<AutosizeInputProps>
|
||||
|
||||
const MaxUnstackedLabels = 7
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ type LabelsPickerProps = {
|
|||
|
||||
export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
|
||||
const inputRef = useRef<HTMLInputElement | null>()
|
||||
const availableLabels = useGetLabelsQuery()
|
||||
const { data: availableLabels } = useGetLabels()
|
||||
const [isStackExpanded, setIsStackExpanded] = useState(false)
|
||||
const {
|
||||
focused,
|
||||
|
|
@ -80,9 +81,10 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
|
|||
setTabCount(_tabCount)
|
||||
}
|
||||
|
||||
const matches = availableLabels.labels.filter((l) =>
|
||||
l.name.toLowerCase().startsWith(_tabStartValue)
|
||||
)
|
||||
const matches =
|
||||
availableLabels?.filter((l) =>
|
||||
l.name.toLowerCase().startsWith(_tabStartValue)
|
||||
) ?? []
|
||||
|
||||
if (_tabCount < matches.length) {
|
||||
setInputValue(matches[_tabCount].name)
|
||||
|
|
|
|||
|
|
@ -8,13 +8,15 @@ import {
|
|||
ModalTitleBar,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { SetLabelsControl } from './SetLabelsControl'
|
||||
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
|
||||
import { showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
|
||||
import { LabelAction } from '../../../lib/hooks/useSetPageLabels'
|
||||
import { Button } from '../../elements/Button'
|
||||
import {
|
||||
useCreateLabel,
|
||||
useGetLabels,
|
||||
} from '../../../lib/networking/labels/useLabels'
|
||||
|
||||
type AddBulkLabelsModalProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
|
|
@ -24,12 +26,14 @@ type AddBulkLabelsModalProps = {
|
|||
export function AddBulkLabelsModal(
|
||||
props: AddBulkLabelsModalProps
|
||||
): JSX.Element {
|
||||
const availableLabels = useGetLabelsQuery()
|
||||
const { data: availableLabels } = useGetLabels()
|
||||
const createLabel = useCreateLabel()
|
||||
const [tabCount, setTabCount] = useState(-1)
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [tabStartValue, setTabStartValue] = useState('')
|
||||
const [errorMessage, setErrorMessage] =
|
||||
useState<string | undefined>(undefined)
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
const errorTimeoutRef = useRef<NodeJS.Timeout | undefined>()
|
||||
const [highlightLastLabel, setHighlightLastLabel] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
|
@ -97,10 +101,11 @@ export function AddBulkLabelsModal(
|
|||
(newLabels: Label[], tempLabel: Label) => {
|
||||
;(async () => {
|
||||
const currentLabels = newLabels
|
||||
const newLabel = await createLabelMutation(
|
||||
tempLabel.name,
|
||||
tempLabel.color
|
||||
)
|
||||
const newLabel = await createLabel.mutateAsync({
|
||||
name: tempLabel.name,
|
||||
color: tempLabel.color,
|
||||
description: undefined,
|
||||
})
|
||||
const idx = currentLabels.findIndex((l) => l.id === tempLabel.id)
|
||||
if (newLabel) {
|
||||
showSuccessToast(`Created label ${newLabel.name}`, {
|
||||
|
|
@ -132,7 +137,7 @@ export function AddBulkLabelsModal(
|
|||
const trimmedValue = value.trim()
|
||||
const current = selectedLabels.labels ?? []
|
||||
const lowerCasedValue = trimmedValue.toLowerCase()
|
||||
const existing = availableLabels.labels.find(
|
||||
const existing = availableLabels?.find(
|
||||
(l) => l.name.toLowerCase() == lowerCasedValue
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,16 @@ import { Button } from '../../elements/Button'
|
|||
import { StyledText } from '../../elements/StyledText'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { Check, Circle, Plus, WarningCircle } from '@phosphor-icons/react'
|
||||
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
|
||||
import { useRouter } from 'next/router'
|
||||
import { LabelsPicker } from '../../elements/LabelsPicker'
|
||||
import { LabelsDispatcher } from '../../../lib/hooks/useSetPageLabels'
|
||||
import {
|
||||
useCreateLabel,
|
||||
useGetLabels,
|
||||
} from '../../../lib/networking/labels/useLabels'
|
||||
|
||||
export interface LabelsProvider {
|
||||
labels?: Label[]
|
||||
|
|
@ -282,10 +284,10 @@ function Footer(props: FooterProps): JSX.Element {
|
|||
}
|
||||
|
||||
export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
||||
const router = useRouter()
|
||||
const { inputValue, setInputValue, selectedLabels, setHighlightLastLabel } =
|
||||
props
|
||||
const { labels, revalidate } = useGetLabelsQuery()
|
||||
const { data: labels } = useGetLabels()
|
||||
const createLabel = useCreateLabel()
|
||||
// Move focus through the labels list on tab or arrow up/down keys
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(0)
|
||||
|
||||
|
|
@ -321,9 +323,8 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
props.dispatchLabels({ type: 'SAVE', labels: newSelectedLabels })
|
||||
|
||||
props.clearInputState()
|
||||
revalidate()
|
||||
},
|
||||
[isSelected, props, revalidate]
|
||||
[isSelected, props]
|
||||
)
|
||||
|
||||
const filteredLabels = useMemo(() => {
|
||||
|
|
@ -342,11 +343,11 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
const createLabelFromFilterText = useCallback(
|
||||
async (text: string) => {
|
||||
const trimmedLabelName = text.trim()
|
||||
const label = await createLabelMutation(
|
||||
trimmedLabelName,
|
||||
randomLabelColorHex(),
|
||||
''
|
||||
)
|
||||
const label = await createLabel.mutateAsync({
|
||||
name: trimmedLabelName,
|
||||
color: randomLabelColorHex(),
|
||||
description: undefined,
|
||||
})
|
||||
if (label) {
|
||||
showSuccessToast(`Created label ${label.name}`, {
|
||||
position: 'bottom-right',
|
||||
|
|
@ -425,7 +426,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
}, [inputValue, setInputValue, createLabelFromFilterText])
|
||||
|
||||
const selectEnteredLabel = useCallback(() => {
|
||||
const label = labels.find(
|
||||
const label = labels?.find(
|
||||
(l: Label) => l.name.toLowerCase() == inputValue.toLowerCase()
|
||||
)
|
||||
if (!label) {
|
||||
|
|
@ -509,7 +510,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
<Footer
|
||||
filterText={inputValue}
|
||||
selectedLabels={props.selectedLabels}
|
||||
availableLabels={labels}
|
||||
availableLabels={labels ?? []}
|
||||
focused={focusedIndex === filteredLabels.length + 1}
|
||||
createEnteredLabel={createEnteredLabel}
|
||||
selectEnteredLabel={selectEnteredLabel}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,15 @@ import {
|
|||
ModalTitleBar,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { LabelsProvider, SetLabelsControl } from './SetLabelsControl'
|
||||
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
|
||||
import { showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
|
||||
import { LabelsDispatcher } from '../../../lib/hooks/useSetPageLabels'
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import {
|
||||
useCreateLabel,
|
||||
useGetLabels,
|
||||
} from '../../../lib/networking/labels/useLabels'
|
||||
|
||||
type SetLabelsModalProps = {
|
||||
provider: LabelsProvider
|
||||
|
|
@ -28,7 +30,7 @@ type SetLabelsModalProps = {
|
|||
export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const { selectedLabels, dispatchLabels } = props
|
||||
const availableLabels = useGetLabelsQuery()
|
||||
const { data: availableLabels } = useGetLabels()
|
||||
const [tabCount, setTabCount] = useState(-1)
|
||||
const [tabStartValue, setTabStartValue] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
|
|
@ -37,6 +39,8 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
const errorTimeoutRef = useRef<NodeJS.Timeout | undefined>()
|
||||
const [highlightLastLabel, setHighlightLastLabel] = useState(false)
|
||||
|
||||
const createLabel = useCreateLabel()
|
||||
|
||||
const showMessage = useCallback(
|
||||
(msg: string, timeout?: number) => {
|
||||
if (errorTimeoutRef.current) {
|
||||
|
|
@ -82,10 +86,11 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
(newLabels: Label[], tempLabel: Label) => {
|
||||
;(async () => {
|
||||
const currentLabels = newLabels
|
||||
const newLabel = await createLabelMutation(
|
||||
tempLabel.name,
|
||||
tempLabel.color
|
||||
)
|
||||
const newLabel = await createLabel.mutateAsync({
|
||||
name: tempLabel.name,
|
||||
color: tempLabel.color,
|
||||
description: undefined,
|
||||
})
|
||||
const idx = currentLabels.findIndex((l) => l.id === tempLabel.id)
|
||||
if (newLabel) {
|
||||
showSuccessToast(`Created label ${newLabel.name}`, {
|
||||
|
|
@ -116,7 +121,7 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
(value: string) => {
|
||||
const current = selectedLabels ?? []
|
||||
const lowerCasedValue = value.toLowerCase()
|
||||
const existing = availableLabels.labels.find(
|
||||
const existing = availableLabels?.find(
|
||||
(l) => l.name.toLowerCase() == lowerCasedValue
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import dayjs, { Dayjs } from 'dayjs'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { updatePageMutation } from '../../../lib/networking/mutations/updatePageMutation'
|
||||
import {
|
||||
ArticleAttributes,
|
||||
useUpdateItem,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
SubscriptionType,
|
||||
useGetSubscriptionsQuery,
|
||||
} from '../../../lib/networking/queries/useGetSubscriptionsQuery'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { useRegisterActions } from 'kbar'
|
||||
|
|
@ -21,6 +20,7 @@ import Link from 'next/link'
|
|||
import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon'
|
||||
import { NavMenuFooter } from './Footer'
|
||||
import { escapeQuotes } from '../../../utils/helper'
|
||||
import { useGetLabels } from '../../../lib/networking/labels/useLabels'
|
||||
|
||||
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ export function LibraryLegacyMenu(props: LibraryFilterMenuProps): JSX.Element {
|
|||
isSessionStorage: false,
|
||||
initialValue: [],
|
||||
})
|
||||
const labelsResponse = useGetLabelsQuery()
|
||||
const labelsResponse = useGetLabels()
|
||||
const searchesResponse = useGetSavedSearchQuery()
|
||||
const subscriptionsResponse = useGetSubscriptionsQuery()
|
||||
|
||||
|
|
@ -58,9 +58,9 @@ export function LibraryLegacyMenu(props: LibraryFilterMenuProps): JSX.Element {
|
|||
if (
|
||||
!labelsResponse.error &&
|
||||
!labelsResponse.isLoading &&
|
||||
labelsResponse.labels
|
||||
labelsResponse.data
|
||||
) {
|
||||
setLabels(labelsResponse.labels)
|
||||
setLabels(labelsResponse.data)
|
||||
}
|
||||
}, [setLabels, labelsResponse])
|
||||
|
||||
|
|
|
|||
71
packages/web/lib/networking/labels/gql.tsx
Normal file
71
packages/web/lib/networking/labels/gql.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { labelFragment } from '../fragments/labelFragment'
|
||||
|
||||
export const GQL_GET_LABELS = gql`
|
||||
query GetLabels {
|
||||
labels {
|
||||
... on LabelsSuccess {
|
||||
labels {
|
||||
...LabelFields
|
||||
}
|
||||
}
|
||||
... on LabelsError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${labelFragment}
|
||||
`
|
||||
|
||||
export const GQL_CREATE_LABEL = gql`
|
||||
mutation CreateLabel($input: CreateLabelInput!) {
|
||||
createLabel(input: $input) {
|
||||
... on CreateLabelSuccess {
|
||||
label {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
... on CreateLabelError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GQL_DELETE_LABEL = gql`
|
||||
mutation DeleteLabel($id: ID!) {
|
||||
deleteLabel(id: $id) {
|
||||
... on DeleteLabelSuccess {
|
||||
label {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on DeleteLabelError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GQL_UPDATE_LABEL = gql`
|
||||
mutation UpdateLabel($input: UpdateLabelInput!) {
|
||||
updateLabel(input: $input) {
|
||||
... on UpdateLabelSuccess {
|
||||
label {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
... on UpdateLabelError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
160
packages/web/lib/networking/labels/useLabels.tsx
Normal file
160
packages/web/lib/networking/labels/useLabels.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import {
|
||||
GQL_CREATE_LABEL,
|
||||
GQL_DELETE_LABEL,
|
||||
GQL_GET_LABELS,
|
||||
GQL_UPDATE_LABEL,
|
||||
} from './gql'
|
||||
import { Label } from '../fragments/labelFragment'
|
||||
|
||||
export function useGetLabels() {
|
||||
return useQuery({
|
||||
queryKey: ['labels'],
|
||||
queryFn: async () => {
|
||||
const response = (await gqlFetcher(GQL_GET_LABELS)) as LabelsData
|
||||
if (response.labels?.errorCodes?.length) {
|
||||
throw new Error(response.labels.errorCodes[0])
|
||||
}
|
||||
return response.labels?.labels
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateLabel = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const createLabel = async (variables: {
|
||||
name: string
|
||||
color: string
|
||||
description: string | undefined
|
||||
}) => {
|
||||
const result = (await gqlFetcher(GQL_CREATE_LABEL, {
|
||||
input: {
|
||||
name: variables.name,
|
||||
color: variables.color,
|
||||
description: variables.description,
|
||||
},
|
||||
})) as CreateLabelData
|
||||
if (result.createLabel.errorCodes?.length) {
|
||||
throw new Error(result.createLabel.errorCodes[0])
|
||||
}
|
||||
return result.createLabel.label
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: createLabel,
|
||||
onSuccess: (newLabel) => {
|
||||
const keys = queryClient.getQueryCache().findAll({ queryKey: ['labels'] })
|
||||
keys.forEach((query) => {
|
||||
queryClient.setQueryData(query.queryKey, (data: Label[]) => {
|
||||
return [...data, newLabel]
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteLabel = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const deleteLabel = async (variables: { labelId: string }) => {
|
||||
const result = (await gqlFetcher(GQL_DELETE_LABEL, {
|
||||
id: variables.labelId,
|
||||
})) as DeleteLabelData
|
||||
if (result.deleteLabel.errorCodes?.length) {
|
||||
throw new Error(result.deleteLabel.errorCodes[0])
|
||||
}
|
||||
return result.deleteLabel?.label?.id
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: deleteLabel,
|
||||
onSuccess: (deletedId) => {
|
||||
if (deletedId) {
|
||||
const keys = queryClient
|
||||
.getQueryCache()
|
||||
.findAll({ queryKey: ['labels'] })
|
||||
keys.forEach((query) => {
|
||||
queryClient.setQueryData(query.queryKey, (data: Label[]) => {
|
||||
return data.filter((label) => label.id !== deletedId)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateLabel = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const updateLabel = async (variables: {
|
||||
labelId: string
|
||||
name: string
|
||||
color: string
|
||||
description: string
|
||||
}) => {
|
||||
const result = (await gqlFetcher(GQL_UPDATE_LABEL, {
|
||||
input: {
|
||||
labelId: variables.labelId,
|
||||
name: variables.name,
|
||||
color: variables.color,
|
||||
description: variables.description,
|
||||
},
|
||||
})) as UpdateLabelData
|
||||
if (result.updateLabel.errorCodes?.length) {
|
||||
throw new Error(result.updateLabel.errorCodes[0])
|
||||
}
|
||||
return result.updateLabel?.label
|
||||
}
|
||||
return useMutation({
|
||||
mutationFn: updateLabel,
|
||||
onSuccess: (updatedLabel) => {
|
||||
console.log('updated label: ', updatedLabel)
|
||||
if (updatedLabel) {
|
||||
const keys = queryClient
|
||||
.getQueryCache()
|
||||
.findAll({ queryKey: ['labels'] })
|
||||
keys.forEach((query) => {
|
||||
queryClient.setQueryData(query.queryKey, (data: Label[]) => {
|
||||
return [
|
||||
...data.filter((label) => label.id !== updatedLabel.id),
|
||||
updatedLabel,
|
||||
]
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type LabelsResult = {
|
||||
labels?: Label[]
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
type LabelsData = {
|
||||
labels?: LabelsResult
|
||||
}
|
||||
|
||||
type CreateLabelResult = {
|
||||
label?: Label
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
type CreateLabelData = {
|
||||
createLabel: CreateLabelResult
|
||||
}
|
||||
|
||||
type DeleteLabelResult = {
|
||||
label?: Label
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
type DeleteLabelData = {
|
||||
deleteLabel: DeleteLabelResult
|
||||
}
|
||||
|
||||
type UpdateLabelResult = {
|
||||
label?: Label
|
||||
errorCodes?: string[]
|
||||
}
|
||||
|
||||
type UpdateLabelData = {
|
||||
updateLabel: UpdateLabelResult
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { Label } from '../fragments/labelFragment'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type CreateLabelResult = {
|
||||
createLabel: CreateLabel
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
type CreateLabel = {
|
||||
label: Label
|
||||
}
|
||||
|
||||
export async function createLabelMutation(
|
||||
name: string,
|
||||
color: string,
|
||||
description?: string
|
||||
): Promise<any | undefined> {
|
||||
const mutation = gql`
|
||||
mutation CreateLabel($input: CreateLabelInput!) {
|
||||
createLabel(input: $input) {
|
||||
... on CreateLabelSuccess {
|
||||
label {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
... on CreateLabelError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = (await gqlFetcher(mutation, {
|
||||
input: {
|
||||
name,
|
||||
color,
|
||||
description,
|
||||
},
|
||||
})) as CreateLabelResult
|
||||
return data.errorCodes ? undefined : data.createLabel.label
|
||||
} catch (error) {
|
||||
console.log('createLabelMutation error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { Label } from '../fragments/labelFragment'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type DeleteLabelResult = {
|
||||
deleteLabel: DeleteLabel
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
type DeleteLabel = {
|
||||
label: Label
|
||||
}
|
||||
|
||||
export async function deleteLabelMutation(
|
||||
labelId: string
|
||||
): Promise<any | undefined> {
|
||||
const mutation = gql`
|
||||
mutation DeleteLabel($id: ID!) {
|
||||
deleteLabel(id: $id) {
|
||||
... on DeleteLabelSuccess {
|
||||
label {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
... on DeleteLabelError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = (await gqlFetcher(mutation, {
|
||||
id: labelId,
|
||||
})) as DeleteLabelResult
|
||||
return data.errorCodes ? undefined : data.deleteLabel.label.id
|
||||
} catch (error) {
|
||||
console.log('deleteLabelMutation error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
export type UpdateLabelInput = {
|
||||
labelId: string
|
||||
name: string
|
||||
color: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export async function updateLabelMutation(
|
||||
input: UpdateLabelInput
|
||||
): Promise<string | undefined> {
|
||||
const mutation = gql`
|
||||
mutation UpdateLabel($input: UpdateLabelInput!) {
|
||||
updateLabel(input: $input) {
|
||||
... on UpdateLabelSuccess {
|
||||
label {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
... on UpdateLabelError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation, {
|
||||
input,
|
||||
})
|
||||
const output = data as any
|
||||
return output?.updatedLabel
|
||||
} catch (err) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { State } from '../fragments/articleFragment'
|
||||
|
||||
export type UpdatePageInput = {
|
||||
pageId: string
|
||||
title?: string
|
||||
byline?: string | undefined
|
||||
description?: string
|
||||
savedAt?: string
|
||||
publishedAt?: string
|
||||
state?: State
|
||||
}
|
||||
|
||||
export async function updatePageMutation(
|
||||
input: UpdatePageInput
|
||||
): Promise<string | undefined> {
|
||||
const mutation = gql`
|
||||
mutation UpdatePage($input: UpdatePageInput!) {
|
||||
updatePage(input: $input) {
|
||||
... on UpdatePageSuccess {
|
||||
updatedPage {
|
||||
id
|
||||
title
|
||||
url
|
||||
createdAt
|
||||
author
|
||||
image
|
||||
description
|
||||
savedAt
|
||||
publishedAt
|
||||
}
|
||||
}
|
||||
... on UpdatePageError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation, {
|
||||
input,
|
||||
})
|
||||
const output = data as any
|
||||
return output.updatePage
|
||||
} catch (err) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import useSWR from 'swr'
|
||||
import { Label, labelFragment } from '../fragments/labelFragment'
|
||||
import { publicGqlFetcher } from '../networkHelpers'
|
||||
|
||||
type LabelsQueryResponse = {
|
||||
error: any
|
||||
isLoading: boolean
|
||||
isValidating: boolean
|
||||
labels: Label[]
|
||||
revalidate: () => void
|
||||
}
|
||||
|
||||
type LabelsResponseData = {
|
||||
labels?: LabelsData
|
||||
}
|
||||
|
||||
type LabelsData = {
|
||||
labels?: unknown
|
||||
}
|
||||
|
||||
export function useGetLabelsQuery(): LabelsQueryResponse {
|
||||
const query = gql`
|
||||
query GetLabels {
|
||||
labels {
|
||||
... on LabelsSuccess {
|
||||
labels {
|
||||
...LabelFields
|
||||
}
|
||||
}
|
||||
... on LabelsError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${labelFragment}
|
||||
`
|
||||
|
||||
const { data, error, mutate, isValidating } = useSWR(query, publicGqlFetcher)
|
||||
|
||||
try {
|
||||
if (data && !error) {
|
||||
const result = data as LabelsResponseData
|
||||
const labels = result.labels?.labels as Label[]
|
||||
return {
|
||||
error,
|
||||
isLoading: !error && !data,
|
||||
isValidating,
|
||||
labels,
|
||||
revalidate: () => {
|
||||
mutate()
|
||||
},
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
return {
|
||||
error,
|
||||
isLoading: !error && !data,
|
||||
isValidating: false,
|
||||
labels: [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
revalidate: () => {},
|
||||
}
|
||||
}
|
||||
|
|
@ -9,10 +9,6 @@ import {
|
|||
VStack,
|
||||
} from '../../components/elements/LayoutPrimitives'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { createLabelMutation } from '../../lib/networking/mutations/createLabelMutation'
|
||||
import { updateLabelMutation } from '../../lib/networking/mutations/updateLabelMutation'
|
||||
import { deleteLabelMutation } from '../../lib/networking/mutations/deleteLabelMutation'
|
||||
import { applyStoredTheme, isDarkTheme } from '../../lib/themeUpdater'
|
||||
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
|
||||
import { Label, LabelColor } from '../../lib/networking/fragments/labelFragment'
|
||||
|
|
@ -35,6 +31,12 @@ import { ConfirmationModal } from '../../components/patterns/ConfirmationModal'
|
|||
import { InfoLink } from '../../components/elements/InfoLink'
|
||||
import { usePersistedState } from '../../lib/hooks/usePersistedState'
|
||||
import { FeatureHelpBox } from '../../components/elements/FeatureHelpBox'
|
||||
import {
|
||||
useCreateLabel,
|
||||
useDeleteLabel,
|
||||
useGetLabels,
|
||||
useUpdateLabel,
|
||||
} from '../../lib/networking/labels/useLabels'
|
||||
|
||||
const HeaderWrapper = styled(Box, {
|
||||
width: '100%',
|
||||
|
|
@ -143,7 +145,11 @@ const Input = styled('input', { ...inputStyles })
|
|||
const TextArea = styled('textarea', { ...inputStyles })
|
||||
|
||||
export default function LabelsPage(): JSX.Element {
|
||||
const { labels, revalidate } = useGetLabelsQuery()
|
||||
const { data: labels, isLoading } = useGetLabels()
|
||||
const createLabel = useCreateLabel()
|
||||
const deleteLabel = useDeleteLabel()
|
||||
const updateLabel = useUpdateLabel()
|
||||
|
||||
const [labelColorHex, setLabelColorHex] = useState('#000000')
|
||||
const [editingLabelId, setEditingLabelId] = useState<string | null>(null)
|
||||
const [nameInputText, setNameInputText] = useState<string>('')
|
||||
|
|
@ -162,6 +168,9 @@ export default function LabelsPage(): JSX.Element {
|
|||
applyStoredTheme()
|
||||
|
||||
const sortedLabels = useMemo(() => {
|
||||
if (!labels) {
|
||||
return []
|
||||
}
|
||||
return labels.sort((left: Label, right: Label) =>
|
||||
left.name.localeCompare(right.name)
|
||||
)
|
||||
|
|
@ -186,29 +195,35 @@ export default function LabelsPage(): JSX.Element {
|
|||
setLabelColorHex('#000000')
|
||||
}
|
||||
|
||||
async function createLabel(): Promise<void> {
|
||||
const res = await createLabelMutation(
|
||||
nameInputText.trim(),
|
||||
labelColorHex,
|
||||
descriptionInputText
|
||||
)
|
||||
async function doCreateLabel(): Promise<void> {
|
||||
const res = await createLabel.mutateAsync({
|
||||
name: nameInputText.trim(),
|
||||
color: labelColorHex,
|
||||
description: descriptionInputText,
|
||||
})
|
||||
if (res) {
|
||||
showSuccessToast('Label created', { position: 'bottom-right' })
|
||||
resetLabelState()
|
||||
revalidate()
|
||||
} else {
|
||||
showErrorToast('Failed to create label')
|
||||
}
|
||||
}
|
||||
|
||||
async function updateLabel(id: string): Promise<void> {
|
||||
await updateLabelMutation({
|
||||
labelId: id,
|
||||
name: nameInputText,
|
||||
color: labelColorHex,
|
||||
description: descriptionInputText,
|
||||
})
|
||||
revalidate()
|
||||
async function doUpdateLabel(id: string): Promise<void> {
|
||||
try {
|
||||
await updateLabel.mutateAsync({
|
||||
labelId: id,
|
||||
name: nameInputText,
|
||||
color: labelColorHex,
|
||||
description: descriptionInputText,
|
||||
})
|
||||
} catch (err) {
|
||||
console.log('error updating label: ', err)
|
||||
showErrorToast('Failed to update label')
|
||||
return
|
||||
}
|
||||
showSuccessToast('Label updated', { position: 'bottom-right' })
|
||||
resetLabelState()
|
||||
}
|
||||
|
||||
const onEditPress = (label: Label | null) => {
|
||||
|
|
@ -222,17 +237,16 @@ export default function LabelsPage(): JSX.Element {
|
|||
}
|
||||
}
|
||||
|
||||
async function onDeleteLabel(id: string): Promise<void> {
|
||||
const result = await deleteLabelMutation(id)
|
||||
async function onDeleteLabel(labelId: string): Promise<void> {
|
||||
const result = await deleteLabel.mutateAsync({ labelId })
|
||||
if (result) {
|
||||
showSuccessToast('Label deleted', { position: 'bottom-right' })
|
||||
} else {
|
||||
showErrorToast('Failed to delete label', { position: 'bottom-right' })
|
||||
}
|
||||
revalidate()
|
||||
}
|
||||
|
||||
async function deleteLabel(id: string): Promise<void> {
|
||||
async function doDeleteLabel(id: string): Promise<void> {
|
||||
setConfirmRemoveLabelId(id)
|
||||
}
|
||||
|
||||
|
|
@ -349,14 +363,14 @@ export default function LabelsPage(): JSX.Element {
|
|||
handleGenerateRandomColor={handleGenerateRandomColor}
|
||||
setEditingLabelId={setEditingLabelId}
|
||||
setLabelColorHex={setLabelColorHex}
|
||||
deleteLabel={deleteLabel}
|
||||
deleteLabel={doDeleteLabel}
|
||||
nameInputText={nameInputText}
|
||||
descriptionInputText={descriptionInputText}
|
||||
setNameInputText={setNameInputText}
|
||||
setDescriptionInputText={setDescriptionInputText}
|
||||
setIsCreateMode={setIsCreateMode}
|
||||
createLabel={createLabel}
|
||||
updateLabel={updateLabel}
|
||||
createLabel={doCreateLabel}
|
||||
updateLabel={doUpdateLabel}
|
||||
onEditPress={onEditPress}
|
||||
resetState={resetLabelState}
|
||||
/>
|
||||
|
|
@ -369,15 +383,15 @@ export default function LabelsPage(): JSX.Element {
|
|||
handleGenerateRandomColor={handleGenerateRandomColor}
|
||||
setEditingLabelId={setEditingLabelId}
|
||||
setLabelColorHex={setLabelColorHex}
|
||||
deleteLabel={deleteLabel}
|
||||
deleteLabel={doDeleteLabel}
|
||||
nameInputText={nameInputText}
|
||||
descriptionInputText={descriptionInputText}
|
||||
setNameInputText={setNameInputText}
|
||||
setDescriptionInputText={setDescriptionInputText}
|
||||
setIsCreateMode={setIsCreateMode}
|
||||
createLabel={createLabel}
|
||||
createLabel={doCreateLabel}
|
||||
resetState={resetLabelState}
|
||||
updateLabel={updateLabel}
|
||||
updateLabel={doUpdateLabel}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
|
|
@ -396,15 +410,15 @@ export default function LabelsPage(): JSX.Element {
|
|||
handleGenerateRandomColor: handleGenerateRandomColor,
|
||||
setEditingLabelId: setEditingLabelId,
|
||||
setLabelColorHex: setLabelColorHex,
|
||||
deleteLabel: deleteLabel,
|
||||
deleteLabel: doDeleteLabel,
|
||||
nameInputText: nameInputText,
|
||||
descriptionInputText: descriptionInputText,
|
||||
setNameInputText: setNameInputText,
|
||||
setDescriptionInputText: setDescriptionInputText,
|
||||
setIsCreateMode: setIsCreateMode,
|
||||
createLabel: createLabel,
|
||||
createLabel: doCreateLabel,
|
||||
resetState: resetLabelState,
|
||||
updateLabel: updateLabel,
|
||||
updateLabel: doUpdateLabel,
|
||||
}
|
||||
|
||||
if (editingLabelId == label.id) {
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ import {
|
|||
import { StyledText } from '../../components/elements/StyledText'
|
||||
import { SettingsLayout } from '../../components/templates/SettingsLayout'
|
||||
import { applyStoredTheme } from '../../lib/themeUpdater'
|
||||
import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { useGetSavedSearchQuery } from '../../lib/networking/queries/useGetSavedSearchQuery'
|
||||
import { Label } from '../../lib/networking/fragments/labelFragment'
|
||||
import { CheckSquare, Circle, Square } from '@phosphor-icons/react'
|
||||
import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
|
||||
import { usePersistedState } from '../../lib/hooks/usePersistedState'
|
||||
import { escapeQuotes } from '../../utils/helper'
|
||||
import { useGetLabels } from '../../lib/networking/labels/useLabels'
|
||||
|
||||
export type PinnedSearch = {
|
||||
type: 'saved-search' | 'label'
|
||||
|
|
@ -34,7 +34,7 @@ const PINNED_SEARCHES_KEY = `--library-pinned-searches`
|
|||
type ListAction = 'RESET' | 'ADD_ITEM' | 'REMOVE_ITEM'
|
||||
|
||||
export default function PinnedSearches(): JSX.Element {
|
||||
const { labels } = useGetLabelsQuery()
|
||||
const { data: labels } = useGetLabels()
|
||||
const { savedSearches } = useGetSavedSearchQuery()
|
||||
const [hidePinnedSearches, setHidePinnedSearches] = usePersistedState({
|
||||
key: '--library-hide-pinned-searches',
|
||||
|
|
@ -103,7 +103,7 @@ export default function PinnedSearches(): JSX.Element {
|
|||
if (pinnedSearches.state == 'INITIAL') {
|
||||
return { labelItems: [], savedSearchItems: [] }
|
||||
}
|
||||
const labelItems = labels.map((label) => {
|
||||
const labelItems = labels?.map((label) => {
|
||||
return {
|
||||
label,
|
||||
isSelected: !!pinnedSearches.items.find(
|
||||
|
|
@ -244,7 +244,7 @@ export default function PinnedSearches(): JSX.Element {
|
|||
<StyledText style="modalTitle" css={{ mt: '20px' }}>
|
||||
Labels
|
||||
</StyledText>
|
||||
{items.labelItems.map((item) => {
|
||||
{items.labelItems?.map((item) => {
|
||||
return (
|
||||
<LabelButton
|
||||
label={item.label}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,16 @@ import { Label } from '../../lib/networking/fragments/labelFragment'
|
|||
import { deleteRuleMutation } from '../../lib/networking/mutations/deleteRuleMutation'
|
||||
import { setRuleMutation } from '../../lib/networking/mutations/setRuleMutation'
|
||||
import { useGetIntegrationsQuery } from '../../lib/networking/queries/useGetIntegrationsQuery'
|
||||
import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
|
||||
import {
|
||||
Rule,
|
||||
RuleAction,
|
||||
RuleActionType,
|
||||
RuleEventType,
|
||||
useGetRulesQuery
|
||||
useGetRulesQuery,
|
||||
} from '../../lib/networking/queries/useGetRulesQuery'
|
||||
import { applyStoredTheme } from '../../lib/themeUpdater'
|
||||
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
|
||||
import { useGetLabels } from '../../lib/networking/labels/useLabels'
|
||||
|
||||
type CreateRuleModalProps = {
|
||||
isModalOpen: boolean
|
||||
|
|
@ -131,7 +131,7 @@ type CreateActionModalProps = {
|
|||
|
||||
const CreateActionModal = (props: CreateActionModalProps): JSX.Element => {
|
||||
const [form] = Form.useForm()
|
||||
const { labels } = useGetLabelsQuery()
|
||||
const { data: labels } = useGetLabels()
|
||||
const { integrations } = useGetIntegrationsQuery()
|
||||
|
||||
const integrationOptions = ['NOTION', 'READWISE']
|
||||
|
|
@ -232,7 +232,7 @@ const CreateActionModal = (props: CreateActionModalProps): JSX.Element => {
|
|||
]}
|
||||
>
|
||||
<Select mode="multiple">
|
||||
{labels.map((label) => {
|
||||
{labels?.map((label) => {
|
||||
return (
|
||||
<Select.Option key={label.id} value={label.id}>
|
||||
{label.name}
|
||||
|
|
@ -302,7 +302,7 @@ const CreateActionModal = (props: CreateActionModalProps): JSX.Element => {
|
|||
|
||||
export default function Rules(): JSX.Element {
|
||||
const { rules, revalidate } = useGetRulesQuery()
|
||||
const { labels } = useGetLabelsQuery()
|
||||
const { data: labels } = useGetLabels()
|
||||
const [isCreateRuleModalOpen, setIsCreateRuleModalOpen] = useState(false)
|
||||
const [createActionRule, setCreateActionRule] = useState<Rule | undefined>(
|
||||
undefined
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
} from 'react'
|
||||
import { applyStoredTheme } from '../../lib/themeUpdater'
|
||||
|
||||
import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { useGetSavedSearchQuery } from '../../lib/networking/queries/useGetSavedSearchQuery'
|
||||
import { SettingsLayout } from '../../components/templates/SettingsLayout'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
|
|
@ -36,6 +35,7 @@ import { styled } from '@stitches/react'
|
|||
import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
|
||||
import { escapeQuotes } from '../../utils/helper'
|
||||
import { Shortcut } from '../../components/templates/navMenu/NavigationMenu'
|
||||
import { useGetLabels } from '../../lib/networking/labels/useLabels'
|
||||
type ListAction = 'RESET' | 'ADD_ITEM' | 'REMOVE_ITEM'
|
||||
|
||||
const SHORTCUTS_KEY = 'library-shortcuts'
|
||||
|
|
@ -232,7 +232,7 @@ type ListProps = {
|
|||
}
|
||||
|
||||
const AvailableItems = (props: ListProps): JSX.Element => {
|
||||
const { labels } = useGetLabelsQuery()
|
||||
const { data: labels } = useGetLabels()
|
||||
const { savedSearches } = useGetSavedSearchQuery()
|
||||
const { subscriptions } = useGetSubscriptionsQuery()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue