Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter modal fixes #4553

Merged
merged 7 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 135 additions & 56 deletions assets/js/dashboard/components/combobox.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import React, { Fragment, useState, useCallback, useEffect, useRef } from 'react'
/** @format */

import React, {
Fragment,
useState,
useCallback,
useEffect,
useRef
} from 'react'
import { Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import classNames from 'classnames'
import { useMountedEffect, useDebounce } from '../custom-hooks'

function Option({isHighlighted, onClick, onMouseEnter, text, id}) {
const className = classNames('relative cursor-pointer select-none py-2 px-3', {
'text-gray-900 dark:text-gray-300': !isHighlighted,
'bg-indigo-600 text-white': isHighlighted,
})
function Option({ isHighlighted, onClick, onMouseEnter, text, id }) {
const className = classNames(
'relative cursor-pointer select-none py-2 px-3',
{
'text-gray-900 dark:text-gray-300': !isHighlighted,
'bg-indigo-600 text-white': isHighlighted
}
)

return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
Expand All @@ -27,7 +38,7 @@ function scrollTo(wrapper, id) {
const el = wrapper.querySelector('#' + id)

if (el) {
el.scrollIntoView({block: 'center'})
el.scrollIntoView({ block: 'center' })
}
}
}
Expand All @@ -48,7 +59,7 @@ export default function PlausibleCombobox({
placeholder,
forceLoading,
className,
boxClass,
boxClass
}) {
const isEmpty = values.length === 0
const [options, setOptions] = useState([])
Expand All @@ -63,8 +74,12 @@ export default function PlausibleCombobox({
const loading = isLoading || !!forceLoading

const visibleOptions = [...options]
if (freeChoice && search.length > 0 && options.every(option => option.value !== search)) {
visibleOptions.push({value: search, label: search, freeChoice: true})
if (
freeChoice &&
search.length > 0 &&
options.every((option) => option.value !== search)
) {
visibleOptions.push({ value: search, label: search, freeChoice: true })
}

const afterFetchOptions = useCallback((loadedOptions) => {
Expand All @@ -74,6 +89,7 @@ export default function PlausibleCombobox({
}, [])

const initialFetchOptions = useCallback(() => {
setLoading(true)
fetchOptions('').then(afterFetchOptions)
}, [fetchOptions, afterFetchOptions])

Expand All @@ -87,7 +103,9 @@ export default function PlausibleCombobox({
const debouncedSearchOptions = useDebounce(searchOptions)

useEffect(() => {
if (isOpen) { initialFetchOptions() }
if (isOpen) {
initialFetchOptions()
}
}, [isOpen, initialFetchOptions])

useMountedEffect(() => {
Expand Down Expand Up @@ -136,13 +154,19 @@ export default function PlausibleCombobox({
}

function isOptionDisabled(option) {
const optionAlreadySelected = values.some((val) => val.value === option.value)
const optionDisabled = (disabledOptions || []).some((val) => val?.value === option.value)
const optionAlreadySelected = values.some(
(val) => val.value === option.value
)
const optionDisabled = (disabledOptions || []).some(
(val) => val?.value === option.value
)
return optionAlreadySelected || optionDisabled
}

function onInput(e) {
if (!isOpen) { setOpen(true) }
if (!isOpen) {
setOpen(true)
}
setSearch(e.target.value)
}

Expand Down Expand Up @@ -177,15 +201,19 @@ export default function PlausibleCombobox({
}

const handleClick = useCallback((e) => {
if (containerRef.current && containerRef.current.contains(e.target)) { return }
if (containerRef.current && containerRef.current.contains(e.target)) {
return
}

setSearch('')
setOpen(false)
}, [])

useEffect(() => {
document.addEventListener("mousedown", handleClick, false)
return () => { document.removeEventListener("mousedown", handleClick, false) }
document.addEventListener('mousedown', handleClick, false)
return () => {
document.removeEventListener('mousedown', handleClick, false)
}
}, [handleClick])

useEffect(() => {
Expand All @@ -194,7 +222,8 @@ export default function PlausibleCombobox({
}
}, [isEmpty, singleOption, autoFocus])

const searchBoxClass = 'border-none py-1 px-0 w-full inline-block rounded-md focus:outline-none focus:ring-0 text-sm'
const searchBoxClass =
'border-none py-1 px-0 w-full inline-block rounded-md focus:outline-none focus:ring-0 text-sm'

const containerClass = classNames('relative w-full', {
[className]: !!className,
Expand All @@ -205,17 +234,17 @@ export default function PlausibleCombobox({
const itemSelected = values.length === 1

return (
<div className='flex items-center truncate'>
{ itemSelected && renderSingleSelectedItem() }
<div className="flex items-center truncate">
{itemSelected && renderSingleSelectedItem()}
<input
className={searchBoxClass}
ref={searchRef}
value={search}
style={{backgroundColor: "inherit"}}
style={{ backgroundColor: 'inherit' }}
placeholder={itemSelected ? '' : placeholder}
type="text"
onChange={onInput}>
</input>
onChange={onInput}
></input>
</div>
)
}
Expand All @@ -233,32 +262,55 @@ export default function PlausibleCombobox({
function renderMultiOptionContent() {
return (
<>
{ values.map((value) => {
return (
<div key={value.value} className="bg-indigo-100 dark:bg-indigo-600 flex justify-between w-full rounded-sm px-2 py-0.5 m-0.5 text-sm">
<span className='break-all'>{value.label}</span>
<span onClick={(e) => removeOption(value, e)} className="cursor-pointer font-bold ml-1">&times;</span>
</div>
)
})
}
<input className={searchBoxClass} ref={searchRef} value={search} style={{backgroundColor: "inherit"}} placeholder={placeholder} type="text" onChange={onInput}></input>
{values.map((value) => {
return (
<div
key={value.value}
className="bg-indigo-100 dark:bg-indigo-600 flex justify-between w-full rounded-sm px-2 py-0.5 m-0.5 text-sm"
>
<span className="break-all">{value.label}</span>
<span
onClick={(e) => removeOption(value, e)}
className="cursor-pointer font-bold ml-1"
>
&times;
</span>
</div>
)
})}
<input
className={searchBoxClass}
ref={searchRef}
value={search}
style={{ backgroundColor: 'inherit' }}
placeholder={placeholder}
type="text"
onChange={onInput}
></input>
</>
)
}

function renderDropDownContent() {
const matchesFound = visibleOptions.length > 0 && visibleOptions.some(option => !isOptionDisabled(option))
const matchesFound =
visibleOptions.length > 0 &&
visibleOptions.some((option) => !isOptionDisabled(option))

if (loading) {
return <div className="relative cursor-default select-none py-2 px-4 text-gray-700 dark:text-gray-300">Loading options...</div>
return (
<div className="relative cursor-default select-none py-2 px-4 text-gray-700 dark:text-gray-300">
Loading options...
</div>
)
}

if (matchesFound) {
return visibleOptions
.filter(option => !isOptionDisabled(option))
.filter((option) => !isOptionDisabled(option))
.map((option, i) => {
const text = option.freeChoice ? `Filter by '${option.label}'` : option.label
const text = option.freeChoice
? `Filter by '${option.label}'`
: option.label

return (
<Option
Expand All @@ -274,51 +326,78 @@ export default function PlausibleCombobox({
}

if (freeChoice) {
return <div className="relative cursor-default select-none py-2 px-4 text-gray-700 dark:text-gray-300">Start typing to apply filter</div>
return (
<div className="relative cursor-default select-none py-2 px-4 text-gray-700 dark:text-gray-300">
Start typing to apply filter
</div>
)
}

return (
<div className="relative cursor-default select-none py-2 px-4 text-gray-700 dark:text-gray-300">
No matches found in the current dashboard. Try selecting a different time range or searching for something different
No matches found in the current dashboard. Try selecting a different
time range or searching for something different
</div>
)
}

const defaultBoxClass = 'pl-2 pr-8 py-1 w-full dark:bg-gray-900 dark:text-gray-300 rounded-md shadow-sm border border-gray-300 dark:border-gray-700 focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500'
const defaultBoxClass =
'pl-2 pr-8 py-1 w-full dark:bg-gray-900 dark:text-gray-300 rounded-md shadow-sm border border-gray-300 dark:border-gray-700 focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500'
const finalBoxClass = classNames(boxClass || defaultBoxClass, {
'border-indigo-500 ring-1 ring-indigo-500': isOpen,
'border-indigo-500 ring-1 ring-indigo-500': isOpen
})

return (
<div onKeyDown={onKeyDown} ref={containerRef} className={containerClass}>
<div onClick={toggleOpen} className={finalBoxClass }>
<div onClick={toggleOpen} className={finalBoxClass}>
{singleOption && renderSingleOptionContent()}
{!singleOption && renderMultiOptionContent()}
<div className="cursor-pointer absolute inset-y-0 right-0 flex items-center pr-2">
{!loading && <ChevronDownIcon className="h-4 w-4 text-gray-500" />}
{loading && <Spinner />}
</div>
</div>
{isOpen && <Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={isOpen}
>
<ul ref={listRef} className="z-50 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm dark:bg-gray-900">
{ renderDropDownContent() }
</ul>
</Transition>}
{isOpen && (
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={isOpen}
>
<ul
ref={listRef}
className="z-50 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm dark:bg-gray-900"
>
{renderDropDownContent()}
</ul>
</Transition>
)}
</div>
)
}

function Spinner() {
return (
<svg className="animate-spin h-4 w-4 text-indigo-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<svg
className="animate-spin h-4 w-4 text-indigo-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
)
}
Loading