mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 22:53:25 +00:00
feat: add validation for search modal
This commit is contained in:
@@ -5,7 +5,8 @@ export default class SearchesController {
|
|||||||
async search({ request, auth }: HttpContext) {
|
async search({ request, auth }: HttpContext) {
|
||||||
const term = request.qs()?.term;
|
const term = request.qs()?.term;
|
||||||
if (!term) {
|
if (!term) {
|
||||||
return console.warn('qs term null');
|
console.warn('qs term null');
|
||||||
|
return { error: 'missing "term" query param' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rows } = await db.rawQuery('SELECT * FROM search_text(?, ?)', [
|
const { rows } = await db.rawQuery('SELECT * FROM search_text(?, ?)', [
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default class extends BaseSchema {
|
|||||||
id INTEGER,
|
id INTEGER,
|
||||||
type TEXT,
|
type TEXT,
|
||||||
name VARCHAR(254),
|
name VARCHAR(254),
|
||||||
|
url TEXT,
|
||||||
collection_id INTEGER,
|
collection_id INTEGER,
|
||||||
matched_part TEXT,
|
matched_part TEXT,
|
||||||
rank DOUBLE PRECISION
|
rank DOUBLE PRECISION
|
||||||
@@ -25,20 +26,20 @@ export default class extends BaseSchema {
|
|||||||
AS $$
|
AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
SELECT links.id, 'link' AS type, links.name, collections.id AS collection_id,
|
SELECT links.id, 'link' AS type, links.name, links.url, collections.id AS collection_id,
|
||||||
ts_headline('english', unaccent(links.name), plainto_tsquery('english', unaccent(search_query))) AS matched_part,
|
ts_headline('english', unaccent(links.name), plainto_tsquery('english', unaccent(search_query))) AS matched_part,
|
||||||
ts_rank_cd(to_tsvector('english', unaccent(links.name)), plainto_tsquery('english', unaccent(search_query)))::DOUBLE PRECISION AS rank
|
ts_rank_cd(to_tsvector('english', unaccent(links.name)), plainto_tsquery('english', unaccent(search_query)))::DOUBLE PRECISION AS rank
|
||||||
FROM links
|
FROM links
|
||||||
LEFT JOIN collections ON links.collection_id = collections.id
|
LEFT JOIN collections ON links.collection_id = collections.id
|
||||||
WHERE unaccent(links.name) ILIKE '%' || unaccent(search_query) || '%'
|
WHERE unaccent(links.name) ILIKE '%' || unaccent(search_query) || '%'
|
||||||
AND (p_author_id IS NULL OR links.author_id = p_author_id)
|
AND (p_author_id IS NULL OR links.author_id = p_author_id)
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT collections.id, 'collection' AS type, collections.name, NULL AS collection_id,
|
SELECT collections.id, 'collection' AS type, collections.name, NULL AS url, NULL AS collection_id,
|
||||||
ts_headline('english', unaccent(collections.name), plainto_tsquery('english', unaccent(search_query))) AS matched_part,
|
ts_headline('english', unaccent(collections.name), plainto_tsquery('english', unaccent(search_query))) AS matched_part,
|
||||||
ts_rank_cd(to_tsvector('english', unaccent(collections.name)), plainto_tsquery('english', unaccent(search_query)))::DOUBLE PRECISION AS rank
|
ts_rank_cd(to_tsvector('english', unaccent(collections.name)), plainto_tsquery('english', unaccent(search_query)))::DOUBLE PRECISION AS rank
|
||||||
FROM collections
|
FROM collections
|
||||||
WHERE unaccent(collections.name) ILIKE '%' || unaccent(search_query) || '%'
|
WHERE unaccent(collections.name) ILIKE '%' || unaccent(search_query) || '%'
|
||||||
AND (p_author_id IS NULL OR collections.author_id = p_author_id)
|
AND (p_author_id IS NULL OR collections.author_id = p_author_id)
|
||||||
ORDER BY rank DESC NULLS LAST, matched_part DESC NULLS LAST;
|
ORDER BY rank DESC NULLS LAST, matched_part DESC NULLS LAST;
|
||||||
END;
|
END;
|
||||||
$$
|
$$
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function Dropdown({
|
|||||||
const { isShowing, toggle, close } = useToggle();
|
const { isShowing, toggle, close } = useToggle();
|
||||||
|
|
||||||
useClickOutside(dropdownRef, close);
|
useClickOutside(dropdownRef, close);
|
||||||
useShortcut('ESCAPE_KEY', close);
|
useShortcut('ESCAPE_KEY', close, { disableGlobalCheck: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownStyle
|
<DropdownStyle
|
||||||
|
|||||||
29
inertia/components/dashboard/search/no_search_result.tsx
Normal file
29
inertia/components/dashboard/search/no_search_result.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FcGoogle } from 'react-icons/fc';
|
||||||
|
|
||||||
|
const NoSearchResultStyle = styled.i({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '0.25em',
|
||||||
|
|
||||||
|
'& > span': {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function NoSearchResult() {
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
return (
|
||||||
|
<NoSearchResultStyle>
|
||||||
|
{t('search-with')}
|
||||||
|
{''}
|
||||||
|
<span>
|
||||||
|
<FcGoogle size={20} />
|
||||||
|
oogle
|
||||||
|
</span>
|
||||||
|
</NoSearchResultStyle>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,11 +2,12 @@ import styled from '@emotion/styled';
|
|||||||
import { route } from '@izzyjs/route/client';
|
import { route } from '@izzyjs/route/client';
|
||||||
import { FormEvent, useCallback, useEffect, useState } from 'react';
|
import { FormEvent, useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AiOutlineFolder } from 'react-icons/ai';
|
|
||||||
import { CiLink } from 'react-icons/ci';
|
|
||||||
import { IoIosSearch } from 'react-icons/io';
|
import { IoIosSearch } from 'react-icons/io';
|
||||||
import Modal from '~/components/common/modal/modal';
|
import Modal from '~/components/common/modal/modal';
|
||||||
import UnstyledList from '~/components/common/unstyled/unstyled_list';
|
import NoSearchResult from '~/components/dashboard/search/no_search_result';
|
||||||
|
import SearchResultList from '~/components/dashboard/search/search_result_list';
|
||||||
|
import { GOOGLE_SEARCH_URL } from '~/constants';
|
||||||
|
import useActiveCollection from '~/hooks/use_active_collection';
|
||||||
import useCollections from '~/hooks/use_collections';
|
import useCollections from '~/hooks/use_collections';
|
||||||
import useToggle from '~/hooks/use_modal';
|
import useToggle from '~/hooks/use_modal';
|
||||||
import useShortcut from '~/hooks/use_shortcut';
|
import useShortcut from '~/hooks/use_shortcut';
|
||||||
@@ -29,8 +30,11 @@ interface SearchModalProps {
|
|||||||
function SearchModal({ openItem: OpenItem }: SearchModalProps) {
|
function SearchModal({ openItem: OpenItem }: SearchModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { collections } = useCollections();
|
const { collections } = useCollections();
|
||||||
|
const { setActiveCollection } = useActiveCollection();
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||||
const [results, setResults] = useState<SearchResult[]>([]);
|
const [results, setResults] = useState<SearchResult[]>([]);
|
||||||
|
const [selectedItem, setSelectedItem] = useState<SearchResult | null>(null);
|
||||||
|
|
||||||
const searchModal = useToggle(!!searchTerm);
|
const searchModal = useToggle(!!searchTerm);
|
||||||
|
|
||||||
@@ -40,25 +44,52 @@ function SearchModal({ openItem: OpenItem }: SearchModalProps) {
|
|||||||
}, [searchModal]);
|
}, [searchModal]);
|
||||||
|
|
||||||
const handleSearchInputChange = (value: string) => setSearchTerm(value);
|
const handleSearchInputChange = (value: string) => setSearchTerm(value);
|
||||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) =>
|
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
handleCloseModal();
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
return window.open(GOOGLE_SEARCH_URL + encodeURI(searchTerm.trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedItem) return;
|
||||||
|
|
||||||
|
if (selectedItem.type === 'collection') {
|
||||||
|
const collection = collections.find((c) => c.id === selectedItem.id);
|
||||||
|
if (collection) {
|
||||||
|
setActiveCollection(collection);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(selectedItem.url);
|
||||||
|
};
|
||||||
|
|
||||||
useShortcut('OPEN_SEARCH_KEY', searchModal.open, {
|
useShortcut('OPEN_SEARCH_KEY', searchModal.open, {
|
||||||
enabled: !searchModal.isShowing,
|
enabled: !searchModal.isShowing,
|
||||||
});
|
});
|
||||||
useShortcut('ESCAPE_KEY', handleCloseModal, {
|
useShortcut('ESCAPE_KEY', handleCloseModal, {
|
||||||
enabled: searchModal.isShowing,
|
enabled: searchModal.isShowing,
|
||||||
|
disableGlobalCheck: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchTerm.trim() === '') {
|
if (searchTerm.trim() === '') {
|
||||||
return setResults([]);
|
return setResults([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
const { url, method } = route('search', { qs: { term: searchTerm } });
|
const { url, method } = route('search', { qs: { term: searchTerm } });
|
||||||
makeRequest({
|
makeRequest({
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
}).then(({ results: _results }) => setResults(_results));
|
controller,
|
||||||
|
}).then(({ results: _results }) => {
|
||||||
|
setResults(_results);
|
||||||
|
setSelectedItem(_results?.[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => controller.abort();
|
||||||
}, [searchTerm]);
|
}, [searchTerm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -70,6 +101,7 @@ function SearchModal({ openItem: OpenItem }: SearchModalProps) {
|
|||||||
close={handleCloseModal}
|
close={handleCloseModal}
|
||||||
opened={searchModal.isShowing}
|
opened={searchModal.isShowing}
|
||||||
hideCloseBtn
|
hideCloseBtn
|
||||||
|
css={{ width: '650px' }}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
@@ -100,37 +132,14 @@ function SearchModal({ openItem: OpenItem }: SearchModalProps) {
|
|||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{results.length > 0 && (
|
{results.length > 0 && selectedItem && (
|
||||||
<UnstyledList css={{ maxHeight: '500px', overflow: 'auto' }}>
|
<SearchResultList
|
||||||
{results.map((result) => {
|
results={results}
|
||||||
const ItemIcon =
|
selectedItem={selectedItem}
|
||||||
result.type === 'link' ? CiLink : AiOutlineFolder;
|
setSelectedItem={setSelectedItem}
|
||||||
const collection =
|
/>
|
||||||
result.type === 'link'
|
|
||||||
? collections.find((c) => c.id === result.collection_id)
|
|
||||||
: undefined;
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={result.type + result.id.toString()}
|
|
||||||
css={{
|
|
||||||
fontSize: '16px',
|
|
||||||
display: 'flex',
|
|
||||||
gap: '0.35em',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ItemIcon size={24} />
|
|
||||||
<span
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: result.matched_part ?? result.name,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{collection && <>({collection.name})</>}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</UnstyledList>
|
|
||||||
)}
|
)}
|
||||||
|
{results.length === 0 && !!searchTerm.trim() && <NoSearchResult />}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={searchTerm.length === 0}
|
disabled={searchTerm.length === 0}
|
||||||
|
|||||||
143
inertia/components/dashboard/search/search_result_item.tsx
Normal file
143
inertia/components/dashboard/search/search_result_item.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { RefObject, useEffect, useRef, useState } from 'react';
|
||||||
|
import { AiOutlineFolder } from 'react-icons/ai';
|
||||||
|
import TextEllipsis from '~/components/common/text_ellipsis';
|
||||||
|
import LinkFavicon from '~/components/dashboard/link/link_favicon';
|
||||||
|
import useCollections from '~/hooks/use_collections';
|
||||||
|
import {
|
||||||
|
SearchResult,
|
||||||
|
SearchResultCollection,
|
||||||
|
SearchResultLink,
|
||||||
|
} from '~/types/search';
|
||||||
|
|
||||||
|
const SearchItemStyle = styled('li', {
|
||||||
|
shouldForwardProp: (propName) => propName !== 'isActive',
|
||||||
|
})<{ isActive: boolean }>(({ theme, isActive }) => ({
|
||||||
|
fontSize: '16px',
|
||||||
|
backgroundColor: isActive ? theme.colors.background : 'transparent',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.35em',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: theme.border.radius,
|
||||||
|
padding: '0.25em 0.35em !important',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ItemLegeng = styled.span(({ theme }) => ({
|
||||||
|
fontSize: '13px',
|
||||||
|
color: theme.colors.grey,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface CommonResultProps {
|
||||||
|
isActive: boolean;
|
||||||
|
innerRef: RefObject<HTMLLIElement>;
|
||||||
|
onHoverEnter: () => void;
|
||||||
|
onHoverLeave: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SearchResultItem({
|
||||||
|
result,
|
||||||
|
isActive,
|
||||||
|
onHover,
|
||||||
|
}: {
|
||||||
|
result: SearchResult;
|
||||||
|
isActive: boolean;
|
||||||
|
onHover: (result: SearchResult) => void;
|
||||||
|
}) {
|
||||||
|
const itemRef = useRef<HTMLLIElement>(null);
|
||||||
|
const [isHovering, setHovering] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleHoverEnter = () => {
|
||||||
|
if (!isHovering) {
|
||||||
|
onHover(result);
|
||||||
|
setHovering(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHoverLeave = () => setHovering(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isActive && !isHovering) {
|
||||||
|
itemRef.current?.scrollIntoView({
|
||||||
|
behavior: 'instant',
|
||||||
|
block: 'nearest',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [itemRef, isActive]);
|
||||||
|
|
||||||
|
return result.type === 'collection' ? (
|
||||||
|
<ResultCollection
|
||||||
|
result={result}
|
||||||
|
isActive={isActive}
|
||||||
|
onHoverEnter={handleHoverEnter}
|
||||||
|
onHoverLeave={handleHoverLeave}
|
||||||
|
innerRef={itemRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ResultLink
|
||||||
|
result={result}
|
||||||
|
isActive={isActive}
|
||||||
|
onHoverEnter={handleHoverEnter}
|
||||||
|
onHoverLeave={handleHoverLeave}
|
||||||
|
innerRef={itemRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResultLink({
|
||||||
|
result,
|
||||||
|
isActive,
|
||||||
|
innerRef,
|
||||||
|
onHoverEnter,
|
||||||
|
onHoverLeave,
|
||||||
|
}: {
|
||||||
|
result: SearchResultLink;
|
||||||
|
} & CommonResultProps) {
|
||||||
|
const { collections } = useCollections();
|
||||||
|
const collection = collections.find((c) => c.id === result.collection_id);
|
||||||
|
const link = collection?.links.find((l) => l.id === result.id);
|
||||||
|
|
||||||
|
if (!collection || !link) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchItemStyle
|
||||||
|
onMouseEnter={onHoverEnter}
|
||||||
|
onMouseLeave={onHoverLeave}
|
||||||
|
isActive={isActive}
|
||||||
|
key={result.type + result.id.toString()}
|
||||||
|
ref={innerRef}
|
||||||
|
>
|
||||||
|
<LinkFavicon url={link.url} size={20} />
|
||||||
|
<TextEllipsis
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: result.matched_part ?? result.name,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ItemLegeng>({collection.name})</ItemLegeng>
|
||||||
|
</SearchItemStyle>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResultCollection = ({
|
||||||
|
result,
|
||||||
|
isActive,
|
||||||
|
innerRef,
|
||||||
|
onHoverEnter,
|
||||||
|
onHoverLeave,
|
||||||
|
}: {
|
||||||
|
result: SearchResultCollection;
|
||||||
|
} & CommonResultProps) => (
|
||||||
|
<SearchItemStyle
|
||||||
|
onMouseEnter={onHoverEnter}
|
||||||
|
onMouseLeave={onHoverLeave}
|
||||||
|
isActive={isActive}
|
||||||
|
key={result.type + result.id.toString()}
|
||||||
|
ref={innerRef}
|
||||||
|
>
|
||||||
|
<AiOutlineFolder size={24} />
|
||||||
|
<TextEllipsis
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: result.matched_part ?? result.name,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SearchItemStyle>
|
||||||
|
);
|
||||||
52
inertia/components/dashboard/search/search_result_list.tsx
Normal file
52
inertia/components/dashboard/search/search_result_list.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import UnstyledList from '~/components/common/unstyled/unstyled_list';
|
||||||
|
import SearchResultItem from '~/components/dashboard/search/search_result_item';
|
||||||
|
import useShortcut from '~/hooks/use_shortcut';
|
||||||
|
import { SearchResult } from '~/types/search';
|
||||||
|
|
||||||
|
export default function SearchResultList({
|
||||||
|
results,
|
||||||
|
selectedItem,
|
||||||
|
setSelectedItem,
|
||||||
|
}: {
|
||||||
|
results: SearchResult[];
|
||||||
|
selectedItem: SearchResult;
|
||||||
|
setSelectedItem: (result: SearchResult) => void;
|
||||||
|
}) {
|
||||||
|
const selectedItemIndex = results.findIndex(
|
||||||
|
(item) => item.id === selectedItem.id && item.type === selectedItem.type
|
||||||
|
);
|
||||||
|
|
||||||
|
useShortcut(
|
||||||
|
'ARROW_UP',
|
||||||
|
() => setSelectedItem(results[selectedItemIndex - 1]),
|
||||||
|
{
|
||||||
|
enabled: results.length > 1 && selectedItemIndex !== 0,
|
||||||
|
disableGlobalCheck: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
useShortcut(
|
||||||
|
'ARROW_DOWN',
|
||||||
|
() => setSelectedItem(results[selectedItemIndex + 1]),
|
||||||
|
{
|
||||||
|
enabled: results.length > 1 && selectedItemIndex !== results.length - 1,
|
||||||
|
disableGlobalCheck: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnstyledList css={{ maxHeight: '500px', overflow: 'auto' }}>
|
||||||
|
{results.map((result) => (
|
||||||
|
<SearchResultItem
|
||||||
|
result={result}
|
||||||
|
onHover={setSelectedItem}
|
||||||
|
isActive={
|
||||||
|
selectedItem &&
|
||||||
|
selectedItem.id === result.id &&
|
||||||
|
selectedItem.type === result.type
|
||||||
|
}
|
||||||
|
key={result.type + result.id.toString()}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</UnstyledList>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,13 +4,15 @@ import useGlobalHotkeys from '~/hooks/use_global_hotkeys';
|
|||||||
|
|
||||||
type ShortcutOptions = {
|
type ShortcutOptions = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
disableGlobalCheck?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function useShortcut(
|
export default function useShortcut(
|
||||||
key: keyof typeof KEYS,
|
key: keyof typeof KEYS,
|
||||||
cb: () => void,
|
cb: () => void,
|
||||||
{ enabled }: ShortcutOptions = {
|
{ enabled, disableGlobalCheck }: ShortcutOptions = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
disableGlobalCheck: false,
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { globalHotkeysEnabled } = useGlobalHotkeys();
|
const { globalHotkeysEnabled } = useGlobalHotkeys();
|
||||||
@@ -21,7 +23,7 @@ export default function useShortcut(
|
|||||||
cb();
|
cb();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: key === 'ESCAPE_KEY' ? true : enabled && globalHotkeysEnabled,
|
enabled: disableGlobalCheck ? enabled : enabled && globalHotkeysEnabled,
|
||||||
enableOnFormTags: ['INPUT'],
|
enableOnFormTags: ['INPUT'],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"no-item-found": "No item found",
|
"no-item-found": "No item found",
|
||||||
"admin": "Administrator",
|
"admin": "Administrator",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
|
"search-with": "Search with",
|
||||||
"avatar": "{{name}}'s avatar",
|
"avatar": "{{name}}'s avatar",
|
||||||
"generic-error": "Something went wrong",
|
"generic-error": "Something went wrong",
|
||||||
"generic-error-description": "An error has occurred, if this happens again please <a href=\"https://github.com/Sonny93/my-links\" target=\"_blank\">create an issue</a> with as much detail as possible.",
|
"generic-error-description": "An error has occurred, if this happens again please <a href=\"https://github.com/Sonny93/my-links\" target=\"_blank\">create an issue</a> with as much detail as possible.",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"no-item-found": "Aucun élément trouvé",
|
"no-item-found": "Aucun élément trouvé",
|
||||||
"admin": "Administrateur",
|
"admin": "Administrateur",
|
||||||
"search": "Rechercher",
|
"search": "Rechercher",
|
||||||
|
"search-with": "Rechercher avec",
|
||||||
"avatar": "Avatar de {{name}}",
|
"avatar": "Avatar de {{name}}",
|
||||||
"generic-error": "Une erreur est survenue",
|
"generic-error": "Une erreur est survenue",
|
||||||
"generic-error-description": "Une erreur est survenue, si cela se reproduit merci de <a href=\"https://github.com/Sonny93/my-links\" target=\"_blank\">créer une issue</a> avec le maximum de détails.",
|
"generic-error-description": "Une erreur est survenue, si cela se reproduit merci de <a href=\"https://github.com/Sonny93/my-links\" target=\"_blank\">créer une issue</a> avec le maximum de détails.",
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ export async function makeRequest({
|
|||||||
method = 'GET',
|
method = 'GET',
|
||||||
url,
|
url,
|
||||||
body,
|
body,
|
||||||
|
controller,
|
||||||
}: {
|
}: {
|
||||||
method?: RequestInit['method'];
|
method?: RequestInit['method'];
|
||||||
url: string;
|
url: string;
|
||||||
body?: object | any[];
|
body?: object | any[];
|
||||||
|
controller?: AbortController;
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
const request = await fetch(url, {
|
const request = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
@@ -15,6 +17,7 @@ export async function makeRequest({
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
signal: controller ? controller.signal : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await request.json();
|
const data = await request.json();
|
||||||
|
|||||||
34
inertia/types/search.d.ts
vendored
34
inertia/types/search.d.ts
vendored
@@ -1,18 +1,18 @@
|
|||||||
import type Link from '#models/link';
|
type SearchResultCommon = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
matched_part?: string;
|
||||||
|
rank?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type SearchResult =
|
export type SearchResultCollection = SearchResultCommon & {
|
||||||
| {
|
type: 'collection';
|
||||||
id: string;
|
};
|
||||||
type: 'collection';
|
|
||||||
name: string;
|
export type SearchResultLink = SearchResultCommon & {
|
||||||
matched_part?: string;
|
type: 'link';
|
||||||
rank?: number;
|
collection_id: number;
|
||||||
}
|
url: string;
|
||||||
| {
|
};
|
||||||
id: string;
|
|
||||||
type: 'link';
|
export type SearchResult = SearchResultCollection | SearchResultLink;
|
||||||
name: string;
|
|
||||||
matched_part?: string;
|
|
||||||
rank?: number;
|
|
||||||
collection_id: number;
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user