Merge pull request #5624 from urbit/hm/configurable-tiles

grid: configurable tile order
This commit is contained in:
Hunter Miller 2022-03-08 15:26:30 -06:00 committed by GitHub
commit 6f8df6815f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 13924 additions and 13787 deletions

27524
pkg/grid/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,9 @@
"postcss-import": "^14.0.2",
"query-string": "^7.0.1",
"react": "^17.0.2",
"react-dnd": "^15.1.1",
"react-dnd-html5-backend": "^15.1.2",
"react-dnd-touch-backend": "^15.1.1",
"react-dom": "^17.0.2",
"react-error-boundary": "^3.1.3",
"react-router-dom": "^5.2.0",

View File

@ -1,46 +1,43 @@
import { map, omit } from 'lodash';
import React, { FunctionComponent, useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Route, RouteComponentProps, useHistory, useParams } from 'react-router-dom';
import { Route, useHistory, useParams } from 'react-router-dom';
import { ErrorAlert } from '../components/ErrorAlert';
import { MenuState, Nav } from '../nav/Nav';
import { useCharges } from '../state/docket';
import useKilnState from '../state/kiln';
import { RemoveApp } from '../tiles/RemoveApp';
import { SuspendApp } from '../tiles/SuspendApp';
import { Tile } from '../tiles/Tile';
import { TileGrid } from '../tiles/TileGrid';
import { TileInfo } from '../tiles/TileInfo';
interface RouteProps {
menu?: MenuState;
}
export const Grid: FunctionComponent<{}> = () => {
const charges = useCharges();
export const Grid: FunctionComponent = () => {
const { push } = useHistory();
const { menu } = useParams<RouteProps>();
const chargesLoaded = Object.keys(charges).length > 0;
useEffect(() => {
// TOOD: rework
// Heuristically detect reload completion and redirect
async function attempt(count = 0) {
if(count > 5) {
if (count > 5) {
window.location.reload();
}
const start = performance.now();
await useKilnState.getState().fetchVats();
await useKilnState.getState().fetchVats();
if((performance.now() - start) > 5000) {
attempt(count+1);
if (performance.now() - start > 5000) {
attempt(count + 1);
} else {
push('/');
}
}
if(menu === 'upgrading') {
if (menu === 'upgrading') {
attempt();
}
}, [menu])
}, [menu]);
return (
<div className="flex flex-col">
@ -49,15 +46,7 @@ export const Grid: FunctionComponent<{}> = () => {
</header>
<main className="h-full w-full flex justify-center pt-4 md:pt-16 pb-32 relative z-0">
{!chargesLoaded && <span>Loading...</span>}
{chargesLoaded && (
<div className="grid justify-center grid-cols-2 sm:grid-cols-[repeat(auto-fit,minmax(auto,250px))] gap-4 px-4 md:px-8 w-full max-w-6xl">
{charges &&
map(omit(charges, window.desk), (charge, desk) => (
<Tile key={desk} charge={charge} desk={desk} disabled={menu === 'upgrading'} />
))}
</div>
)}
<TileGrid menu={menu} />
<ErrorBoundary FallbackComponent={ErrorAlert} onReset={() => push('/')}>
<Route exact path="/app/:desk">
<TileInfo />

View File

@ -21,6 +21,9 @@ interface BaseSettingsState {
theme: 'light' | 'dark' | 'auto';
doNotDisturb: boolean;
};
tiles: {
order: string[];
};
putEntry: (bucket: string, key: string, value: Value) => Promise<void>;
[ref: string]: unknown;
}
@ -71,6 +74,9 @@ export const useSettingsState = createState<BaseSettingsState>(
theme: 'auto',
doNotDisturb: true
},
tiles: {
order: []
},
loaded: false,
putEntry: async (bucket, key, val) => {
const poke = doPutEntry(window.desk, bucket, key, val);

View File

@ -1,5 +1,6 @@
import classNames from 'classnames';
import React, { FunctionComponent } from 'react';
import { useDrag } from 'react-dnd';
import { chadIsRunning } from '@urbit/api';
import { TileMenu } from './TileMenu';
import { Spinner } from '../components/Spinner';
@ -9,6 +10,7 @@ import { ChargeWithDesk } from '../state/docket';
import { useTileColor } from './useTileColor';
import { useVat } from '../state/kiln';
import { Bullet } from '../components/icons/Bullet';
import { dragTypes } from './TileGrid';
type TileProps = {
charge: ChargeWithDesk;
@ -28,13 +30,23 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk, disabled = fa
const link = getAppHref(href);
const backgroundColor = suspended ? suspendColor : active ? tileColor || 'purple' : suspendColor;
const [{ isDragging }, drag] = useDrag(() => ({
type: dragTypes.TILE,
item: { desk },
collect: (monitor) => ({
isDragging: !!monitor.isDragging()
})
}));
return (
<a
ref={drag}
href={active ? link : undefined}
target="_blank"
rel="noreferrer"
className={classNames(
'group relative font-semibold aspect-w-1 aspect-h-1 rounded-3xl default-ring focus-visible:ring-4 overflow-hidden',
'group absolute font-semibold w-full h-full rounded-3xl default-ring focus-visible:ring-4 overflow-hidden',
isDragging && 'opacity-0',
lightText && active && !loading ? 'text-gray-200' : 'text-gray-800',
!active && 'cursor-default'
)}
@ -48,7 +60,7 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk, disabled = fa
<>
{loading && <Spinner className="h-6 w-6 mr-2" />}
<span className="text-gray-500">
{suspended ? 'Suspended' : loading ? 'Installing' : hung ? 'Errored' : null }
{suspended ? 'Suspended' : loading ? 'Installing' : hung ? 'Errored' : null}
</span>
</>
)}

View File

@ -0,0 +1,56 @@
import classNames from 'classnames';
import { uniq, without } from 'lodash';
import React, { FunctionComponent } from 'react';
import { useDrop } from 'react-dnd';
import { useSettingsState } from '../state/settings';
import { dragTypes, selTiles } from './TileGrid';
interface TileContainerProps {
desk: string;
}
export const TileContainer: FunctionComponent<TileContainerProps> = ({ desk, children }) => {
const { order } = useSettingsState(selTiles);
const [{ isOver }, drop] = useDrop<{ desk: string }, undefined, { isOver: boolean }>(
() => ({
accept: dragTypes.TILE,
drop: ({ desk: itemDesk }) => {
if (!itemDesk || itemDesk === desk) {
return undefined;
}
// [1, 2, 3, 4] 1 -> 3
// [2, 3, 4]
const beforeSlot = order.indexOf(itemDesk) < order.indexOf(desk);
const orderWithoutOriginal = without(order, itemDesk);
const slicePoint = orderWithoutOriginal.indexOf(desk);
// [2, 3] [4]
const left = orderWithoutOriginal.slice(0, beforeSlot ? slicePoint + 1 : slicePoint);
const right = orderWithoutOriginal.slice(slicePoint);
// concat([2, 3], [1], [4])
const newOrder = uniq(left.concat([itemDesk], right));
// [2, 3, 1, 4]
console.log({ order, left, right, slicePoint, newOrder });
useSettingsState.getState().putEntry('tiles', 'order', newOrder);
return undefined;
},
collect: (monitor) => ({
isOver: !!monitor.isOver()
})
}),
[desk, order]
);
return (
<div
ref={drop}
className={classNames(
'relative aspect-w-1 aspect-h-1 rounded-3xl ring-4',
isOver && 'ring-blue-500',
!isOver && 'ring-transparent'
)}
>
{children}
</div>
);
};

View File

@ -0,0 +1,75 @@
import React, { useEffect } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { TouchBackend } from 'react-dnd-touch-backend';
import { uniq } from 'lodash';
import { ChargeWithDesk, useCharges } from '../state/docket';
import { Tile } from './Tile';
import { MenuState } from '../nav/Nav';
import { SettingsState, useSettingsState } from '../state/settings';
import { TileContainer } from './TileContainer';
import { useMedia } from '../logic/useMedia';
export interface TileData {
desk: string;
charge: ChargeWithDesk;
position: number;
dragging: boolean;
}
interface TileGridProps {
menu?: MenuState;
}
export const dragTypes = {
TILE: 'tile'
};
export const selTiles = (s: SettingsState) => s.tiles;
export const TileGrid = ({ menu }: TileGridProps) => {
const charges = useCharges();
const chargesLoaded = Object.keys(charges).length > 0;
const { order } = useSettingsState(selTiles);
const isMobile = useMedia('(pointer: coarse)');
useEffect(() => {
const hasKeys = order && !!order.length;
const chargeKeys = Object.keys(charges);
if (!hasKeys) {
useSettingsState.getState().putEntry('tiles', 'order', chargeKeys);
} else if (order.length < chargeKeys.length) {
useSettingsState.getState().putEntry('tiles', 'order', uniq(order.concat(chargeKeys)));
}
}, [charges, order]);
if (!chargesLoaded) {
return <span>Loading...</span>;
}
return (
<DndProvider
backend={isMobile ? TouchBackend : HTML5Backend}
options={
isMobile
? {
delay: 50,
scrollAngleRanges: [
{ start: 30, end: 150 },
{ start: 210, end: 330 }
]
}
: undefined
}
>
<div className="grid justify-center grid-cols-2 sm:grid-cols-[repeat(auto-fit,minmax(auto,250px))] gap-4 px-4 md:px-8 w-full max-w-6xl">
{order.map((desk) => (
<TileContainer desk={desk}>
<Tile key={desk} charge={charges[desk]} desk={desk} disabled={menu === 'upgrading'} />
</TileContainer>
))}
</div>
</DndProvider>
);
};