mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-11-28 11:40:11 +03:00
Merge pull request #5187 from urbit/hm/grid-system-update-flow
grid: stateful notification link and leap bug fixes
This commit is contained in:
commit
f816b9af40
6704
pkg/grid/package-lock.json
generated
6704
pkg/grid/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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,14 +45,14 @@ 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>
|
||||
);
|
||||
return link(
|
||||
<>
|
||||
<div
|
||||
className={classNames('flex-none relative bg-gray-200 rounded-lg', sizeMap[size])}
|
||||
className={classNames('flex-none relative bg-gray-200', sizeMap[size])}
|
||||
style={{ backgroundColor: app.color }}
|
||||
>
|
||||
{app.image && (
|
||||
|
@ -1,5 +1,6 @@
|
||||
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';
|
||||
@ -27,18 +28,28 @@ export const AppList = <T extends Docket>({
|
||||
matchAgainst,
|
||||
onClick,
|
||||
listClass = 'space-y-8',
|
||||
size = 'default',
|
||||
...props
|
||||
}: AppListProps<T>) => {
|
||||
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
|
||||
const selected = useCallback((app: Docket) => appMatches(app, matchAgainst), [matchAgainst]);
|
||||
|
||||
return (
|
||||
<ul className={listClass} aria-labelledby={labelledBy}>
|
||||
<ul
|
||||
className={classNames(
|
||||
size === 'default' && 'space-y-8',
|
||||
size === 'small' && 'space-y-4',
|
||||
size === 'xs' && 'space-y-2',
|
||||
listClass
|
||||
)}
|
||||
aria-labelledby={labelledBy}
|
||||
>
|
||||
{apps.map((app) => (
|
||||
<li key={app.title} id={app.title} role="option" aria-selected={selected(app)}>
|
||||
<AppLink
|
||||
{...props}
|
||||
app={app}
|
||||
size={size}
|
||||
selected={selected(app)}
|
||||
onClick={(e) => {
|
||||
addRecentApp(app);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { MouseEvent, useCallback } from 'react';
|
||||
import { Provider } from '@urbit/api';
|
||||
import classNames from 'classnames';
|
||||
import { MatchItem } from '../nav/Nav';
|
||||
import { useRecentsStore } from '../nav/search/Home';
|
||||
import { ProviderLink, ProviderLinkProps } from './ProviderLink';
|
||||
@ -9,6 +10,7 @@ export type ProviderListProps = {
|
||||
labelledBy: string;
|
||||
matchAgainst?: MatchItem;
|
||||
onClick?: (e: MouseEvent<HTMLAnchorElement>, p: Provider) => void;
|
||||
listClass?: string;
|
||||
} & Omit<ProviderLinkProps, 'provider' | 'onClick'>;
|
||||
|
||||
export function providerMatches(target: Provider, match?: MatchItem): boolean {
|
||||
@ -25,6 +27,8 @@ export const ProviderList = ({
|
||||
labelledBy,
|
||||
matchAgainst,
|
||||
onClick,
|
||||
listClass,
|
||||
small = false,
|
||||
...props
|
||||
}: ProviderListProps) => {
|
||||
const addRecentDev = useRecentsStore((state) => state.addRecentDev);
|
||||
@ -34,11 +38,15 @@ export const ProviderList = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<ul className="space-y-8" aria-labelledby={labelledBy}>
|
||||
<ul
|
||||
className={classNames(small ? 'space-y-4' : 'space-y-8', listClass)}
|
||||
aria-labelledby={labelledBy}
|
||||
>
|
||||
{providers.map((p) => (
|
||||
<li key={p.shipName} id={p.shipName} role="option" aria-selected={selected(p)}>
|
||||
<ProviderLink
|
||||
{...props}
|
||||
small={small}
|
||||
provider={p}
|
||||
selected={selected(p)}
|
||||
onClick={(e) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Treaty, daToDate } from '@urbit/api';
|
||||
import { Treaty } from '@urbit/api';
|
||||
|
||||
import moment from 'moment';
|
||||
import { Attribute } from './Attribute';
|
||||
@ -9,14 +9,13 @@ const meta = ['license', 'website', 'version'] as const;
|
||||
export function TreatyMeta(props: { treaty: Treaty }) {
|
||||
const { treaty } = props;
|
||||
const { desk, ship, cass } = treaty;
|
||||
console.log(cass.da);
|
||||
return (
|
||||
<div className="mt-5 sm:mt-8 space-y-5 sm:space-y-8">
|
||||
<Attribute title="Developer Desk" attr="desk">
|
||||
{ship}/{desk}
|
||||
</Attribute>
|
||||
<Attribute title="Last Software Update" attr="case">
|
||||
{moment(daToDate(cass.da as unknown as string)).format('YYYY.MM.DD')}
|
||||
{moment(cass.da).format('YYYY.MM.DD')}
|
||||
</Attribute>
|
||||
{meta.map((d) => (
|
||||
<Attribute key={d} attr={d}>
|
||||
|
@ -2,6 +2,6 @@ import React from 'react';
|
||||
|
||||
export const Bullet = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 16 16">
|
||||
<circle cx="8" cy="8" r="3" />
|
||||
<circle className="fill-current" cx="8" cy="8" r="3" />
|
||||
</svg>
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -2,12 +2,13 @@ import { DialogContent } from '@radix-ui/react-dialog';
|
||||
import * as Portal from '@radix-ui/react-portal';
|
||||
import classNames from 'classnames';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Link, Route, Switch, useHistory } from 'react-router-dom';
|
||||
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||
import create from 'zustand';
|
||||
import { Dialog } from '../components/Dialog';
|
||||
import { Help } from './Help';
|
||||
import { Leap } from './Leap';
|
||||
import { Notifications } from './Notifications';
|
||||
import { NotificationsLink } from './NotificationsLink';
|
||||
import { Search } from './Search';
|
||||
import { SystemMenu } from './SystemMenu';
|
||||
import { SystemPreferences } from './SystemPreferences';
|
||||
@ -15,6 +16,7 @@ import { SystemPreferences } from './SystemPreferences';
|
||||
export interface MatchItem {
|
||||
value: string;
|
||||
display?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface LeapStore {
|
||||
@ -141,12 +143,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
showOverlay={!isOpen}
|
||||
className={classNames('relative z-50 flex-none', eitherOpen ? 'bg-white' : 'bg-gray-100')}
|
||||
/>
|
||||
<Link
|
||||
to="/leap/notifications"
|
||||
className="relative z-50 flex-none circle-button bg-blue-400 text-white h4"
|
||||
>
|
||||
3
|
||||
</Link>
|
||||
<NotificationsLink isOpen={isOpen} />
|
||||
<Leap
|
||||
ref={inputRef}
|
||||
menu={menuState}
|
||||
@ -170,7 +167,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
<DialogContent
|
||||
onOpenAutoFocus={onOpen}
|
||||
onInteractOutside={disableCloseWhenDropdownOpen}
|
||||
className="fixed bottom-0 sm:top-0 scroll-left-50 flex flex-col scroll-full-width max-w-3xl px-4 text-gray-400 -translate-x-1/2 outline-none"
|
||||
className="fixed bottom-0 sm:top-0 scroll-left-50 flex flex-col scroll-full-width max-w-3xl px-4 pb-4 text-gray-400 -translate-x-1/2 outline-none"
|
||||
role="combobox"
|
||||
aria-controls="leap-items"
|
||||
aria-owns="leap-items"
|
||||
@ -179,7 +176,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
<header ref={dialogNavRef} className="my-6 order-last sm:order-none" />
|
||||
<div
|
||||
id="leap-items"
|
||||
className="grid grid-rows-[fit-content(calc(100vh-7.5rem))] bg-white rounded-3xl overflow-hidden default-ring"
|
||||
className="grid grid-rows-[fit-content(100vh)] bg-white rounded-3xl overflow-hidden default-ring"
|
||||
tabIndex={0}
|
||||
role="listbox"
|
||||
>
|
||||
|
@ -1,15 +1,14 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useLeapStore } from './Nav';
|
||||
import { useBlockers, useLag } from '../state/kiln';
|
||||
import { Button } from '../components/Button';
|
||||
import { useHarkStore } from '../state/hark';
|
||||
import { Notification } from '../state/hark-types';
|
||||
import { BasicNotification } from './notifications/BasicNotification';
|
||||
import {
|
||||
BaseBlockedNotification,
|
||||
RuntimeLagNotification
|
||||
} from './notifications/SystemNotification';
|
||||
import { useNotifications } from '../state/notifications';
|
||||
|
||||
function renderNotification(notification: Notification, key: string) {
|
||||
if (notification.type === 'system-updates-blocked') {
|
||||
@ -27,31 +26,16 @@ const Empty = () => (
|
||||
</section>
|
||||
);
|
||||
|
||||
function getSystemNotifications(lag: boolean, blockers: string[]) {
|
||||
const nots = [] as Notification[];
|
||||
if (lag) {
|
||||
nots.push({ type: 'runtime-lag' });
|
||||
}
|
||||
if (blockers.length > 0) {
|
||||
nots.push({ type: 'system-updates-blocked', desks: blockers });
|
||||
}
|
||||
return nots;
|
||||
}
|
||||
|
||||
export const Notifications = () => {
|
||||
const select = useLeapStore((s) => s.select);
|
||||
const notifications = useHarkStore((s) => s.notifications);
|
||||
const blockers = useBlockers();
|
||||
const lag = useLag();
|
||||
const systemNotifications = getSystemNotifications(lag, blockers);
|
||||
const hasNotifications = notifications.length > 0 || systemNotifications.length > 0;
|
||||
const { notifications, systemNotifications, hasAnyNotifications } = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
select('Notifications');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-8">
|
||||
<div className="grid grid-rows-[auto,1fr] h-full p-4 md:p-8 overflow-hidden">
|
||||
<header className="space-x-2 mb-8">
|
||||
<Button variant="secondary" className="py-1.5 px-6 rounded-full">
|
||||
Mark All as Read
|
||||
@ -66,9 +50,9 @@ export const Notifications = () => {
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
{!hasNotifications && <Empty />}
|
||||
{hasNotifications && (
|
||||
<section className="min-h-[480px] text-gray-400 space-y-2 overflow-y-auto">
|
||||
{!hasAnyNotifications && <Empty />}
|
||||
{hasAnyNotifications && (
|
||||
<section className="text-gray-400 space-y-2 overflow-y-auto">
|
||||
{notifications.map((n, index) => renderNotification(n, index.toString()))}
|
||||
{systemNotifications.map((n, index) =>
|
||||
renderNotification(n, (notifications.length + index).toString())
|
||||
|
55
pkg/grid/src/nav/NotificationsLink.tsx
Normal file
55
pkg/grid/src/nav/NotificationsLink.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { Link, LinkProps } from 'react-router-dom';
|
||||
import { Bullet } from '../components/icons/Bullet';
|
||||
import { Notification } from '../state/hark-types';
|
||||
import { useNotifications } from '../state/notifications';
|
||||
|
||||
type NotificationsState = 'empty' | 'unread' | 'attention-needed';
|
||||
|
||||
function getNotificationsState(
|
||||
notifications: Notification[],
|
||||
systemNotifications: Notification[]
|
||||
): NotificationsState {
|
||||
if (systemNotifications.length > 0) {
|
||||
return 'attention-needed';
|
||||
}
|
||||
|
||||
// TODO: when real structure, this should be actually be unread not just existence
|
||||
if (notifications.length > 0) {
|
||||
return 'unread';
|
||||
}
|
||||
|
||||
return 'empty';
|
||||
}
|
||||
|
||||
type NotificationsLinkProps = Omit<LinkProps<HTMLAnchorElement>, 'to'> & {
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
export const NotificationsLink = ({ isOpen }: NotificationsLinkProps) => {
|
||||
const { notifications, systemNotifications } = useNotifications();
|
||||
const state = getNotificationsState(notifications, systemNotifications);
|
||||
|
||||
return (
|
||||
<Link
|
||||
to="/leap/notifications"
|
||||
className={classNames(
|
||||
'relative z-50 flex-none circle-button h4',
|
||||
isOpen && 'text-opacity-60',
|
||||
state === 'empty' && !isOpen && 'text-gray-400 bg-gray-100',
|
||||
state === 'empty' && isOpen && 'text-gray-400 bg-white',
|
||||
state === 'unread' && 'bg-blue-400 text-white',
|
||||
state === 'attention-needed' && 'text-white bg-orange-500'
|
||||
)}
|
||||
>
|
||||
{state === 'empty' && <Bullet className="w-6 h-6" />}
|
||||
{state === 'unread' && notifications.length}
|
||||
{state === 'attention-needed' && (
|
||||
<span className="h2">
|
||||
! <span className="sr-only">Attention needed</span>
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
};
|
@ -4,6 +4,7 @@ import clipboardCopy from 'clipboard-copy';
|
||||
import React, { HTMLAttributes, useCallback, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Adjust } from '../components/icons/Adjust';
|
||||
import { disableDefault } from '../state/util';
|
||||
|
||||
type SystemMenuProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
open: boolean;
|
||||
@ -36,7 +37,7 @@ export const SystemMenu = ({ open, setOpen, className, showOverlay = false }: Sy
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
onCloseAutoFocus={disableDefault}
|
||||
sideOffset={12}
|
||||
className="dropdown min-w-64 p-6 font-semibold text-gray-500 bg-white"
|
||||
>
|
||||
|
@ -7,11 +7,10 @@ import { Dialog, DialogClose, DialogContent, DialogTrigger } from '../../compone
|
||||
import { Elbow } from '../../components/icons/Elbow';
|
||||
import api from '../../state/api';
|
||||
import { useCharges } from '../../state/docket';
|
||||
import {
|
||||
BaseBlockedNotification as BaseBlockedNotificationType,
|
||||
} from '../../state/hark-types';
|
||||
import { BaseBlockedNotification as BaseBlockedNotificationType } from '../../state/hark-types';
|
||||
|
||||
import { NotificationButton } from './NotificationButton';
|
||||
import { disableDefault } from '../../state/util';
|
||||
|
||||
interface BaseBlockedNotificationProps {
|
||||
notification: BaseBlockedNotificationType;
|
||||
@ -50,7 +49,7 @@ export const BaseBlockedNotification = ({ notification }: BaseBlockedNotificatio
|
||||
const handlePauseOTAs = useCallback(() => {}, []);
|
||||
|
||||
const handleArchiveApps = useCallback(async () => {
|
||||
await Promise.all(desks.map(d => api.poke(kilnSuspend(d))));
|
||||
await Promise.all(desks.map((d) => api.poke(kilnSuspend(d))));
|
||||
// TODO: retrigger OTA?
|
||||
}, [desks]);
|
||||
|
||||
@ -69,13 +68,7 @@ export const BaseBlockedNotification = ({ notification }: BaseBlockedNotificatio
|
||||
<h2 id="blocked-apps">The following ({count}) apps blocked a System Update:</h2>
|
||||
</div>
|
||||
</header>
|
||||
<AppList
|
||||
apps={blockedCharges}
|
||||
labelledBy="blocked-apps"
|
||||
size="xs"
|
||||
className="font-medium"
|
||||
listClass="space-y-2"
|
||||
/>
|
||||
<AppList apps={blockedCharges} labelledBy="blocked-apps" size="xs" className="font-medium" />
|
||||
<div className="space-y-6">
|
||||
<p>
|
||||
In order to proceed with the System Update, you’ll need to temporarily archive these apps,
|
||||
@ -120,6 +113,7 @@ export const BaseBlockedNotification = ({ notification }: BaseBlockedNotificatio
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
showClose={false}
|
||||
onOpenAutoFocus={disableDefault}
|
||||
className="max-w-[400px] space-y-6 text-base tracking-tight"
|
||||
>
|
||||
<h2 className="h4">Archive ({count}) Apps and Apply System Update</h2>
|
||||
@ -131,7 +125,7 @@ export const BaseBlockedNotification = ({ notification }: BaseBlockedNotificatio
|
||||
apps={blockedCharges}
|
||||
labelledBy="blocked-apps"
|
||||
size="xs"
|
||||
listClass="space-y-2"
|
||||
className="text-sm"
|
||||
/>
|
||||
<div className="flex space-x-6">
|
||||
<DialogClose as={Button} variant="secondary">
|
||||
|
@ -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]);
|
||||
|
@ -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.href === docket.href);
|
||||
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({
|
||||
@ -85,22 +85,33 @@ export const Home = () => {
|
||||
}, [recentApps, recentDevs]);
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 md:p-8 space-y-8 font-semibold leading-tight text-black overflow-y-auto">
|
||||
<h2 id="recent-apps" className="h4 text-gray-500">
|
||||
<div className="h-full p-4 md:p-8 font-semibold leading-tight text-black overflow-y-auto">
|
||||
<h2 id="recent-apps" className="mb-6 h4 text-gray-500">
|
||||
Recent Apps
|
||||
</h2>
|
||||
{recentApps.length === 0 && (
|
||||
<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} small onClick={() => addRecentApp(groups)} />}
|
||||
{groups && (
|
||||
<AppLink
|
||||
app={groups}
|
||||
size="small"
|
||||
onClick={() => addRecentApp({ ...groups, desk: 'groups' })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{recentApps.length > 0 && (
|
||||
<AppList apps={recentApps} labelledBy="recent-apps" matchAgainst={selectedMatch} small />
|
||||
<AppList
|
||||
apps={recentApps}
|
||||
labelledBy="recent-apps"
|
||||
matchAgainst={selectedMatch}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
<hr className="-mx-4 md:-mx-8" />
|
||||
<h2 id="recent-devs" className="h4 text-gray-500">
|
||||
<hr className="-mx-4 my-6 md:-mx-8 md:my-9" />
|
||||
<h2 id="recent-devs" className="mb-6 h4 text-gray-500">
|
||||
Recent Developers
|
||||
</h2>
|
||||
{recentDevs.length === 0 && (
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import create from 'zustand';
|
||||
import { Notification } from './hark-types';
|
||||
import { mockBlockedChargeNotification } from './mock-data';
|
||||
import { mockNotification } from './mock-data';
|
||||
import { useMockData } from './util';
|
||||
|
||||
interface HarkStore {
|
||||
@ -8,5 +8,5 @@ interface HarkStore {
|
||||
}
|
||||
|
||||
export const useHarkStore = create<HarkStore>(() => ({
|
||||
notifications: useMockData ? [mockBlockedChargeNotification] : []
|
||||
notifications: useMockData ? [mockNotification] : []
|
||||
}));
|
||||
|
@ -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() {
|
||||
|
@ -2,10 +2,13 @@ import _ from 'lodash-es';
|
||||
import { Allies, Charges, DocketHrefGlob, Treaties, Treaty } from '@urbit/api/docket';
|
||||
import { Vat, Vats } from '@urbit/api/hood';
|
||||
import systemUrl from '../assets/system.png';
|
||||
// import { SystemNotification } from './hark-types';
|
||||
import { BasicNotification } from './hark-types';
|
||||
|
||||
export const appMetaData: Pick<Treaty, 'cass' | 'hash' | 'website' | 'license' | 'version'> = {
|
||||
cass: '~2021.8.11..05.11.10..b721',
|
||||
cass: {
|
||||
da: 1629849472746,
|
||||
ud: 1
|
||||
},
|
||||
hash: '0v6.nj6ls.l7unh.l9bhk.d839n.n8nlq.m2dmc.fj80i.pvqun.uhg6g.1kk0h',
|
||||
website: 'https://tlon.io',
|
||||
license: 'MIT',
|
||||
@ -28,7 +31,7 @@ export const mockTreaties: Treaties = {
|
||||
title: 'Messages',
|
||||
ship: '~zod',
|
||||
desk: 'messages',
|
||||
href: makeHref('messaages'),
|
||||
href: makeHref('messages'),
|
||||
info: 'A lengthier description of the app down here',
|
||||
color: '#8BE789',
|
||||
...appMetaData
|
||||
@ -152,9 +155,10 @@ export const mockAllies: Allies = [
|
||||
'~nalrys'
|
||||
].reduce((acc, val) => ({ ...acc, [val]: charter }), {});
|
||||
|
||||
export const mockBlockedChargeNotification: any = {
|
||||
type: 'system-updates-blocked',
|
||||
charges: ['groups', 'pomodoro']
|
||||
export const mockNotification: BasicNotification = {
|
||||
type: 'basic',
|
||||
time: '',
|
||||
message: 'test'
|
||||
};
|
||||
|
||||
export const mockVat = (desk: string, blockers?: boolean): Vat => ({
|
||||
@ -184,5 +188,3 @@ export const mockVats = _.reduce(
|
||||
},
|
||||
{ base: mockVat('base', true) } as Vats
|
||||
);
|
||||
|
||||
console.log(mockVats);
|
||||
|
28
pkg/grid/src/state/notifications.ts
Normal file
28
pkg/grid/src/state/notifications.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { useHarkStore } from './hark';
|
||||
import { Notification } from './hark-types';
|
||||
import { useBlockers, useLag } from './kiln';
|
||||
|
||||
function getSystemNotifications(lag: boolean, blockers: string[]) {
|
||||
const nots = [] as Notification[];
|
||||
if (lag) {
|
||||
nots.push({ type: 'runtime-lag' });
|
||||
}
|
||||
if (blockers.length > 0) {
|
||||
nots.push({ type: 'system-updates-blocked', desks: blockers });
|
||||
}
|
||||
return nots;
|
||||
}
|
||||
|
||||
export const useNotifications = () => {
|
||||
const notifications = useHarkStore((s) => s.notifications);
|
||||
const blockers = useBlockers();
|
||||
const lag = useLag();
|
||||
const systemNotifications = getSystemNotifications(lag, blockers);
|
||||
const hasAnyNotifications = notifications.length > 0 || systemNotifications.length > 0;
|
||||
|
||||
return {
|
||||
notifications,
|
||||
systemNotifications,
|
||||
hasAnyNotifications
|
||||
};
|
||||
};
|
@ -19,3 +19,7 @@ export async function fakeRequest<T>(data: T, time = 300): Promise<T> {
|
||||
export function getAppHref(href: DocketHref) {
|
||||
return 'site' in href ? href.site : `/apps/${href.glob.base}`;
|
||||
}
|
||||
|
||||
export function disableDefault<T extends Event>(e: T): void {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
@ -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=""
|
||||
/>
|
||||
|
@ -4,6 +4,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import useDocketState from '../state/docket';
|
||||
import { disableDefault } from '../state/util';
|
||||
|
||||
export interface TileMenuProps {
|
||||
desk: string;
|
||||
@ -66,7 +67,7 @@ export const TileMenu = ({ desk, active, menuColor, lightText, className }: Tile
|
||||
align="start"
|
||||
alignOffset={-32}
|
||||
sideOffset={4}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
onCloseAutoFocus={disableDefault}
|
||||
className={classNames(
|
||||
'dropdown py-2 font-semibold',
|
||||
lightText ? 'text-gray-100' : 'text-gray-800'
|
||||
|
14352
pkg/npm/http-api/package-lock.json
generated
14352
pkg/npm/http-api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user