leap: making leap transitions more direct

This commit is contained in:
Hunter Miller 2021-08-31 12:57:11 -05:00
parent 967a074734
commit 0b22ee3bb5
14 changed files with 112 additions and 102 deletions

1
pkg/grid/.gitignore vendored
View File

@ -6,3 +6,4 @@ dist-ssr
stats.html
.eslintcache
.vercel
.env

Binary file not shown.

View File

@ -41,6 +41,7 @@
"@tailwindcss/aspect-ratio": "^0.2.1",
"@types/lodash-es": "^4.17.4",
"@types/mousetrap": "^1.6.8",
"@types/node": "^16.7.9",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.8",

View File

@ -1,7 +1,7 @@
import classNames from 'classnames';
import React, { HTMLProps, ReactNode } from 'react';
import { Link, LinkProps } from 'react-router-dom';
import { App } from '../state/docket';
import { DocketWithDesk } from '../state/docket';
import { getAppHref } from '../state/util';
type Sizes = 'xs' | 'small' | 'default';
@ -12,11 +12,11 @@ type LinkOrAnchorProps = {
: never;
};
export type AppLinkProps = Omit<LinkOrAnchorProps, 'to'> & {
app: App;
export type AppLinkProps<T extends DocketWithDesk> = Omit<LinkOrAnchorProps, 'to'> & {
app: T;
size?: Sizes;
selected?: boolean;
to?: (app: App) => LinkProps['to'] | undefined;
to?: (app: T) => LinkProps['to'] | undefined;
};
const sizeMap: Record<Sizes, string> = {
@ -25,14 +25,14 @@ const sizeMap: Record<Sizes, string> = {
default: 'w-12 h-12 mr-3 rounded-lg'
};
export const AppLink = ({
export const AppLink = <T extends DocketWithDesk>({
app,
to,
size = 'default',
selected = false,
className,
...props
}: AppLinkProps) => {
}: AppLinkProps<T>) => {
const linkTo = to?.(app);
const linkClassnames = classNames(
'flex items-center default-ring ring-offset-2 rounded-lg',

View File

@ -1,28 +1,28 @@
import React, { MouseEvent, useCallback } from 'react';
import { Docket } from '@urbit/api';
import classNames from 'classnames';
import { MatchItem } from '../nav/Nav';
import { useRecentsStore } from '../nav/search/Home';
import { AppLink, AppLinkProps } from './AppLink';
import { DocketWithDesk } from '../state/docket';
type AppListProps<T extends Docket> = {
type AppListProps<T extends DocketWithDesk> = {
apps: T[];
labelledBy: string;
matchAgainst?: MatchItem;
onClick?: (e: MouseEvent<HTMLAnchorElement>, app: Docket) => void;
onClick?: (e: MouseEvent<HTMLAnchorElement>, app: T) => void;
listClass?: string;
} & Omit<AppLinkProps<T>, 'app' | 'onClick'>;
export function appMatches(target: Docket, match?: MatchItem): boolean {
export function appMatches(target: DocketWithDesk, match?: MatchItem): boolean {
if (!match) {
return false;
}
const matchValue = match.display || match.value;
return target.title === matchValue; // TODO: need desk name or something || target.href === matchValue;
return target.title === matchValue || target.desk === matchValue;
}
export const AppList = <T extends Docket>({
export const AppList = <T extends DocketWithDesk>({
apps,
labelledBy,
matchAgainst,
@ -32,7 +32,7 @@ export const AppList = <T extends Docket>({
...props
}: AppListProps<T>) => {
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
const selected = useCallback((app: Docket) => appMatches(app, matchAgainst), [matchAgainst]);
const selected = useCallback((app: T) => appMatches(app, matchAgainst), [matchAgainst]);
return (
<ul
@ -53,9 +53,7 @@ export const AppList = <T extends Docket>({
selected={selected(app)}
onClick={(e) => {
addRecentApp(app);
if (onClick) {
onClick(e, app);
}
onClick?.(e, app);
}}
/>
</li>

View File

@ -7,7 +7,8 @@ import React, {
HTMLAttributes,
useCallback,
useImperativeHandle,
useRef
useRef,
useEffect
} from 'react';
import { Link, useHistory, useRouteMatch } from 'react-router-dom';
import { Cross } from '../components/icons/Cross';
@ -46,6 +47,12 @@ export const Leap = React.forwardRef(({ menu, dropdown, showClose, className }:
useImperativeHandle(ref, () => inputRef.current);
const { rawInput, selectedMatch, matches, selection, select } = useLeapStore();
useEffect(() => {
if (selection && rawInput === '') {
inputRef.current?.focus();
}
}, [selection, rawInput]);
const toggleSearch = useCallback(() => {
if (selection || menu === 'search') {
return;
@ -133,32 +140,19 @@ export const Leap = React.forwardRef(({ menu, dropdown, showClose, className }:
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (selectedMatch?.href) {
window.open(selectedMatch.href, selectedMatch.value);
const value = inputRef.current?.value.trim();
const currentMatch = selectedMatch || (value && getMatch(value));
if (!currentMatch) {
return;
}
if (!selectedMatch?.value) {
if (currentMatch?.openInNewTab) {
window.open(currentMatch.url, currentMatch.value);
return;
}
// TODO: evaluate if we still need this for manual entry
// const value = inputRef.current?.value.trim();
// if (!value) {
// return;
// }
// const input = [getMatch(value)?.value || slugify(value)];
const input = [selectedMatch.value];
if (appsMatch) {
input.unshift(match?.params.query || '');
} else {
input.push('');
}
navigateByInput(input.join('/'));
push(currentMatch.url);
useLeapStore.setState({ rawInput: '' });
},
[match, selectedMatch]

View File

@ -14,9 +14,10 @@ import { SystemMenu } from './SystemMenu';
import { SystemPreferences } from './SystemPreferences';
export interface MatchItem {
url: string;
openInNewTab: boolean;
value: string;
display?: string;
href?: string;
}
interface LeapStore {

View File

@ -26,25 +26,25 @@ export const usePreferencesStore = create<PreferencesStore>((set) => ({
*/
toggleOTAs: async () => {
if (useMockData) {
await fakeRequest();
await fakeRequest({});
set((state) => ({ otasEnabled: !state.otasEnabled }));
}
},
setOTASource: async (source: string) => {
if (useMockData) {
await fakeRequest();
await fakeRequest({});
set({ otaSource: source });
}
},
toggleDoNotDisturb: async () => {
if (useMockData) {
await fakeRequest();
await fakeRequest({});
set((state) => ({ doNotDisturb: !state.doNotDisturb }));
}
},
toggleMentions: async () => {
if (useMockData) {
await fakeRequest();
await fakeRequest({});
set((state) => ({ mentions: !state.mentions }));
}
}

View File

@ -1,25 +1,14 @@
import React, { useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import fuzzy from 'fuzzy';
import slugify from 'slugify';
import { Treaty } from '@urbit/api';
import { ShipName } from '../../components/ShipName';
import useDocketState, { App, useAllyTreaties } from '../../state/docket';
import { MatchItem, useLeapStore } from '../Nav';
import useDocketState, { useAllyTreaties } from '../../state/docket';
import { useLeapStore } from '../Nav';
import { AppList } from '../../components/AppList';
import { getAppHref } from '../../state/util';
type AppsProps = RouteComponentProps<{ ship: string }>;
export function appMatch(app: App, includeHref = false): MatchItem {
const match: MatchItem = { value: app.desk, display: app.title };
if (includeHref) {
match.href = getAppHref(app.href);
}
return match;
}
export const Apps = ({ match }: AppsProps) => {
const { searchInput, selectedMatch, select } = useLeapStore((state) => ({
searchInput: state.searchInput,
@ -48,6 +37,11 @@ export const Apps = ({ match }: AppsProps) => {
}, [treaties, searchInput]);
const count = results?.length;
const getAppPath = useCallback(
(app: Treaty) => `${match?.path.replace(':ship', provider)}/${app.ship}/${app.desk}`,
[match]
);
useEffect(() => {
const { fetchAllyTreaties } = useDocketState.getState();
fetchAllyTreaties(provider);
@ -61,7 +55,12 @@ export const Apps = ({ match }: AppsProps) => {
useEffect(() => {
if (results) {
useLeapStore.setState({
matches: results.map((r) => appMatch(r))
matches: results.map((r) => ({
url: getAppPath(r),
openInNewTab: false,
value: r.desk,
display: r.title
}))
});
}
}, [results]);
@ -87,7 +86,7 @@ export const Apps = ({ match }: AppsProps) => {
apps={results}
labelledBy="developed-by"
matchAgainst={selectedMatch}
to={(app) => `${match?.path.replace(':ship', provider)}/${app.ship}/${slugify(app.desk)}`}
to={getAppPath}
/>
)}
<p>That&apos;s it!</p>

View File

@ -5,19 +5,19 @@ import { persist } from 'zustand/middleware';
import { take } from 'lodash-es';
import { Provider } from '@urbit/api';
import { MatchItem, useLeapStore } from '../Nav';
import { appMatch } from './Apps';
import { providerMatch } from './Providers';
import { AppList } from '../../components/AppList';
import { ProviderList } from '../../components/ProviderList';
import { AppLink } from '../../components/AppLink';
import { ShipName } from '../../components/ShipName';
import { ProviderLink } from '../../components/ProviderLink';
import { App, useCharges } from '../../state/docket';
import { DocketWithDesk, useCharges } from '../../state/docket';
import { getAppHref } from '../../state/util';
interface RecentsStore {
recentApps: App[];
recentApps: DocketWithDesk[];
recentDevs: Provider[];
addRecentApp: (app: App) => void;
addRecentApp: (app: DocketWithDesk) => void;
addRecentDev: (dev: Provider) => void;
}
@ -64,7 +64,7 @@ export function addRecentDev(dev: Provider) {
return useRecentsStore.getState().addRecentDev(dev);
}
export function addRecentApp(app: App) {
export function addRecentApp(app: DocketWithDesk) {
return useRecentsStore.getState().addRecentApp(app);
}
@ -76,7 +76,12 @@ export const Home = () => {
const zod = { shipName: '~zod' };
useEffect(() => {
const apps = recentApps.map((app) => appMatch(app, true));
const apps = recentApps.map((app) => ({
url: getAppHref(app.href),
openInNewTab: true,
value: app.desk,
display: app.title
}));
const devs = recentDevs.map(providerMatch);
useLeapStore.setState({

View File

@ -9,11 +9,15 @@ import { ProviderList } from '../../components/ProviderList';
type ProvidersProps = RouteComponentProps<{ ship: string }>;
export function providerMatch(provider: Provider | string): MatchItem {
if (typeof provider === 'string') {
return { value: provider, display: provider };
}
const value = typeof provider === 'string' ? provider : provider.shipName;
const display = typeof provider === 'string' ? undefined : provider.nickname;
return { value: provider.shipName, display: provider.nickname };
return {
value,
display,
url: `/leap/search/${value}/apps`,
openInNewTab: false
};
}
export const Providers = ({ match }: ProvidersProps) => {

View File

@ -31,7 +31,9 @@ export interface ChargesWithDesks {
[ref: string]: ChargeWithDesk;
}
export type App = Treaty | ChargeWithDesk;
export interface DocketWithDesk extends Docket {
desk: string;
}
interface DocketState {
charges: ChargesWithDesks;

View File

@ -8,7 +8,7 @@ export function makeKeyFn(key: string) {
export const useMockData = import.meta.env.MODE === 'mock';
export async function fakeRequest<T>(data?: any, time = 300): Promise<T> {
export async function fakeRequest<T>(data: T, time = 300): Promise<T> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(data);

View File

@ -1,4 +1,4 @@
import { defineConfig } from 'vite';
import { loadEnv, defineConfig } from 'vite';
import analyze from 'rollup-plugin-analyzer';
import { visualizer } from 'rollup-plugin-visualizer';
import reactRefresh from '@vitejs/plugin-react-refresh';
@ -7,36 +7,41 @@ import htmlPlugin from 'vite-plugin-html-config';
const htmlPluginOpt = {
headScripts: [{ src: '/apps/grid/desk.js' }, { src: '/session.js' }]
};
const SHIP_URL = process.env.SHIP_URL || 'http://localhost:8080';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
base: mode === 'mock' ? undefined : '/apps/grid/',
server:
mode === 'mock'
? undefined
: {
proxy: {
'^/apps/grid/desk.js': {
target: SHIP_URL
},
'^((?!/apps/grid).)*$': {
target: SHIP_URL
export default ({ mode }) => {
Object.assign(process.env, loadEnv(mode, process.cwd()));
const SHIP_URL = process.env.SHIP_URL || process.env.VITE_SHIP_URL || 'http://localhost:8080';
console.log(SHIP_URL);
return defineConfig({
base: mode === 'mock' ? undefined : '/apps/grid/',
server:
mode === 'mock'
? undefined
: {
proxy: {
'^/apps/grid/desk.js': {
target: SHIP_URL
},
'^((?!/apps/grid).)*$': {
target: SHIP_URL
}
}
}
},
build:
mode !== 'profile'
? undefined
: {
rollupOptions: {
plugins: [
analyze({
limit: 20
}),
visualizer()
]
}
},
plugins: [htmlPlugin(htmlPluginOpt), reactRefresh()]
}));
},
build:
mode !== 'profile'
? undefined
: {
rollupOptions: {
plugins: [
analyze({
limit: 20
}),
visualizer()
]
}
},
plugins: [htmlPlugin(htmlPluginOpt), reactRefresh()]
});
};