leap: normalizing desk prop and fixing bugs

This commit is contained in:
Hunter Miller 2021-08-27 11:55:25 -05:00
parent 432ee1c548
commit 38b1576bcc
11 changed files with 95 additions and 70 deletions

View File

@ -1,4 +1,4 @@
import { chadIsRunning, Charge, Treaty } from '@urbit/api/docket';
import { chadIsRunning } from '@urbit/api/docket';
import clipboardCopy from 'clipboard-copy';
import React, { FC } from 'react';
import cn from 'classnames';
@ -8,19 +8,19 @@ import { Dialog, DialogClose, DialogContent, DialogTrigger } from './Dialog';
import { DocketHeader } from './DocketHeader';
import { Spinner } from './Spinner';
import { VatMeta } from './VatMeta';
import useDocketState from '../state/docket';
import useDocketState, { App } from '../state/docket';
import { getAppHref } from '../state/util';
import { addRecentApp } from '../nav/search/Home';
import { TreatyMeta } from './TreatyMeta';
type InstallStatus = 'uninstalled' | 'installing' | 'installed';
interface AppInfoProps {
docket: Charge | Treaty;
docket: App;
vat?: Vat;
className?: string;
}
function getInstallStatus(docket: Treaty | Charge): InstallStatus {
function getInstallStatus(docket: App): InstallStatus {
if (!('chad' in docket)) {
return 'uninstalled';
}
@ -33,7 +33,7 @@ function getInstallStatus(docket: Treaty | Charge): InstallStatus {
return 'uninstalled';
}
function getRemoteDesk(docket: Treaty | Charge, vat?: Vat) {
function getRemoteDesk(docket: App, vat?: Vat) {
if ('chad' in docket) {
const { ship, desk } = vat!.arak;
return [ship, desk];
@ -74,7 +74,7 @@ export const AppInfo: FC<AppInfoProps> = ({ docket, vat, className }) => {
variant="alt-primary"
as="a"
href={getAppHref(docket.href)}
target={docket.title || '_blank'}
target={docket.desk || '_blank'}
onClick={() => addRecentApp(docket)}
>
Open App

View File

@ -1,7 +1,7 @@
import classNames from 'classnames';
import React, { HTMLProps, ReactNode } from 'react';
import { Link, LinkProps } from 'react-router-dom';
import { Docket } from '@urbit/api';
import { App } from '../state/docket';
import { getAppHref } from '../state/util';
type Sizes = 'xs' | 'small' | 'default';
@ -12,11 +12,11 @@ type LinkOrAnchorProps = {
: never;
};
export type AppLinkProps<T extends Docket> = Omit<LinkOrAnchorProps, 'to'> & {
app: T;
export type AppLinkProps = Omit<LinkOrAnchorProps, 'to'> & {
app: App;
size?: Sizes;
selected?: boolean;
to?: (app: T) => LinkProps['to'] | undefined;
to?: (app: App) => 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 = <T extends Docket>({
export const AppLink = ({
app,
to,
size = 'default',
selected = false,
className,
...props
}: AppLinkProps<T>) => {
}: AppLinkProps) => {
const linkTo = to?.(app);
const linkClassnames = classNames(
'flex items-center default-ring ring-offset-2 rounded-lg',
@ -45,7 +45,7 @@ export const AppLink = <T extends Docket>({
{children}
</Link>
) : (
<a href={getAppHref(app.href)} className={linkClassnames} {...props}>
<a href={getAppHref(app.href)} target={app.desk} className={linkClassnames} {...props}>
{children}
</a>
);

View File

@ -11,7 +11,6 @@ import React, {
useRef
} from 'react';
import { Link, useHistory, useRouteMatch } from 'react-router-dom';
import slugify from 'slugify';
import { Cross } from '../components/icons/Cross';
import { MenuState, useLeapStore } from './Nav';
@ -70,7 +69,12 @@ export const Leap = React.forwardRef(({ menu, dropdown, showClose, className }:
const getMatch = useCallback(
(value: string) => {
return matches.find((m) => m.display?.startsWith(value) || m.value.startsWith(value));
const normValue = value.toLocaleLowerCase();
return matches.find(
(m) =>
m.display?.toLocaleLowerCase().startsWith(normValue) ||
m.value.toLocaleLowerCase().startsWith(normValue)
);
},
[matches]
);
@ -130,13 +134,25 @@ export const Leap = React.forwardRef(({ menu, dropdown, showClose, className }:
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const value = inputRef.current?.value.trim();
if (!value) {
if (selectedMatch?.href) {
window.open(selectedMatch.href, selectedMatch.value);
return;
}
const input = [getMatch(value)?.value || slugify(value)];
if (!selectedMatch?.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 {
@ -146,7 +162,7 @@ export const Leap = React.forwardRef(({ menu, dropdown, showClose, className }:
navigateByInput(input.join('/'));
useLeapStore.setState({ rawInput: '' });
},
[match]
[match, selectedMatch]
);
const onKeyDown = useCallback(
@ -156,7 +172,7 @@ export const Leap = React.forwardRef(({ menu, dropdown, showClose, className }:
if (deletion && !rawInput && selection) {
e.preventDefault();
select(null, appsMatch ? undefined : match?.params.query);
select(null, appsMatch && !appsMatch.isExact ? undefined : match?.params.query);
const pathBack = createPreviousPath(match?.url || '');
push(pathBack);
}

View File

@ -16,6 +16,7 @@ import { SystemPreferences } from './SystemPreferences';
export interface MatchItem {
value: string;
display?: string;
href?: string;
}
interface LeapStore {

View File

@ -2,19 +2,22 @@ import React, { useEffect, useMemo } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import fuzzy from 'fuzzy';
import slugify from 'slugify';
import { Docket } from '@urbit/api/docket';
import { ShipName } from '../../components/ShipName';
import useDocketState, { useAllyTreaties } from '../../state/docket';
import useDocketState, { App, useAllyTreaties } from '../../state/docket';
import { MatchItem, useLeapStore } from '../Nav';
import { AppList } from '../../components/AppList';
import { getAppHref } from '../../state/util';
type AppsProps = RouteComponentProps<{ ship: string }>;
export function appMatch(app: Docket): MatchItem {
// TODO: do we need display vs value here,
// will all apps have unique titles? If not,
// what would we use?
return { value: app.title, display: app.title };
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) => {
@ -25,7 +28,6 @@ export const Apps = ({ match }: AppsProps) => {
}));
const provider = match?.params.ship;
const treaties = useAllyTreaties(provider);
console.log(treaties);
const results = useMemo(() => {
if (!treaties) {
return undefined;
@ -34,7 +36,7 @@ export const Apps = ({ match }: AppsProps) => {
return fuzzy
.filter(
searchInput,
values.map((t) => t.title)
values.map((v) => v.title)
)
.sort((a, b) => {
const left = a.string.startsWith(searchInput) ? a.score + 1 : a.score;
@ -59,7 +61,7 @@ export const Apps = ({ match }: AppsProps) => {
useEffect(() => {
if (results) {
useLeapStore.setState({
matches: results.map(appMatch)
matches: results.map((r) => appMatch(r))
});
}
}, [results]);

View File

@ -3,7 +3,7 @@ import create from 'zustand';
import React, { useEffect } from 'react';
import { persist } from 'zustand/middleware';
import { take } from 'lodash-es';
import { Docket, Provider } from '@urbit/api';
import { Provider } from '@urbit/api';
import { MatchItem, useLeapStore } from '../Nav';
import { appMatch } from './Apps';
import { providerMatch } from './Providers';
@ -12,12 +12,12 @@ import { ProviderList } from '../../components/ProviderList';
import { AppLink } from '../../components/AppLink';
import { ShipName } from '../../components/ShipName';
import { ProviderLink } from '../../components/ProviderLink';
import { useCharges } from '../../state/docket';
import { App, useCharges } from '../../state/docket';
interface RecentsStore {
recentApps: Docket[];
recentApps: App[];
recentDevs: Provider[];
addRecentApp: (docket: Docket) => void;
addRecentApp: (app: App) => void;
addRecentDev: (dev: Provider) => void;
}
@ -26,12 +26,12 @@ export const useRecentsStore = create<RecentsStore>(
(set) => ({
recentApps: [],
recentDevs: [],
addRecentApp: (docket) => {
addRecentApp: (app) => {
set(
produce((draft: RecentsStore) => {
const hasApp = draft.recentApps.find((app) => app.title === docket.title);
const hasApp = draft.recentApps.find((a) => a.desk === app.desk);
if (!hasApp) {
draft.recentApps.unshift(docket);
draft.recentApps.unshift(app);
}
draft.recentApps = take(draft.recentApps, 3);
@ -64,8 +64,8 @@ export function addRecentDev(dev: Provider) {
return useRecentsStore.getState().addRecentDev(dev);
}
export function addRecentApp(docket: Docket) {
return useRecentsStore.getState().addRecentApp(docket);
export function addRecentApp(app: App) {
return useRecentsStore.getState().addRecentApp(app);
}
export const Home = () => {
@ -76,7 +76,7 @@ export const Home = () => {
const zod = { shipName: '~zod' };
useEffect(() => {
const apps = recentApps.map(appMatch);
const apps = recentApps.map((app) => appMatch(app, true));
const devs = recentDevs.map(providerMatch);
useLeapStore.setState({
@ -93,7 +93,13 @@ export const Home = () => {
<div className="min-h-[150px] p-6 rounded-xl bg-gray-100">
<p className="mb-4">Apps you use will be listed here, in the order you used them.</p>
<p className="mb-6">You can click/tap/keyboard on a listed app to open it.</p>
{groups && <AppLink app={groups} size="small" onClick={() => addRecentApp(groups)} />}
{groups && (
<AppLink
app={groups}
size="small"
onClick={() => addRecentApp({ ...groups, desk: 'groups' })}
/>
)}
</div>
)}
{recentApps.length > 0 && (

View File

@ -17,10 +17,7 @@ export function providerMatch(provider: Provider | string): MatchItem {
}
export const Providers = ({ match }: ProvidersProps) => {
const { selectedMatch, select } = useLeapStore((state) => ({
select: state.select,
selectedMatch: state.selectedMatch
}));
const selectedMatch = useLeapStore((state) => state.selectedMatch);
const provider = match?.params.ship;
const allies = useAllies();
const search = provider || '';
@ -43,11 +40,6 @@ export const Providers = ({ match }: ProvidersProps) => {
[allies, search]
);
const count = results?.length;
const ally = match?.params.ship;
useEffect(() => {
select(null, ally);
}, [ally]);
useEffect(() => {
if (results) {

View File

@ -1,11 +1,10 @@
import create from 'zustand';
import produce from 'immer';
import { useCallback, useEffect } from 'react';
import { mapValues, omit, pick } from 'lodash-es';
import { omit, pick } from 'lodash-es';
import {
Allies,
Charge,
Charges,
ChargeUpdateInitial,
scryAllies,
scryAllyTreaties,
@ -24,8 +23,18 @@ import api from './api';
import { mockAllies, mockCharges, mockTreaties } from './mock-data';
import { fakeRequest, useMockData } from './util';
export interface ChargeWithDesk extends Charge {
desk: string;
}
export interface ChargesWithDesks {
[ref: string]: ChargeWithDesk;
}
export type App = Treaty | ChargeWithDesk;
interface DocketState {
charges: Charges;
charges: ChargesWithDesks;
treaties: Treaties;
allies: Allies;
fetchCharges: () => Promise<void>;
@ -43,9 +52,9 @@ const useDocketState = create<DocketState>((set, get) => ({
? await fakeRequest(mockCharges)
: (await api.scry<ChargeUpdateInitial>(scryCharges)).initial;
const charges = Object.entries(charg).reduce((obj: Charges, [key, value]) => {
const charges = Object.entries(charg).reduce((obj: ChargesWithDesks, [key, value]) => {
// eslint-disable-next-line no-param-reassign
obj[key] = normalizeDocket<Charge>(value);
obj[key] = normalizeDocket(value as ChargeWithDesk, key);
return obj;
}, {});
@ -60,7 +69,7 @@ const useDocketState = create<DocketState>((set, get) => ({
let treaties = useMockData
? mockTreaties
: (await api.scry<TreatyUpdateIni>(scryAllyTreaties(ally))).ini;
treaties = mapValues(treaties, normalizeDocket);
treaties = normalizeDockets(treaties);
set((s) => ({ treaties: { ...s.treaties, ...treaties } }));
return treaties;
},
@ -77,7 +86,7 @@ const useDocketState = create<DocketState>((set, get) => ({
}
const result = await api.subscribeOnce('docket', `/treaty/${key}`, 20000);
const treaty = { ...normalizeDocket(result), ship, desk };
const treaty = { ...normalizeDocket(result, desk), ship };
set((state) => ({
treaties: { ...state.treaties, [key]: treaty }
}));
@ -134,13 +143,14 @@ const useDocketState = create<DocketState>((set, get) => ({
set
}));
function normalizeDocket<T extends Docket>(docket: T): T {
function normalizeDocket<T extends Docket>(docket: T, desk: string): T {
const color = docket?.color?.startsWith('#')
? docket.color
: `#${docket.color.slice(2).replace('.', '')}`.toUpperCase();
return {
...docket,
desk,
color
};
}
@ -148,13 +158,13 @@ function normalizeDocket<T extends Docket>(docket: T): T {
function normalizeDockets<T extends Docket>(dockets: Record<string, T>): Record<string, T> {
return Object.entries(dockets).reduce((obj: Record<string, T>, [key, value]) => {
// eslint-disable-next-line no-param-reassign
obj[key] = normalizeDocket(value);
obj[key] = normalizeDocket(value, key);
return obj;
}, {});
}
function addCharge(state: DocketState, desk: string, charge: Charge) {
return { charges: { ...state.charges, [desk]: normalizeDocket(charge) } };
return { charges: { ...state.charges, [desk]: normalizeDocket(charge as ChargeWithDesk, desk) } };
}
function delCharge(state: DocketState, desk: string) {

View File

@ -30,7 +30,6 @@ export const useKilnState = create<KilnState>((set) => ({
},
set: produce(set)
}));
console.log(useKilnState.getState());
const selBlockers = (s: KilnState) => getBlockers(s.vats);
export function useBlockers() {

View File

@ -180,7 +180,7 @@ export const mockVat = (desk: string, blockers?: boolean): Vat => ({
hash: '0vh.lhfn6.julg1.fs52d.g2lqj.q5kp0.2o7j3.2bljl.jdm34.hd46v.9uv5v'
});
const badVats = []; // ['inbox', 'system', 'terminal', 'base'];
const badVats = ['inbox', 'system', 'terminal', 'base'];
export const mockVats = _.reduce(
mockCharges,
(vats, charge, desk) => {
@ -188,5 +188,3 @@ export const mockVats = _.reduce(
},
{ base: mockVat('base', true) } as Vats
);
console.log(mockVats);

View File

@ -1,14 +1,15 @@
import classNames from 'classnames';
import React, { FunctionComponent } from 'react';
import { darken, hsla, lighten, parseToHsla, readableColorIsBlack } from 'color2k';
import { chadIsRunning, Charge } from '@urbit/api/docket';
import { chadIsRunning } from '@urbit/api/docket';
import { TileMenu } from './TileMenu';
import { Spinner } from '../components/Spinner';
import { getAppHref } from '../state/util';
import { useRecentsStore } from '../nav/search/Home';
import { ChargeWithDesk } from '../state/docket';
type TileProps = {
charge: Charge;
charge: ChargeWithDesk;
desk: string;
};
@ -40,7 +41,7 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
href={active ? link : undefined}
target={desk}
className={classNames(
'group relative font-semibold aspect-w-1 aspect-h-1 rounded-3xl default-ring',
'group relative font-semibold aspect-w-1 aspect-h-1 rounded-3xl default-ring overflow-hidden',
!active && 'cursor-default'
)}
style={{ backgroundColor: active ? color || 'purple' : suspendColor }}
@ -61,7 +62,7 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
className="absolute z-10 top-2.5 right-2.5 sm:top-4 sm:right-4 opacity-0 hover-none:opacity-100 focus:opacity-100 group-hover:opacity-100"
/>
)}
<div className="h4 absolute bottom-4 left-4 lg:bottom-8 lg:left-8">
<div className="h4 absolute z-10 bottom-4 left-4 lg:bottom-8 lg:left-8">
<h3
className={`${
lightText && active && !loading ? 'text-gray-200' : 'text-gray-800'
@ -75,7 +76,7 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
</div>
{image && !loading && (
<img
className="absolute top-1/2 left-1/2 h-[40%] w-[40%] object-contain transform -translate-x-1/2 -translate-y-1/2"
className="absolute top-1/2 left-1/2 h-full w-full object-contain transform -translate-x-1/2 -translate-y-1/2"
src={image}
alt=""
/>