mirror of
https://github.com/urbit/shrub.git
synced 2024-12-21 01:41:37 +03:00
Merge pull request #5624 from urbit/hm/configurable-tiles
grid: configurable tile order
This commit is contained in:
commit
6f8df6815f
27524
pkg/grid/package-lock.json
generated
27524
pkg/grid/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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 />
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
56
pkg/grid/src/tiles/TileContainer.tsx
Normal file
56
pkg/grid/src/tiles/TileContainer.tsx
Normal 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>
|
||||
);
|
||||
};
|
75
pkg/grid/src/tiles/TileGrid.tsx
Normal file
75
pkg/grid/src/tiles/TileGrid.tsx
Normal 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>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user