From a730a4cb85dc9cea6850a061edbfcad95afd1986 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 19 Jun 2023 18:58:39 +0800 Subject: [PATCH] Push props down so we can manipulate at the top level --- .../web/components/elements/LabelChip.tsx | 29 +- .../web/components/elements/LabelsPicker.tsx | 404 ++---------------- .../templates/article/SetLabelsControl.tsx | 83 +++- .../templates/article/SetLabelsModal.tsx | 118 ++++- 4 files changed, 251 insertions(+), 383 deletions(-) diff --git a/packages/web/components/elements/LabelChip.tsx b/packages/web/components/elements/LabelChip.tsx index e32ac2e37..29c29e0cb 100644 --- a/packages/web/components/elements/LabelChip.tsx +++ b/packages/web/components/elements/LabelChip.tsx @@ -2,14 +2,16 @@ import { getLuminance, lighten, parseToRgba, toHsla } from 'color2k' import { useRouter } from 'next/router' import { Button } from './Button' import { SpanBox, HStack } from './LayoutPrimitives' -import { Circle } from 'phosphor-react' +import { Circle, X } from 'phosphor-react' import { isDarkTheme } from '../../lib/themeUpdater' +import { theme } from '../tokens/stitches.config' type LabelChipProps = { text: string color: string // expected to be a RGB hex color string isSelected?: boolean useAppAppearance?: boolean + xAction?: () => void } export function LabelChip(props: LabelChipProps): JSX.Element { @@ -18,7 +20,7 @@ export function LabelChip(props: LabelChipProps): JSX.Element { const luminance = getLuminance(props.color) const textColor = luminance > 0.5 ? '#000000' : '#ffffff' - const selectedBorder = isDark ? 'white' : 'black' + const selectedBorder = isDark ? '#FFEA9F' : 'black' const unSelectedBorder = isDark ? '#6A6968' : '#D9D9D9' if (props.useAppAppearance) { @@ -42,9 +44,30 @@ export function LabelChip(props: LabelChipProps): JSX.Element { backgroundColor: isDark ? '#2A2A2A' : '#F5F5F5', }} > - + {props.text} + {props.xAction && ( + + )} ) diff --git a/packages/web/components/elements/LabelsPicker.tsx b/packages/web/components/elements/LabelsPicker.tsx index 97440c6ff..3acd2e47d 100644 --- a/packages/web/components/elements/LabelsPicker.tsx +++ b/packages/web/components/elements/LabelsPicker.tsx @@ -4,29 +4,34 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Label } from '../../lib/networking/fragments/labelFragment' import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery' import { LabelChip } from './LabelChip' -import { randomLabelColorHex } from '../../utils/settings-page/labels/labelColorObjects' -import { v4 as uuidv4 } from 'uuid' -import { createLabelMutation } from '../../lib/networking/mutations/createLabelMutation' -import { showSuccessToast } from '../../lib/toastHelpers' import { isTouchScreenDevice } from '../../lib/deviceType' type LabelsPickerProps = { selectedLabels: Label[] focused: boolean - onFocus?: () => void - onFilterTextChange?: (filterText: string) => void + inputValue: string + setInputValue: (value: string) => void + clearInputState: () => void + onFocus?: () => void setSelectedLabels: (labels: Label[]) => void + + deleteLastLabel: () => void + selectOrCreateLabel: (value: string) => void + + tabCount: number + setTabCount: (count: number) => void + tabStartValue: string + setTabStartValue: (value: string) => void + + highlightLastLabel: boolean + setHighlightLastLabel: (set: boolean) => void } export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => { const inputRef = useRef() const availableLabels = useGetLabelsQuery() - const [inputValue, setInputValue] = useState('') - const [tabCount, setTabCount] = useState(-1) - const [tabStartValue, setTabStartValue] = useState('') - const [highlightLastLabel, setHighlightLastLabel] = useState(false) useEffect(() => { if (!isTouchScreenDevice() && props.focused && inputRef.current) { @@ -34,115 +39,25 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => { } }, [props.focused]) - useEffect(() => { - if (props.onFilterTextChange) { - props.onFilterTextChange(inputValue) - } - }, [inputValue]) - - const showMessage = useCallback((msg: string) => { - console.log('showMessage: ', msg) - }, []) - - const clearInputState = useCallback(() => { - setTabCount(-1) - setInputValue('') - setTabStartValue('') - setHighlightLastLabel(false) - }, [tabCount, inputValue, tabStartValue]) - - const createLabelAsync = useCallback( - (tempLabel: Label) => { - ;(async () => { - const currentLabels = props.selectedLabels - const newLabel = await createLabelMutation( - tempLabel.name, - tempLabel.color - ) - if (newLabel) { - const idx = currentLabels.findIndex((l) => l.id === tempLabel.id) - showSuccessToast(`Created label ${newLabel.name}`, { - position: 'bottom-right', - }) - if (idx !== -1) { - currentLabels[idx] = newLabel - props.setSelectedLabels([...currentLabels]) - } else { - props.setSelectedLabels([...currentLabels, newLabel]) - } - } else { - showMessage(`Error creating label ${tempLabel.name}`) - } - })() - }, - [props.selectedLabels] - ) - - const selectOrCreateLabel = useCallback( - (value: string) => { - const current = props.selectedLabels ?? [] - const lowerCasedValue = value.toLowerCase() - const existing = availableLabels.labels.find( - (l) => l.name.toLowerCase() == lowerCasedValue - ) - - if (lowerCasedValue.length < 1) { - return - } - - if (existing) { - const isAdded = props.selectedLabels.find( - (l) => l.name.toLowerCase() == lowerCasedValue - ) - if (!isAdded) { - props.setSelectedLabels([...current, existing]) - clearInputState() - } else { - showMessage(`label ${value} already added.`) - } - } else { - const tempLabel = { - id: uuidv4(), - name: value, - color: randomLabelColorHex(), - description: '', - createdAt: new Date(), - _temporary: true, - } - props.setSelectedLabels([...current, tempLabel]) - clearInputState() - - createLabelAsync(tempLabel) - } - }, - [ - availableLabels, - props.selectedLabels, - clearInputState, - createLabelAsync, - showMessage, - ] - ) - const autoComplete = useCallback(() => { - const lowerCasedValue = inputValue.toLowerCase() + const lowerCasedValue = props.inputValue.toLowerCase() if (lowerCasedValue.length < 1) { return } - let _tabCount = tabCount - let _tabStartValue = tabStartValue.toLowerCase() + let _tabCount = props.tabCount + let _tabStartValue = props.tabStartValue.toLowerCase() if (_tabCount === -1) { _tabCount = 0 _tabStartValue = lowerCasedValue - setTabCount(0) - setTabStartValue(lowerCasedValue) + props.setTabCount(0) + props.setTabStartValue(lowerCasedValue) } else { - _tabCount = tabCount + 1 - setTabCount(_tabCount) + _tabCount = props.tabCount + 1 + props.setTabCount(_tabCount) } const matches = availableLabels.labels.filter((l) => @@ -150,32 +65,21 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => { ) if (_tabCount < matches.length) { - setInputValue(matches[_tabCount].name) + props.setInputValue(matches[_tabCount].name) } else if (matches.length > 0) { - setTabCount(0) - setInputValue(matches[0].name) + props.setTabCount(0) + props.setInputValue(matches[0].name) } - }, [inputValue, availableLabels, tabCount, tabStartValue]) - - const deleteLastLabel = useCallback(() => { - if (highlightLastLabel) { - const current = props.selectedLabels - current.pop() - props.setSelectedLabels([...current]) - setHighlightLastLabel(false) - } else { - setHighlightLastLabel(true) - } - }, [highlightLastLabel, props.selectedLabels]) + }, [props.inputValue, availableLabels, props.tabCount, props.tabStartValue]) const clearTabState = useCallback(() => { - setTabCount(-1) - setTabStartValue('') + props.setTabCount(-1) + props.setTabStartValue('') }, []) const isEmpty = useMemo(() => { - return props.selectedLabels.length === 0 && inputValue.length === 0 - }, [inputValue, props.selectedLabels]) + return props.selectedLabels.length === 0 && props.inputValue.length === 0 + }, [props.inputValue, props.selectedLabels]) return ( { '>span': { marginTop: '0px', marginBottom: '0px', - borderColor: 'transparent', }, }} onMouseDown={(event) => { @@ -228,8 +131,16 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => { text={label.name} color={label.color} isSelected={ - highlightLastLabel && idx == props.selectedLabels.length - 1 + props.highlightLastLabel && idx == props.selectedLabels.length - 1 } + xAction={() => { + const idx = props.selectedLabels.findIndex((l) => l.id == label.id) + if (idx !== -1) { + const _selectedLabels = props.selectedLabels + _selectedLabels.splice(idx, 1) + props.setSelectedLabels([..._selectedLabels]) + } + }} useAppAppearance={true} /> ))} @@ -253,17 +164,17 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => { }} minWidth="2px" maxLength={48} - value={inputValue} + value={props.inputValue} onClick={(event) => { event.stopPropagation() }} onKeyUp={(event) => { switch (event.key) { case 'Escape': - clearInputState() + props.clearInputState() break case 'Enter': - selectOrCreateLabel(inputValue) + props.selectOrCreateLabel(props.inputValue) event.preventDefault() break } @@ -277,243 +188,18 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => { case 'Delete': case 'Backspace': clearTabState() - if (inputValue.length === 0) { - deleteLastLabel() + if (props.inputValue.length === 0) { + props.deleteLastLabel() event.preventDefault() } break } }} onChange={function (event) { - setInputValue(event.target.value) + props.setInputValue(event.target.value) }} /> ) } - -// import { styled } from '@stitches/react' -// import { Label } from '../../lib/networking/fragments/labelFragment' -// import { LabelChip } from './LabelChip' -// import { Box } from './LayoutPrimitives' -// import { useCallback, useMemo, useState } from 'react' -// import { useCombobox, useMultipleSelection } from 'downshift' - -// type LabelsPickerProps = { -// // selectedLabels: Label[] -// } - -// const InputLabel = styled('input', { -// outline: 'none', -// boxSizing: 'content-box', -// maxWidth: '237px', -// background: 'red', -// borderStyle: 'none', -// width: '100%', -// }) - -// const StyledInput = styled('input', { -// outline: 'none', -// boxSizing: 'content-box', -// maxWidth: '237px', -// background: 'red', -// borderStyle: 'none', -// width: '100%', -// }) - -// type Suggestion = { -// name: string -// year: number -// } - -// const languages = [ -// { -// name: 'C', -// year: 1972, -// }, -// { -// name: 'Elm', -// year: 2012, -// }, -// ] - -// export function LabelsPicker(props: LabelsPickerProps): JSX.Element { -// const labels: Label[] = [ -// { id: '123', name: 'Label 01', color: '#000000', createdAt: new Date(0) }, -// { id: '124', name: 'Label 02', color: '#000000', createdAt: new Date(0) }, -// { id: '125', name: 'Label 03', color: '#000000', createdAt: new Date(0) }, -// ] -// const initialSelectedItems: Label[] = [] - -// function getFilteredBooks(selectedItems: Label[], inputValue: string) { -// const lowerCasedInputValue = inputValue.toLowerCase() - -// return labels.filter(function filterBook(book) { -// return ( -// !selectedItems.includes(book) && -// book.name.toLowerCase().includes(lowerCasedInputValue) -// ) -// }) -// } - -// function MultipleComboBox() { -// const [inputValue, setInputValue] = useState('') -// const [selectedItems, setSelectedItems] = useState(initialSelectedItems) -// const items = useMemo( -// () => getFilteredBooks(selectedItems, inputValue), -// [selectedItems, inputValue] -// ) -// const { getSelectedItemProps, getDropdownProps, removeSelectedItem } = -// useMultipleSelection({ -// selectedItems, -// onStateChange({ selectedItems: newSelectedItems, type }) { -// switch (type) { -// case useMultipleSelection.stateChangeTypes -// .SelectedItemKeyDownBackspace: -// case useMultipleSelection.stateChangeTypes -// .SelectedItemKeyDownDelete: -// case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace: -// case useMultipleSelection.stateChangeTypes -// .FunctionRemoveSelectedItem: -// setSelectedItems(newSelectedItems as Label[]) -// break -// default: -// break -// } -// }, -// }) -// const { -// isOpen, -// getToggleButtonProps, -// getLabelProps, -// getMenuProps, -// getInputProps, -// highlightedIndex, -// getItemProps, -// selectedItem, -// } = useCombobox({ -// items, -// itemToString(item: Label | null) { -// return item ? item.name : '' -// }, -// defaultHighlightedIndex: 0, // after selection, highlight the first item. -// selectedItem: null, -// stateReducer(state, actionAndChanges) { -// const { changes, type } = actionAndChanges - -// switch (type) { -// case useCombobox.stateChangeTypes.InputKeyDownEnter: -// case useCombobox.stateChangeTypes.ItemClick: -// return { -// ...changes, -// isOpen: true, // keep the menu open after selection. -// highlightedIndex: 0, // with the first option highlighted. -// } -// default: -// return changes -// } -// }, -// onStateChange({ -// inputValue: newInputValue, -// type, -// selectedItem: newSelectedItem, -// }) { -// switch (type) { -// case useCombobox.stateChangeTypes.InputKeyDownEnter: -// case useCombobox.stateChangeTypes.ItemClick: -// case useCombobox.stateChangeTypes.InputBlur: -// if (newSelectedItem) { -// setSelectedItems([...selectedItems, newSelectedItem]) -// } -// break - -// case useCombobox.stateChangeTypes.InputChange: -// setInputValue(newInputValue || '') - -// break -// default: -// break -// } -// }, -// }) - -// return ( -//
-//
-// -//
-// {selectedItems.map(function renderSelectedItem( -// selectedItemForRender, -// index -// ) { -// return ( -// -// {selectedItemForRender.name} -// { -// e.stopPropagation() -// removeSelectedItem(selectedItemForRender) -// }} -// > -// ✕ -// -// -// ) -// })} -//
-// -// -//
-//
-//
-//
    -// {isOpen && -// items.map((item, index) => ( -//
  • -// {item.name} -// {item.name} -//
  • -// ))} -//
-//
-// ) -// } -// return -// } diff --git a/packages/web/components/templates/article/SetLabelsControl.tsx b/packages/web/components/templates/article/SetLabelsControl.tsx index a24af4a49..6272c4480 100644 --- a/packages/web/components/templates/article/SetLabelsControl.tsx +++ b/packages/web/components/templates/article/SetLabelsControl.tsx @@ -3,12 +3,10 @@ import Link from 'next/link' import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { Button } from '../../elements/Button' import { StyledText } from '../../elements/StyledText' -import { CrossIcon } from '../../elements/images/CrossIcon' import { styled, theme } from '../../tokens/stitches.config' import { Label } from '../../../lib/networking/fragments/labelFragment' import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery' import { Check, Circle, PencilSimple, Plus } from 'phosphor-react' -import { isTouchScreenDevice } from '../../../lib/deviceType' import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects' @@ -22,20 +20,48 @@ export interface LabelsProvider { type SetLabelsControlProps = { provider: LabelsProvider + inputValue: string + setInputValue: (value: string) => void + clearInputState: () => void + selectedLabels: Label[] setSelectedLabels: (labels: Label[]) => void onLabelsUpdated?: (labels: Label[]) => void + + tabCount: number + setTabCount: (count: number) => void + tabStartValue: string + setTabStartValue: (value: string) => void + + highlightLastLabel: boolean + setHighlightLastLabel: (set: boolean) => void + + deleteLastLabel: () => void + selectOrCreateLabel: (value: string) => void } type HeaderProps = { - filterText: string focused: boolean resetFocusedIndex: () => void - setFilterText: (text: string) => void + + inputValue: string + setInputValue: (value: string) => void + clearInputState: () => void selectedLabels: Label[] setSelectedLabels: (labels: Label[]) => void + + tabCount: number + setTabCount: (count: number) => void + tabStartValue: string + setTabStartValue: (value: string) => void + + highlightLastLabel: boolean + setHighlightLastLabel: (set: boolean) => void + + deleteLastLabel: () => void + selectOrCreateLabel: (value: string) => void } const StyledLabel = styled('label', { @@ -55,14 +81,22 @@ function Header(props: HeaderProps): JSX.Element { > { - props.setFilterText(filterText) - }} + tabCount={props.tabCount} + setTabCount={props.setTabCount} + tabStartValue={props.tabStartValue} + setTabStartValue={props.setTabStartValue} + highlightLastLabel={props.highlightLastLabel} + setHighlightLastLabel={props.setHighlightLastLabel} onFocus={() => { props.resetFocusedIndex() }} + clearInputState={props.clearInputState} + deleteLastLabel={props.deleteLastLabel} + selectOrCreateLabel={props.selectOrCreateLabel} /> @@ -223,12 +257,11 @@ function Footer(props: FooterProps): JSX.Element { export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element { const router = useRouter() - const [filterText, setFilterText] = useState('') const { labels, revalidate } = useGetLabelsQuery() useEffect(() => { setFocusedIndex(undefined) - }, [filterText]) + }, [props.inputValue]) const isSelected = useCallback( (label: Label): boolean => { @@ -256,6 +289,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element { props.onLabelsUpdated(newSelectedLabels) } + props.clearInputState() revalidate() }, [isSelected, props, revalidate] @@ -267,12 +301,12 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element { } return labels .filter((label) => { - return label.name.toLowerCase().includes(filterText.toLowerCase()) + return label.name.toLowerCase().includes(props.inputValue.toLowerCase()) }) .sort((left: Label, right: Label) => { return left.name.localeCompare(right.name) }) - }, [labels, filterText]) + }, [labels, props.inputValue]) // Move focus through the labels list on tab or arrow up/down keys const [focusedIndex, setFocusedIndex] = useState( @@ -291,7 +325,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element { showErrorToast('Failed to create label', { position: 'bottom-right' }) } }, - [filterText, toggleLabel] + [props.inputValue, toggleLabel] ) const handleKeyDown = useCallback( @@ -307,7 +341,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element { } // If the `Create New label` button isn't visible we skip it // when navigating with the arrow keys - if (focusedIndex === maxIndex && !filterText) { + if (focusedIndex === maxIndex && !props.inputValue) { newIndex = maxIndex - 2 } setFocusedIndex(newIndex) @@ -322,7 +356,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element { } // If the `Create New label` button isn't visible we skip it // when navigating with the arrow keys - if (focusedIndex === maxIndex - 2 && !filterText) { + if (focusedIndex === maxIndex - 2 && !props.inputValue) { newIndex = maxIndex } setFocusedIndex(newIndex) @@ -334,8 +368,8 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element { return } if (focusedIndex === maxIndex - 1) { - const _filterText = filterText - setFilterText('') + const _filterText = props.inputValue + props.setInputValue('') await createLabelFromFilterText(_filterText) return } @@ -348,7 +382,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element { } }, [ - filterText, + props.inputValue, filteredLabels, focusedIndex, createLabelFromFilterText, @@ -369,10 +403,19 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
setFocusedIndex(undefined)} - setFilterText={setFilterText} - filterText={filterText} + inputValue={props.inputValue} + setInputValue={props.setInputValue} selectedLabels={props.selectedLabels} setSelectedLabels={props.setSelectedLabels} + tabCount={props.tabCount} + setTabCount={props.setTabCount} + tabStartValue={props.tabStartValue} + setTabStartValue={props.setTabStartValue} + highlightLastLabel={props.highlightLastLabel} + setHighlightLastLabel={props.setHighlightLastLabel} + deleteLastLabel={props.deleteLastLabel} + selectOrCreateLabel={props.selectOrCreateLabel} + clearInputState={props.clearInputState} />