diff --git a/resources/scripts/api/admin/databases/searchDatabases.ts b/resources/scripts/api/admin/databases/searchDatabases.ts index cff92ee8..cb8b8669 100644 --- a/resources/scripts/api/admin/databases/searchDatabases.ts +++ b/resources/scripts/api/admin/databases/searchDatabases.ts @@ -1,25 +1,18 @@ -import { Database, rawDataToDatabase } from '@/api/admin/databases/getDatabases'; import http from '@/api/http'; +import { Database, rawDataToDatabase } from '@/api/admin/databases/getDatabases'; interface Filters { name?: string; host?: string; } -interface Wow { - [index: string]: string; -} - export default (filters?: Filters): Promise => { - let params = {}; + const params = {}; if (filters !== undefined) { - params = Object.keys(filters).map((key) => { - const a: Wow = {}; - a[`filter[${key}]`] = (filters as unknown as Wow)[key]; - return a; + Object.keys(filters).forEach(key => { + // @ts-ignore + params['filter[' + key + ']'] = filters[key]; }); - - console.log(params); } return new Promise((resolve, reject) => { diff --git a/resources/scripts/api/admin/locations/searchLocations.ts b/resources/scripts/api/admin/locations/searchLocations.ts index 4d065f94..e87078bf 100644 --- a/resources/scripts/api/admin/locations/searchLocations.ts +++ b/resources/scripts/api/admin/locations/searchLocations.ts @@ -1,7 +1,12 @@ import http from '@/api/http'; import { Location, rawDataToLocation } from '@/api/admin/locations/getLocations'; -export default (filters?: Record): Promise => { +interface Filters { + short?: string; + long?: string; +} + +export default (filters?: Filters): Promise => { const params = {}; if (filters !== undefined) { Object.keys(filters).forEach(key => { diff --git a/resources/scripts/api/admin/nodes/getNodes.ts b/resources/scripts/api/admin/nodes/getNodes.ts index 99a2fd6e..3e3fd5cb 100644 --- a/resources/scripts/api/admin/nodes/getNodes.ts +++ b/resources/scripts/api/admin/nodes/getNodes.ts @@ -61,7 +61,7 @@ export const rawDataToNode = ({ attributes }: FractalResponseData): Node => ({ updatedAt: new Date(attributes.updated_at), relations: { - databaseHost: attributes.relationships?.databaseHost !== undefined ? rawDataToDatabase(attributes.relationships.databaseHost as FractalResponseData) : undefined, + databaseHost: attributes.relationships?.database_host !== undefined ? rawDataToDatabase(attributes.relationships.database_host as FractalResponseData) : undefined, location: attributes.relationships?.location !== undefined ? rawDataToLocation(attributes.relationships.location as FractalResponseData) : undefined, }, }); diff --git a/resources/scripts/components/admin/nodes/DatabaseSelect.tsx b/resources/scripts/components/admin/nodes/DatabaseSelect.tsx index fca37ad8..ac768735 100644 --- a/resources/scripts/components/admin/nodes/DatabaseSelect.tsx +++ b/resources/scripts/components/admin/nodes/DatabaseSelect.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; -import SearchableSelect, { Option } from '@/components/elements/SearchableSelect'; -import searchDatabases from '@/api/admin/databases/searchDatabases'; import { Database } from '@/api/admin/databases/getDatabases'; +import searchDatabases from '@/api/admin/databases/searchDatabases'; +import SearchableSelect, { Option } from '@/components/elements/SearchableSelect'; -export default ({ selected }: { selected?: Database | null }) => { - const [ database, setDatabase ] = useState(selected || null); +export default ({ selected }: { selected: Database | null }) => { + const [ database, setDatabase ] = useState(selected); const [ databases, setDatabases ] = useState([]); const onSearch = (query: string): Promise => { @@ -37,7 +37,7 @@ export default ({ selected }: { selected?: Database | null }) => { nullable > {databases.map(d => ( - ))} diff --git a/resources/scripts/components/admin/nodes/LocationSelect.tsx b/resources/scripts/components/admin/nodes/LocationSelect.tsx index 8a17abc0..4f23c206 100644 --- a/resources/scripts/components/admin/nodes/LocationSelect.tsx +++ b/resources/scripts/components/admin/nodes/LocationSelect.tsx @@ -1,140 +1,46 @@ -import React, { useEffect, useState } from 'react'; -import styled from 'styled-components/macro'; -import tw from 'twin.macro'; -import Input from '@/components/elements/Input'; -import Label from '@/components/elements/Label'; +import React, { useState } from 'react'; import { Location } from '@/api/admin/locations/getLocations'; import searchLocations from '@/api/admin/locations/searchLocations'; -import InputSpinner from '@/components/elements/InputSpinner'; -import { debounce } from 'debounce'; +import SearchableSelect, { Option } from '@/components/elements/SearchableSelect'; -const Dropdown = styled.div<{ expanded: boolean }>` - ${tw`absolute mt-1 w-full rounded-md bg-neutral-900 shadow-lg z-10`}; - ${props => !props.expanded && tw`hidden`}; -`; - -export default ({ defaultLocation }: { defaultLocation: Location | null }) => { - const [ loading, setLoading ] = useState(false); - const [ expanded, setExpanded ] = useState(false); - const [ location, setLocation ] = useState(defaultLocation); +export default ({ selected }: { selected: Location | null }) => { + const [ location, setLocation ] = useState(selected); const [ locations, setLocations ] = useState([]); - const [ inputText, setInputText ] = useState(''); - - const onFocus = () => { - setInputText(''); - setLocations([]); - setExpanded(true); + const onSearch = (query: string): Promise => { + return new Promise((resolve, reject) => { + searchLocations({ short: query }).then((locations) => { + setLocations(locations); + return resolve(); + }).catch(reject); + }); }; - const search = debounce((query: string) => { - if (!expanded) { - return; - } - - if (query === '' || query.length < 2) { - setLocations([]); - return; - } - - setLoading(true); - searchLocations({ short: query }).then((locations) => { - setLocations(locations); - }).then(() => setLoading(false)); - }, 250); - - const selectLocation = (location: Location) => { + const onSelect = (location: Location) => { setLocation(location); }; - useEffect(() => { - setInputText(location?.short || ''); - setExpanded(false); - }, [ location ]); - - useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (e.key !== 'Escape') { - return; - } - - setInputText(location?.short || ''); - setExpanded(false); - }; - - window.addEventListener('keydown', handler); - return () => { - window.removeEventListener('keydown', handler); - }; - }, [ expanded ]); + const getSelectedText = (location: Location | null): string => { + return location?.short || ''; + }; return ( -
- - -
- - { - setInputText(e.currentTarget.value); - search(e.currentTarget.value); - }} - /> - - -
- -
- - - {locations.length < 1 ? - inputText.length < 2 ? -
-

Please type 2 or more characters.

-
- : -
-

No results found.

-
- : -
    - {locations.map(l => ( - l.id === location?.id ? -
  • { - e.stopPropagation(); - selectLocation(l); - }} - > -
    - - {l.short} - -
    - - - - -
  • - : -
  • { - e.stopPropagation(); - selectLocation(l); - }} - > -
    - - {l.short} - -
    -
  • - ))} -
- } -
-
-
+ + {locations.map(d => ( + + ))} + ); }; diff --git a/resources/scripts/components/admin/nodes/NodeSettingsContainer.tsx b/resources/scripts/components/admin/nodes/NodeSettingsContainer.tsx index 90c8cf87..5df7e85d 100644 --- a/resources/scripts/components/admin/nodes/NodeSettingsContainer.tsx +++ b/resources/scripts/components/admin/nodes/NodeSettingsContainer.tsx @@ -98,11 +98,11 @@ export default () => {
- +
- +
diff --git a/resources/scripts/components/elements/SearchableSelect.tsx b/resources/scripts/components/elements/SearchableSelect.tsx index 915fabda..148bb4e0 100644 --- a/resources/scripts/components/elements/SearchableSelect.tsx +++ b/resources/scripts/components/elements/SearchableSelect.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useEffect, useState } from 'react'; +import React, { createRef, ReactElement, useEffect, useState } from 'react'; import { debounce } from 'debounce'; import styled from 'styled-components/macro'; import tw from 'twin.macro'; @@ -35,12 +35,20 @@ function SearchableSelect ({ id, name, selected, items, setItems, onSearch, o const [ inputText, setInputText ] = useState(''); + const searchInput = createRef(); + const itemsList = createRef(); + const onFocus = () => { setInputText(''); setItems([]); setExpanded(true); }; + const onBlur = () => { + setInputText(getSelectedText(selected) || ''); + setExpanded(false); + }; + const search = debounce((query: string) => { if (!expanded) { return; @@ -53,7 +61,7 @@ function SearchableSelect ({ id, name, selected, items, setItems, onSearch, o setLoading(true); onSearch(query).then(() => setLoading(false)); - }, 250); + }, 1000); useEffect(() => { setInputText(getSelectedText(selected) || ''); @@ -61,18 +69,52 @@ function SearchableSelect ({ id, name, selected, items, setItems, onSearch, o }, [ selected ]); useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (e.key !== 'Escape') { + const keydownHandler = (e: KeyboardEvent) => { + if (e.key !== 'Tab' && e.key !== 'Escape') { return; } - setInputText(getSelectedText(selected) || ''); - setExpanded(false); + onBlur(); }; - window.addEventListener('keydown', handler); + const clickHandler = (e: MouseEvent) => { + const input = searchInput.current; + const menu = itemsList.current; + + if (e.button === 2 || !expanded || !input || !menu) { + return; + } + + if (e.target === input || input.contains(e.target as Node)) { + return; + } + + if (e.target === menu || menu.contains(e.target as Node)) { + return; + } + + if (e.target === input || input.contains(e.target as Node)) { + return; + } + + if (e.target === menu || menu.contains(e.target as Node)) { + return; + } + + onBlur(); + }; + + const contextmenuHandler = () => { + onBlur(); + }; + + window.addEventListener('keydown', keydownHandler); + window.addEventListener('click', clickHandler); + window.addEventListener('contextmenu', contextmenuHandler); return () => { - window.removeEventListener('keydown', handler); + window.removeEventListener('keydown', keydownHandler); + window.removeEventListener('click', clickHandler); + window.removeEventListener('contextmenu', contextmenuHandler); }; }, [ expanded ]); @@ -86,13 +128,16 @@ function SearchableSelect ({ id, name, selected, items, setItems, onSearch, o onClick: onClick.bind(child), })); + // @ts-ignore + const selectedId = selected?.id; + return (
- +
- { + { setInputText(e.currentTarget.value); search(e.currentTarget.value); }} @@ -105,7 +150,7 @@ function SearchableSelect ({ id, name, selected, items, setItems, onSearch, o
- + { items.length < 1 ? inputText.length < 2 ?
@@ -116,7 +161,13 @@ function SearchableSelect ({ id, name, selected, items, setItems, onSearch, o

No results found.

: -
    +
      {c}
    } @@ -127,6 +178,7 @@ function SearchableSelect ({ id, name, selected, items, setItems, onSearch, o } interface OptionProps { + selectId: string; id: string | number; item: T; active: boolean; @@ -136,7 +188,8 @@ interface OptionProps { children: React.ReactNode; } -export function Option ({ id, item, active, onClick, children }: OptionProps) { +export function Option ({ selectId, id, item, active, onClick, children }: OptionProps) { + // This should never be true, but just in-case we set it to an empty function to make sure shit doesn't blow up. if (onClick === undefined) { // eslint-disable-next-line @typescript-eslint/no-empty-function onClick = () => () => {}; @@ -144,7 +197,7 @@ export function Option ({ id, item, active, onClick, children }: OptionProps< if (active) { return ( -
  • +
  • {children}