mirror of
https://github.com/urbit/shrub.git
synced 2024-12-25 04:52:06 +03:00
leap: autocomplete and bksp improvements
This commit is contained in:
parent
40a0fa7698
commit
e5f839b52c
@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useNavStore } from './Nav';
|
import { useLeapStore } from './Nav';
|
||||||
|
|
||||||
export const Help = () => {
|
export const Help = () => {
|
||||||
const select = useNavStore((state) => state.select);
|
const select = useLeapStore((state) => state.select);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
select('Help and Support');
|
select('Help and Support');
|
||||||
|
@ -4,15 +4,32 @@ import React, {
|
|||||||
ChangeEvent,
|
ChangeEvent,
|
||||||
FocusEvent,
|
FocusEvent,
|
||||||
FormEvent,
|
FormEvent,
|
||||||
|
KeyboardEvent,
|
||||||
HTMLAttributes,
|
HTMLAttributes,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef
|
useRef
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Link, useHistory, useRouteMatch } from 'react-router-dom';
|
import { Link, useHistory, useRouteMatch } from 'react-router-dom';
|
||||||
|
import slugify from 'slugify';
|
||||||
import { Cross } from '../components/icons/Cross';
|
import { Cross } from '../components/icons/Cross';
|
||||||
import { MenuState, useNavStore } from './Nav';
|
import { MenuState, useLeapStore } from './Nav';
|
||||||
|
|
||||||
|
function normalizePathEnding(path: string) {
|
||||||
|
const end = path.length - 1;
|
||||||
|
return path[end] === '/' ? path.substring(0, end - 1) : path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPreviousPath(current: string): string {
|
||||||
|
const parts = normalizePathEnding(current).split('/');
|
||||||
|
parts.pop();
|
||||||
|
|
||||||
|
if (parts[parts.length - 1] === 'leap') {
|
||||||
|
parts.push('search');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
type LeapProps = {
|
type LeapProps = {
|
||||||
menu: MenuState;
|
menu: MenuState;
|
||||||
@ -26,36 +43,7 @@ export const Leap = React.forwardRef(({ menu, className }: LeapProps, ref) => {
|
|||||||
const appsMatch = useRouteMatch(`/leap/${menu}/${match?.params.query}/apps`);
|
const appsMatch = useRouteMatch(`/leap/${menu}/${match?.params.query}/apps`);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
useImperativeHandle(ref, () => inputRef.current);
|
useImperativeHandle(ref, () => inputRef.current);
|
||||||
const { searchInput, setSearchInput, selection, select } = useNavStore();
|
const { rawInput, searchInput, matches, selection, select } = useLeapStore();
|
||||||
|
|
||||||
const navigateByInput = useCallback(
|
|
||||||
(input: string) => {
|
|
||||||
const normalizedValue = input.trim().replace(/(~?[\w^_-]{3,13})\//, '$1/apps/');
|
|
||||||
push(`/leap/${menu}/${normalizedValue}`);
|
|
||||||
},
|
|
||||||
[menu]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSearch = useCallback(
|
|
||||||
debounce(
|
|
||||||
(input: string) => {
|
|
||||||
if (!match || appsMatch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateByInput(input);
|
|
||||||
},
|
|
||||||
300,
|
|
||||||
{ leading: true }
|
|
||||||
),
|
|
||||||
[menu, match]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (searchInput) {
|
|
||||||
handleSearch(searchInput);
|
|
||||||
}
|
|
||||||
}, [searchInput]);
|
|
||||||
|
|
||||||
const toggleSearch = useCallback(() => {
|
const toggleSearch = useCallback(() => {
|
||||||
if (selection || menu === 'search') {
|
if (selection || menu === 'search') {
|
||||||
@ -75,17 +63,69 @@ export const Leap = React.forwardRef(({ menu, className }: LeapProps, ref) => {
|
|||||||
toggleSearch();
|
toggleSearch();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
const getMatch = useCallback(
|
||||||
const input = e.target as HTMLInputElement;
|
(value: string) => {
|
||||||
const value = input.value.trim();
|
return matches.find((m) => m.display?.startsWith(value) || m.value.startsWith(value));
|
||||||
setSearchInput(value);
|
},
|
||||||
}, []);
|
[matches]
|
||||||
|
);
|
||||||
|
|
||||||
|
const navigateByInput = useCallback(
|
||||||
|
(input: string) => {
|
||||||
|
const normalizedValue = input.trim().replace(/(~?[\w^_-]{3,13})\//, '$1/apps/');
|
||||||
|
push(`/leap/${menu}/${normalizedValue}`);
|
||||||
|
},
|
||||||
|
[menu]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearch = useCallback(
|
||||||
|
debounce(
|
||||||
|
(input: string) => {
|
||||||
|
if (!match || appsMatch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
useLeapStore.setState({ searchInput: input });
|
||||||
|
navigateByInput(input);
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
{ leading: true }
|
||||||
|
),
|
||||||
|
[menu, match]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const input = e.target as HTMLInputElement;
|
||||||
|
const value = input.value.trim();
|
||||||
|
const isDeletion = (e.nativeEvent as InputEvent).inputType === 'deleteContentBackward';
|
||||||
|
const inputMatch = getMatch(value);
|
||||||
|
const matchValue = inputMatch?.display || inputMatch?.value;
|
||||||
|
|
||||||
|
if (matchValue && inputRef.current && !isDeletion) {
|
||||||
|
inputRef.current.value = matchValue;
|
||||||
|
inputRef.current.setSelectionRange(value.length, matchValue.length);
|
||||||
|
useLeapStore.setState({ rawInput: matchValue });
|
||||||
|
} else {
|
||||||
|
useLeapStore.setState({ rawInput: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearch(value);
|
||||||
|
},
|
||||||
|
[matches]
|
||||||
|
);
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(e: FormEvent<HTMLFormElement>) => {
|
(e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const input = [searchInput];
|
const value = inputRef.current?.value.trim();
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = [slugify(getMatch(value)?.value || value)];
|
||||||
if (appsMatch) {
|
if (appsMatch) {
|
||||||
input.unshift(match?.params.query || '');
|
input.unshift(match?.params.query || '');
|
||||||
} else {
|
} else {
|
||||||
@ -93,8 +133,25 @@ export const Leap = React.forwardRef(({ menu, className }: LeapProps, ref) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
navigateByInput(input.join('/'));
|
navigateByInput(input.join('/'));
|
||||||
|
useLeapStore.setState({ rawInput: '' });
|
||||||
},
|
},
|
||||||
[searchInput, match]
|
[match]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
(e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if ((!selection && rawInput) || rawInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||||
|
e.preventDefault();
|
||||||
|
select(null, appsMatch ? undefined : match?.params.query);
|
||||||
|
const pathBack = createPreviousPath(match?.url || '');
|
||||||
|
push(pathBack);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[selection, rawInput, match]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -120,10 +177,11 @@ export const Leap = React.forwardRef(({ menu, className }: LeapProps, ref) => {
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder={selection ? '' : 'Search Landscape'}
|
placeholder={selection ? '' : 'Search Landscape'}
|
||||||
className="flex-1 w-full h-full px-2 h4 rounded-full bg-transparent outline-none"
|
className="flex-1 w-full h-full px-2 h4 rounded-full bg-transparent outline-none"
|
||||||
value={searchInput}
|
value={rawInput}
|
||||||
onClick={toggleSearch}
|
onClick={toggleSearch}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-controls="leap-items"
|
aria-controls="leap-items"
|
||||||
aria-expanded
|
aria-expanded
|
||||||
|
@ -1,15 +1,8 @@
|
|||||||
import { DialogContent } from '@radix-ui/react-dialog';
|
import { DialogContent } from '@radix-ui/react-dialog';
|
||||||
import * as Portal from '@radix-ui/react-portal';
|
import * as Portal from '@radix-ui/react-portal';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, {
|
import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
FunctionComponent,
|
import { Link, Route, Switch, useHistory } from 'react-router-dom';
|
||||||
KeyboardEvent,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from 'react';
|
|
||||||
import { Link, Route, Switch, useHistory, useLocation } from 'react-router-dom';
|
|
||||||
import create from 'zustand';
|
import create from 'zustand';
|
||||||
import { Dialog } from '../components/Dialog';
|
import { Dialog } from '../components/Dialog';
|
||||||
import { Help } from './Help';
|
import { Help } from './Help';
|
||||||
@ -19,6 +12,32 @@ import { Search } from './Search';
|
|||||||
import { SystemMenu } from './SystemMenu';
|
import { SystemMenu } from './SystemMenu';
|
||||||
import { SystemPreferences } from './SystemPreferences';
|
import { SystemPreferences } from './SystemPreferences';
|
||||||
|
|
||||||
|
export interface MatchItem {
|
||||||
|
value: string;
|
||||||
|
display?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LeapStore {
|
||||||
|
rawInput: string;
|
||||||
|
searchInput: string;
|
||||||
|
matches: MatchItem[];
|
||||||
|
selection: React.ReactNode;
|
||||||
|
select: (selection: React.ReactNode, input?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLeapStore = create<LeapStore>((set) => ({
|
||||||
|
rawInput: '',
|
||||||
|
searchInput: '',
|
||||||
|
matches: [],
|
||||||
|
selection: null,
|
||||||
|
select: (selection: React.ReactNode, input?: string) =>
|
||||||
|
set({
|
||||||
|
rawInput: input || '',
|
||||||
|
searchInput: input || '',
|
||||||
|
selection
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
export type MenuState =
|
export type MenuState =
|
||||||
| 'closed'
|
| 'closed'
|
||||||
| 'search'
|
| 'search'
|
||||||
@ -30,60 +49,17 @@ interface NavProps {
|
|||||||
menu?: MenuState;
|
menu?: MenuState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NavStore {
|
|
||||||
searchInput: string;
|
|
||||||
setSearchInput: (input: string) => void;
|
|
||||||
selection: React.ReactNode;
|
|
||||||
select: (selection: React.ReactNode, input?: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useNavStore = create<NavStore>((set) => ({
|
|
||||||
searchInput: '',
|
|
||||||
setSearchInput: (input: string) => set({ searchInput: input }),
|
|
||||||
selection: null,
|
|
||||||
select: (selection: React.ReactNode, input?: string) =>
|
|
||||||
set({ searchInput: input || '', selection })
|
|
||||||
}));
|
|
||||||
|
|
||||||
function normalizePathEnding(path: string) {
|
|
||||||
const end = path.length - 1;
|
|
||||||
return path[end] === '/' ? path.substring(0, end - 1) : path;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createNextPath(current: string, nextPart?: string): string {
|
|
||||||
let end = nextPart;
|
|
||||||
const parts = normalizePathEnding(current).split('/').reverse();
|
|
||||||
if (parts[1] === 'search') {
|
|
||||||
end = 'apps';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts[0] === 'leap') {
|
|
||||||
end = `search/${nextPart}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${current}/${end}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPreviousPath(current: string): string {
|
|
||||||
const parts = normalizePathEnding(current).split('/');
|
|
||||||
parts.pop();
|
|
||||||
|
|
||||||
if (parts[parts.length - 1] === 'leap') {
|
|
||||||
parts.push('search');
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Nav: FunctionComponent<NavProps> = ({ menu = 'closed' }) => {
|
export const Nav: FunctionComponent<NavProps> = ({ menu = 'closed' }) => {
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
const location = useLocation();
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const navRef = useRef<HTMLDivElement>(null);
|
const navRef = useRef<HTMLDivElement>(null);
|
||||||
const dialogNavRef = useRef<HTMLDivElement>(null);
|
const dialogNavRef = useRef<HTMLDivElement>(null);
|
||||||
const { searchInput, selection, select } = useNavStore();
|
const { selection, select } = useLeapStore((state) => ({
|
||||||
|
selection: state.selection,
|
||||||
|
select: state.select
|
||||||
|
}));
|
||||||
const [systemMenuOpen, setSystemMenuOpen] = useState(false);
|
const [systemMenuOpen, setSystemMenuOpen] = useState(false);
|
||||||
const [delayedOpen, setDelayedOpen] = useState(false);
|
const [dialogContentOpen, setDialogContentOpen] = useState(false);
|
||||||
|
|
||||||
const isOpen = menu !== 'closed';
|
const isOpen = menu !== 'closed';
|
||||||
const eitherOpen = isOpen || systemMenuOpen;
|
const eitherOpen = isOpen || systemMenuOpen;
|
||||||
@ -92,7 +68,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu = 'closed' }) => {
|
|||||||
(event: Event) => {
|
(event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
setDelayedOpen(true);
|
setDialogContentOpen(true);
|
||||||
if (menu === 'search' && inputRef.current) {
|
if (menu === 'search' && inputRef.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
@ -109,30 +85,17 @@ export const Nav: FunctionComponent<NavProps> = ({ menu = 'closed' }) => {
|
|||||||
const onDialogClose = useCallback((open: boolean) => {
|
const onDialogClose = useCallback((open: boolean) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
select(null);
|
select(null);
|
||||||
setDelayedOpen(false);
|
setDialogContentOpen(false);
|
||||||
push('/');
|
push('/');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onDialogKey = useCallback(
|
|
||||||
(e: KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if ((!selection && searchInput) || searchInput) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
||||||
e.preventDefault();
|
|
||||||
select(null);
|
|
||||||
const pathBack = createPreviousPath(location.pathname);
|
|
||||||
push(pathBack);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[selection, searchInput, location.pathname]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Portal.Root containerRef={delayedOpen ? dialogNavRef : navRef} className="flex space-x-2">
|
<Portal.Root
|
||||||
|
containerRef={dialogContentOpen ? dialogNavRef : navRef}
|
||||||
|
className="flex space-x-2"
|
||||||
|
>
|
||||||
<SystemMenu
|
<SystemMenu
|
||||||
open={systemMenuOpen}
|
open={systemMenuOpen}
|
||||||
setOpen={setSystemMenuOpen}
|
setOpen={setSystemMenuOpen}
|
||||||
@ -150,7 +113,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu = 'closed' }) => {
|
|||||||
ref={navRef}
|
ref={navRef}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'w-full max-w-3xl my-6 px-4 text-gray-400 font-semibold',
|
'w-full max-w-3xl my-6 px-4 text-gray-400 font-semibold',
|
||||||
delayedOpen && 'h-12'
|
dialogContentOpen && 'h-12'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Dialog open={isOpen} onOpenChange={onDialogClose}>
|
<Dialog open={isOpen} onOpenChange={onDialogClose}>
|
||||||
@ -158,20 +121,18 @@ export const Nav: FunctionComponent<NavProps> = ({ menu = 'closed' }) => {
|
|||||||
onOpenAutoFocus={onOpen}
|
onOpenAutoFocus={onOpen}
|
||||||
className="fixed top-0 left-[calc(50%-7.5px)] w-[calc(100%-15px)] max-w-3xl px-4 text-gray-400 -translate-x-1/2 outline-none"
|
className="fixed top-0 left-[calc(50%-7.5px)] w-[calc(100%-15px)] max-w-3xl px-4 text-gray-400 -translate-x-1/2 outline-none"
|
||||||
>
|
>
|
||||||
<div tabIndex={-1} onKeyDown={onDialogKey} role="presentation">
|
<header ref={dialogNavRef} className="my-6" />
|
||||||
<header ref={dialogNavRef} className="my-6" />
|
<div
|
||||||
<div
|
id="leap-items"
|
||||||
id="leap-items"
|
className="grid grid-rows-[fit-content(calc(100vh-7.5rem))] bg-white rounded-3xl overflow-hidden"
|
||||||
className="grid grid-rows-[fit-content(calc(100vh-7.5rem))] bg-white rounded-3xl overflow-hidden"
|
role="listbox"
|
||||||
role="listbox"
|
>
|
||||||
>
|
<Switch>
|
||||||
<Switch>
|
<Route path="/leap/notifications" component={Notifications} />
|
||||||
<Route path="/leap/notifications" component={Notifications} />
|
<Route path="/leap/system-preferences" component={SystemPreferences} />
|
||||||
<Route path="/leap/system-preferences" component={SystemPreferences} />
|
<Route path="/leap/help-and-support" component={Help} />
|
||||||
<Route path="/leap/help-and-support" component={Help} />
|
<Route path={['/leap/search', '/leap']} component={Search} />
|
||||||
<Route path={['/leap/search', '/leap']} component={Search} />
|
</Switch>
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useNavStore } from './Nav';
|
import { useLeapStore } from './Nav';
|
||||||
|
|
||||||
export const Notifications = () => {
|
export const Notifications = () => {
|
||||||
const select = useNavStore((state) => state.select);
|
const select = useLeapStore((state) => state.select);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
select('Notifications');
|
select('Notifications');
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useNavStore } from './Nav';
|
import { useLeapStore } from './Nav';
|
||||||
|
|
||||||
export const SystemPreferences = () => {
|
export const SystemPreferences = () => {
|
||||||
const select = useNavStore((state) => state.select);
|
const select = useLeapStore((state) => state.select);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
select('System Preferences');
|
select('System Preferences');
|
||||||
|
@ -7,10 +7,10 @@ import { Spinner } from '../../components/Spinner';
|
|||||||
import { TreatyMeta } from '../../components/TreatyMeta';
|
import { TreatyMeta } from '../../components/TreatyMeta';
|
||||||
import { useTreaty } from '../../logic/useTreaty';
|
import { useTreaty } from '../../logic/useTreaty';
|
||||||
import { chargesKey, fetchCharges } from '../../state/docket';
|
import { chargesKey, fetchCharges } from '../../state/docket';
|
||||||
import { useNavStore } from '../Nav';
|
import { useLeapStore } from '../Nav';
|
||||||
|
|
||||||
export const AppInfo = () => {
|
export const AppInfo = () => {
|
||||||
const select = useNavStore((state) => state.select);
|
const select = useLeapStore((state) => state.select);
|
||||||
const { ship, desk, treaty, installStatus, copyApp, installApp } = useTreaty();
|
const { ship, desk, treaty, installStatus, copyApp, installApp } = useTreaty();
|
||||||
const { data: charges } = useQuery(chargesKey(), fetchCharges);
|
const { data: charges } = useQuery(chargesKey(), fetchCharges);
|
||||||
const installed = (charges || {})[desk] || installStatus.isSuccess;
|
const installed = (charges || {})[desk] || installStatus.isSuccess;
|
||||||
|
@ -6,13 +6,13 @@ import slugify from 'slugify';
|
|||||||
import { ShipName } from '../../components/ShipName';
|
import { ShipName } from '../../components/ShipName';
|
||||||
import { fetchProviderTreaties, treatyKey } from '../../state/docket';
|
import { fetchProviderTreaties, treatyKey } from '../../state/docket';
|
||||||
import { Treaty } from '../../state/docket-types';
|
import { Treaty } from '../../state/docket-types';
|
||||||
import { useNavStore } from '../Nav';
|
import { useLeapStore } from '../Nav';
|
||||||
|
|
||||||
type AppsProps = RouteComponentProps<{ ship: string }>;
|
type AppsProps = RouteComponentProps<{ ship: string }>;
|
||||||
|
|
||||||
export const Apps = ({ match }: AppsProps) => {
|
export const Apps = ({ match }: AppsProps) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { searchInput, select } = useNavStore((state) => ({
|
const { searchInput, select } = useLeapStore((state) => ({
|
||||||
searchInput: state.searchInput,
|
searchInput: state.searchInput,
|
||||||
select: state.select
|
select: state.select
|
||||||
}));
|
}));
|
||||||
@ -38,6 +38,14 @@ export const Apps = ({ match }: AppsProps) => {
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
useLeapStore.setState({
|
||||||
|
matches: data.map((treaty) => ({ value: treaty.desk, display: treaty.title }))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
const preloadApp = useCallback(
|
const preloadApp = useCallback(
|
||||||
(app: Treaty) => {
|
(app: Treaty) => {
|
||||||
queryClient.setQueryData(treatyKey([provider, app.desk]), app);
|
queryClient.setQueryData(treatyKey([provider, app.desk]), app);
|
||||||
@ -58,7 +66,7 @@ export const Apps = ({ match }: AppsProps) => {
|
|||||||
{results && (
|
{results && (
|
||||||
<ul className="space-y-8" aria-labelledby="developed-by">
|
<ul className="space-y-8" aria-labelledby="developed-by">
|
||||||
{results.map((app) => (
|
{results.map((app) => (
|
||||||
<li key={app.desk}>
|
<li key={app.desk} role="option" aria-selected={false}>
|
||||||
<Link
|
<Link
|
||||||
to={`${match?.path.replace(':ship', provider)}/${slugify(app.desk)}`}
|
to={`${match?.path.replace(':ship', provider)}/${slugify(app.desk)}`}
|
||||||
className="flex items-center space-x-3 default-ring ring-offset-2 rounded-lg"
|
className="flex items-center space-x-3 default-ring ring-offset-2 rounded-lg"
|
||||||
|
@ -3,12 +3,12 @@ import { useQuery } from 'react-query';
|
|||||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||||
import { ShipName } from '../../components/ShipName';
|
import { ShipName } from '../../components/ShipName';
|
||||||
import { fetchProviders, providersKey } from '../../state/docket';
|
import { fetchProviders, providersKey } from '../../state/docket';
|
||||||
import { useNavStore } from '../Nav';
|
import { useLeapStore } from '../Nav';
|
||||||
|
|
||||||
type ProvidersProps = RouteComponentProps<{ ship: string }>;
|
type ProvidersProps = RouteComponentProps<{ ship: string }>;
|
||||||
|
|
||||||
export const Providers = ({ match }: ProvidersProps) => {
|
export const Providers = ({ match }: ProvidersProps) => {
|
||||||
const select = useNavStore((state) => state.select);
|
const select = useLeapStore((state) => state.select);
|
||||||
const provider = match?.params.ship;
|
const provider = match?.params.ship;
|
||||||
const { data } = useQuery(providersKey([provider]), () => fetchProviders(provider), {
|
const { data } = useQuery(providersKey([provider]), () => fetchProviders(provider), {
|
||||||
enabled: !!provider,
|
enabled: !!provider,
|
||||||
@ -20,6 +20,14 @@ export const Providers = ({ match }: ProvidersProps) => {
|
|||||||
select(null, provider);
|
select(null, provider);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
useLeapStore.setState({
|
||||||
|
matches: data.map((p) => ({ value: p.shipName, display: p.nickname }))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400" aria-live="polite">
|
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400" aria-live="polite">
|
||||||
<div id="providers">
|
<div id="providers">
|
||||||
@ -31,7 +39,7 @@ export const Providers = ({ match }: ProvidersProps) => {
|
|||||||
{data && (
|
{data && (
|
||||||
<ul className="space-y-8" aria-labelledby="providers">
|
<ul className="space-y-8" aria-labelledby="providers">
|
||||||
{data.map((p) => (
|
{data.map((p) => (
|
||||||
<li key={p.shipName}>
|
<li key={p.shipName} role="option" aria-selected={false}>
|
||||||
<Link
|
<Link
|
||||||
to={`${match?.path.replace(':ship', p.shipName)}/apps`}
|
to={`${match?.path.replace(':ship', p.shipName)}/apps`}
|
||||||
className="flex items-center space-x-3 default-ring ring-offset-2 rounded-lg"
|
className="flex items-center space-x-3 default-ring ring-offset-2 rounded-lg"
|
||||||
|
@ -98,7 +98,7 @@ export const treaties: Treaties = {
|
|||||||
'my-apps': {
|
'my-apps': {
|
||||||
title: 'My Apps',
|
title: 'My Apps',
|
||||||
ship: '~zod',
|
ship: '~zod',
|
||||||
desk: 'groups',
|
desk: 'my-apps',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
base: 'my-apps',
|
base: 'my-apps',
|
||||||
info: 'A lengthier description of the app down here',
|
info: 'A lengthier description of the app down here',
|
||||||
|
Loading…
Reference in New Issue
Block a user