From e540c57da43c3c1d1ae91868722a60bafd9874ab Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 21 Jun 2023 19:59:32 +0800 Subject: [PATCH] Throttle set label calls --- .../web/components/elements/EditLabelChip.tsx | 3 +- .../web/components/elements/LabelsPicker.tsx | 76 +++++++++++++------ .../templates/homeFeed/HomeFeedContainer.tsx | 5 -- packages/web/lib/hooks/useSetPageLabels.tsx | 43 ++++++----- .../networking/mutations/setLabelsMutation.ts | 4 +- 5 files changed, 82 insertions(+), 49 deletions(-) diff --git a/packages/web/components/elements/EditLabelChip.tsx b/packages/web/components/elements/EditLabelChip.tsx index 1ec3a1062..586758031 100644 --- a/packages/web/components/elements/EditLabelChip.tsx +++ b/packages/web/components/elements/EditLabelChip.tsx @@ -2,7 +2,6 @@ import { Button } from './Button' import { SpanBox, HStack } from './LayoutPrimitives' import { Circle, X } from 'phosphor-react' import { isDarkTheme } from '../../lib/themeUpdater' -import { theme } from '../tokens/stitches.config' type EditLabelChipProps = { text: string @@ -11,7 +10,7 @@ type EditLabelChipProps = { xAction: () => void } -export function EditLabelLabelChip(props: EditLabelChipProps): JSX.Element { +export function EditLabelChip(props: EditLabelChipProps): JSX.Element { const isDark = isDarkTheme() const selectedBorder = isDark ? '#FFEA9F' : '#D9D9D9' diff --git a/packages/web/components/elements/LabelsPicker.tsx b/packages/web/components/elements/LabelsPicker.tsx index 61aac142d..f27362bef 100644 --- a/packages/web/components/elements/LabelsPicker.tsx +++ b/packages/web/components/elements/LabelsPicker.tsx @@ -1,11 +1,14 @@ import AutosizeInput from 'react-input-autosize' import { Box, SpanBox } from './LayoutPrimitives' -import { useCallback, useEffect, useMemo, useRef } from 'react' +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 { EditLabelLabelChip } from './EditLabelChip' +import { EditLabelChip } from './EditLabelChip' import { LabelsDispatcher } from '../../lib/hooks/useSetPageLabels' +import { EditLabelChipStack } from './EditLabelChipStack' + +const MaxUnstackedLabels = 4 type LabelsPickerProps = { selectedLabels: Label[] @@ -33,6 +36,7 @@ type LabelsPickerProps = { export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => { const inputRef = useRef() const availableLabels = useGetLabelsQuery() + const [isStackExpanded, setIsStackExpanded] = useState(false) const { focused, inputValue, @@ -100,6 +104,10 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => { return selectedLabels.length === 0 && inputValue.length === 0 }, [inputValue, selectedLabels]) + const isStacked = useMemo(() => { + return selectedLabels.length > MaxUnstackedLabels && !isStackExpanded + }, [selectedLabels.length, isStackExpanded]) + return ( { event.preventDefault() }} > - {props.selectedLabels.map((label, idx) => ( - { - const idx = props.selectedLabels.findIndex((l) => l.id == label.id) - if (idx !== -1) { - const _selectedLabels = props.selectedLabels - _selectedLabels.splice(idx, 1) - props.dispatchLabels({ - type: 'SAVE', - labels: [..._selectedLabels], - }) - } + {isStacked ? ( + { + setIsStackExpanded(true) }} + isSelected={props.highlightLastLabel} /> - ))} + ) : ( + props.selectedLabels.map((label, idx) => ( + { + const idx = props.selectedLabels.findIndex( + (l) => l.id == label.id + ) + if (idx !== -1) { + const _selectedLabels = props.selectedLabels + _selectedLabels.splice(idx, 1) + props.dispatchLabels({ + type: 'SAVE', + labels: [..._selectedLabels], + }) + } + }} + /> + )) + )} { props.clearInputState() break case 'Enter': - props.selectOrCreateLabel(props.inputValue) + if (isStacked && props.highlightLastLabel) { + setIsStackExpanded(true) + props.setHighlightLastLabel(false) + } else { + props.selectOrCreateLabel(props.inputValue) + } event.preventDefault() break } @@ -223,7 +248,12 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => { case 'Backspace': clearTabState() if (props.inputValue.length === 0) { - props.deleteLastLabel() + if (isStacked && props.highlightLastLabel) { + setIsStackExpanded(true) + props.setHighlightLastLabel(false) + } else { + props.deleteLastLabel() + } event.preventDefault() } break diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 2d1f1f3e9..831510bc9 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -52,11 +52,6 @@ import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { SetPageLabelsModalPresenter } from '../article/SetLabelsModalPresenter' import { NotebookPresenter } from '../article/NotebookPresenter' import { Highlight } from '../../../lib/networking/fragments/highlightFragment' -import { Label } from '../../../lib/networking/fragments/labelFragment' -import { - LabelAction, - LabelsDispatcher, -} from '../../../lib/hooks/useSetPageLabels' export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT' export type LibraryMode = 'reads' | 'highlights' diff --git a/packages/web/lib/hooks/useSetPageLabels.tsx b/packages/web/lib/hooks/useSetPageLabels.tsx index 25550bb10..339fe7a06 100644 --- a/packages/web/lib/hooks/useSetPageLabels.tsx +++ b/packages/web/lib/hooks/useSetPageLabels.tsx @@ -1,8 +1,8 @@ -import { useEffect, useReducer } from 'react' +import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react' import { setLabelsMutation } from '../networking/mutations/setLabelsMutation' import { Label } from '../networking/fragments/labelFragment' import { showErrorToast } from '../toastHelpers' -import { LabelsProvider } from '../../components/templates/article/SetLabelsControl' +import throttle from 'lodash/throttle' export type LabelAction = 'RESET' | 'TEMP' | 'SAVE' export type LabelsDispatcher = (action: { @@ -13,9 +13,24 @@ export type LabelsDispatcher = (action: { export const useSetPageLabels = ( articleId?: string ): [{ labels: Label[] }, LabelsDispatcher] => { + const saveLabels = (labels: Label[]) => { + ;(async () => { + const labelIds = labels.map((l) => l.id) + if (articleId) { + const result = await setLabelsMutation(articleId, labelIds) + if (!result) { + showErrorToast('Error saving labels', { + position: 'bottom-right', + }) + } + } + })() + } + const labelsReducer = ( state: { labels: Label[] + throttledSave: (labels: Label[]) => void }, action: { type: string @@ -25,37 +40,26 @@ export const useSetPageLabels = ( switch (action.type) { case 'RESET': { return { + ...state, labels: action.labels, } } case 'TEMP': { return { + ...state, labels: action.labels, } } case 'SAVE': { - const labelIds = action.labels.map((l) => l.id) if (articleId) { - ;(async () => { - const result = await setLabelsMutation(articleId, labelIds) - if (result) { - dispatchLabels({ - type: 'RESET', - // Use the original labels value here so we dont re-order - labels: action.labels ?? [], - }) - } else { - showErrorToast('Error saving labels', { - position: 'bottom-right', - }) - } - })() + state.throttledSave(action.labels) } else { showErrorToast('Unable to update labels', { position: 'bottom-right', }) } return { + ...state, labels: action.labels, } } @@ -64,8 +68,13 @@ export const useSetPageLabels = ( } } + const debouncedSave = useCallback( + throttle((labels: Label[]) => saveLabels(labels), 2000), + [] + ) const [labels, dispatchLabels] = useReducer(labelsReducer, { labels: [], + throttledSave: debouncedSave, }) return [labels, dispatchLabels] diff --git a/packages/web/lib/networking/mutations/setLabelsMutation.ts b/packages/web/lib/networking/mutations/setLabelsMutation.ts index 46d842e64..9889c130a 100644 --- a/packages/web/lib/networking/mutations/setLabelsMutation.ts +++ b/packages/web/lib/networking/mutations/setLabelsMutation.ts @@ -4,11 +4,11 @@ import { gqlFetcher } from '../networkHelpers' type SetLabelsResult = { setLabels: SetLabels - errorCodes?: unknown[] } type SetLabels = { labels: Label[] + errorCodes?: unknown[] } export async function setLabelsMutation( @@ -35,7 +35,7 @@ export async function setLabelsMutation( const data = (await gqlFetcher(mutation, { input: { pageId, labelIds }, })) as SetLabelsResult - return data.errorCodes ? undefined : data.setLabels.labels + return data.setLabels.errorCodes ? undefined : data.setLabels.labels } catch (error) { console.log(' -- SetLabelsOutput error', error) return undefined