settings: add drag and drop component to reorder tiles

This commit is contained in:
Liam Fitzgerald 2020-08-05 11:48:46 +10:00
parent 9a00ef5f56
commit c88dcc7b06
7 changed files with 312 additions and 26 deletions

View File

@ -1585,6 +1585,22 @@
}
}
},
"@react-dnd/asap": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz",
"integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ=="
},
"@react-dnd/invariant": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz",
"integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw=="
},
"@react-dnd/shallowequal": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz",
"integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==",
"dev": true
},
"@styled-system/background": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@styled-system/background/-/background-5.1.2.tgz",
@ -1732,6 +1748,16 @@
"integrity": "sha512-GRTZLeLJ8ia00ZH8mxMO8t0aC9M1N9bN461Z2eaRurJo6Fpa+utgCwLzI4jQHcrdzuzp5WPN9jRwpsCQ1VhJ5w==",
"dev": true
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dev": true,
"requires": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"@types/html-minifier-terser": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz",
@ -3641,6 +3667,21 @@
"randombytes": "^2.0.0"
}
},
"dnd-core": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-11.1.3.tgz",
"integrity": "sha512-QugF55dNW+h+vzxVJ/LSJeTeUw9MCJ2cllhmVThVPEtF16ooBkxj0WBE5RB+AceFxMFo1rO6bJKXtqKl+JNnyA==",
"requires": {
"@react-dnd/asap": "^4.0.0",
"@react-dnd/invariant": "^2.0.0",
"redux": "^4.0.4"
}
},
"dnd-multi-backend": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/dnd-multi-backend/-/dnd-multi-backend-6.0.0.tgz",
"integrity": "sha512-qfUO4V0IACs24xfE9m9OUnwIzoL+SWzSiFbKVIHE0pFddJeZ93BZOdHS1XEYr8X3HNh+CfnfjezXgOMgjvh74g=="
},
"dns-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
@ -7532,6 +7573,53 @@
"resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-6.0.1.tgz",
"integrity": "sha512-rutEKVgvFhWcy/GeVA1hFbqrO89qLqgqdhUr7YhYgIzdyICdlRQv+ztuNvOFQMXrO0fLt0VkaYOdMdYdQgsSUA=="
},
"react-dnd": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-11.1.3.tgz",
"integrity": "sha512-8rtzzT8iwHgdSC89VktwhqdKKtfXaAyC4wiqp0SywpHG12TTLvfOoL6xNEIUWXwIEWu+CFfDn4GZJyynCEuHIQ==",
"dev": true,
"requires": {
"@react-dnd/shallowequal": "^2.0.0",
"@types/hoist-non-react-statics": "^3.3.1",
"dnd-core": "^11.1.3",
"hoist-non-react-statics": "^3.3.0"
}
},
"react-dnd-html5-backend": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-11.1.3.tgz",
"integrity": "sha512-/1FjNlJbW/ivkUxlxQd7o3trA5DE33QiRZgxent3zKme8DwF4Nbw3OFVhTRFGaYhHFNL1rZt6Rdj1D78BjnNLw==",
"requires": {
"dnd-core": "^11.1.3"
}
},
"react-dnd-multi-backend": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/react-dnd-multi-backend/-/react-dnd-multi-backend-6.0.2.tgz",
"integrity": "sha512-SwpqRv0HkJYu244FbHf9NbvGzGy14Ir9wIAhm909uvOVaHgsOq6I1THMSWSgpwUI31J3Bo5uS19tuvGpVPjzZw==",
"requires": {
"dnd-multi-backend": "^6.0.0",
"prop-types": "^15.7.2",
"react-dnd-preview": "^6.0.2"
}
},
"react-dnd-preview": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/react-dnd-preview/-/react-dnd-preview-6.0.2.tgz",
"integrity": "sha512-F2+uK4Be+q+7mZfNh9kaZols7wp1hX6G7UBTVaTpDsBpMhjFvY7/v7odxYSerSFBShh23MJl33a4XOVRFj1zoQ==",
"requires": {
"prop-types": "^15.7.2"
}
},
"react-dnd-touch-backend": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-11.1.3.tgz",
"integrity": "sha512-8lz4fxfYwUuJ6Y2seQYwh8+OfwKcbBX0CIbz7AwXfBYz54Wg2nIDU6CP8Dyybt/Wyx4D3oXmTPEaOMB62uqJvQ==",
"requires": {
"@react-dnd/invariant": "^2.0.0",
"dnd-core": "^11.1.3"
}
},
"react-dom": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
@ -7669,6 +7757,15 @@
"picomatch": "^2.2.1"
}
},
"redux": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
"integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
"requires": {
"loose-envify": "^1.4.0",
"symbol-observable": "^1.2.0"
}
},
"regenerate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
@ -8989,6 +9086,11 @@
"xml-reader": "2.4.3"
}
},
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
},
"synchronous-promise": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.13.tgz",

View File

@ -21,6 +21,9 @@
"prop-types": "^15.7.2",
"react": "^16.5.2",
"react-codemirror2": "^6.0.1",
"react-dnd-html5-backend": "^11.1.3",
"react-dnd-multi-backend": "^6.0.2",
"react-dnd-touch-backend": "^11.1.3",
"react-dom": "^16.8.6",
"react-markdown": "^4.3.1",
"react-router-dom": "^5.0.0",
@ -54,6 +57,7 @@
"eslint-plugin-react": "^7.19.0",
"file-loader": "^6.0.0",
"html-webpack-plugin": "^4.2.0",
"react-dnd": "^11.1.3",
"react-hot-loader": "^4.12.21",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",

View File

@ -17,21 +17,12 @@ import _ from "lodash";
import GlobalApi from "../../../../api/global";
import { BackgroundConfig } from "../../../../types/local-update";
import { LaunchState } from "../../../../types/launch-update";
import { DropLaunchTiles } from "./DropLaunch";
const tiles = ["publish", "links", "chat", "dojo", "clock", "weather"];
const formSchema = Yup.object().shape({
order: Yup.string()
.required("Required")
.test(
"tiles",
"Invalid tile ordering",
(o: string = "") =>
_.difference(
o.split(", ").map((i) => i.trim()),
tiles
).length === 0
),
tileOrdering: Yup.array().of(Yup.string()),
bgType: Yup.string()
.oneOf(["none", "color", "url"], "invalid")
.required("Required"),
@ -44,7 +35,7 @@ const formSchema = Yup.object().shape({
type BgType = "none" | "url" | "color";
interface FormSchema {
order: string;
tileOrdering: string[];
bgType: BgType;
bgColor: string | undefined;
bgUrl: string | undefined;
@ -107,7 +98,6 @@ function BackgroundPicker({
export default function DisplayForm(props: DisplayFormProps) {
const { api, launch, background, hideAvatars, hideNicknames } = props;
const initialOrder = launch.tileOrdering.join(", ");
let bgColor, bgUrl;
if (background?.type === "url") {
bgUrl = background.url;
@ -119,21 +109,25 @@ export default function DisplayForm(props: DisplayFormProps) {
const logoutAll = useCallback(() => {}, []);
console.log(tiles);
return (
<Formik
validationSchema={formSchema}
initialValues={
{
order: initialOrder,
bgType,
bgColor,
bgUrl,
avatars: hideAvatars,
nicknames: hideNicknames,
tileOrdering: launch.tileOrdering,
} as FormSchema
}
onSubmit={(values, actions) => {
api.launch.changeOrder(values.order.split(", "));
console.log("saving");
console.log(values.tileOrdering);
api.launch.changeOrder(values.tileOrdering);
const bgConfig: BackgroundConfig =
values.bgType === "color"
@ -155,18 +149,27 @@ export default function DisplayForm(props: DisplayFormProps) {
display="grid"
gridTemplateColumns="1fr"
gridTemplateRows="auto"
gridRowGap={2}
gridRowGap={3}
>
<Box color="black" fontSize={1} mb={3} fontWeight={900}>
Display Preferences
</Box>
<Box>
<Input
<Box mb={2}>
{/*<Input
label="Home Tile Order"
id="order"
type="text"
width={256}
/>*/}
<InputLabel display="block" pb={2}>
Tile Order
</InputLabel>
<DropLaunchTiles
id="tileOrdering"
name="tileOrdering"
tiles={launch.tiles}
order={launch.tileOrdering}
/>
</Box>
<BackgroundPicker
@ -186,11 +189,7 @@ export default function DisplayForm(props: DisplayFormProps) {
/>
</Box>
</Box>
<Button
border={1}
borderColor="washedGray"
type="submit"
>
<Button border={1} borderColor="washedGray" type="submit">
Save
</Button>
</Form>

View File

@ -0,0 +1,96 @@
import React, { useMemo } from "react";
import { useDrag } from "react-dnd";
import { usePreview } from "react-dnd-multi-backend";
import { capitalize } from 'lodash';
import { TileTypeBasic, Tile } from "../../../../types/launch-update";
import { Box, Img, Text } from "@tlon/indigo-react";
interface DragTileProps {
index: number;
tile: Tile;
title: string;
style?: any;
}
function DragTileBox({ title, index, tile, ...props }: any) {
const [, dragRef] = useDrag({
item: { type: "launchTile", index, tile, title },
collect: (monitor) => ({}),
});
return (
<Box
ref={dragRef}
display="flex"
alignItems="center"
justifyContent="space-around"
flexDirection="column"
border={1}
height="100%"
width="100%"
style={{ cursor: "move" }}
{...props}
></Box>
);
}
function DragTileCustom({ index, title, style }: any) {
const tile = { type: { custom: null } };
return (
<DragTileBox bg="white" style={style} title={title} tile={tile} index={index}>
<Text fontSize={1}>{capitalize(title)}</Text>
</DragTileBox>
);
}
function DragTileBasic(props: {
tile: TileTypeBasic;
index: number;
style: any;
}) {
const { basic: tile } = props.tile;
const isDojo = useMemo(() => tile.title === "Dojo", [tile.title]);
return (
<DragTileBox
tile={{ type: props.tile }}
index={props.index}
bg={isDojo ? "black" : "white"}
style={props.style}
>
<Img width="48px" height="48px" src={tile.iconUrl} />
<Text color={isDojo ? "white" : "black"}>{tile.title}</Text>
</DragTileBox>
);
}
export function DragTile(props: DragTileProps) {
if ("basic" in props.tile.type) {
return (
<DragTileBasic
index={props.index}
style={props.style}
tile={props.tile.type}
/>
);
} else {
return (
<DragTileCustom
style={props.style}
title={props.title}
index={props.index}
/>
);
}
}
export function DragTilePreview() {
let { display, style, item } = usePreview();
if (!display) {
return null;
}
style = { ...style, height: "96px", width: "96px", "z-index": "5" };
return <DragTile style={style} {...item} />;
}

View File

@ -0,0 +1,85 @@
import React, { useCallback, ReactNode } from "react";
import { useDrop } from "react-dnd";
import { DndProvider, usePreview } from "react-dnd-multi-backend";
import HTML5toTouch from "react-dnd-multi-backend/dist/esm/HTML5toTouch";
import { Box } from "@tlon/indigo-react";
import { DragTile, DragTilePreview } from "./DragTile";
import { useField } from "formik";
function DropLaunchTile({
children,
index,
didDrop,
}: {
index: number;
children: ReactNode;
didDrop: (item: number, location: number) => void;
}) {
const onDrop = useCallback(
(item: any, monitor: any) => {
didDrop(item.index, index);
},
[index, didDrop]
);
const { display, style, item } = usePreview();
const [{ isOver }, drop] = useDrop({
accept: "launchTile",
drop: onDrop,
collect: (monitor) => ({
isOver: !!monitor.isOver(),
}),
});
return (
<div
ref={drop}
style={{
position: "relative",
width: "100%",
height: "100%",
}}
>
{children}
</div>
);
}
export function DropLaunchTiles({ tiles, name }: any) {
const [field, meta, helpers] = useField<string[]>(name);
const { value } = meta;
const { setValue } = helpers;
const onChange = useCallback(
(x: number, y: number) => {
// swap tiles
let t = value.slice();
const c = t[x];
t[x] = t[y];
t[y] = c;
setValue(t);
},
[setValue, value]
);
return (
<DndProvider options={HTML5toTouch}>
<Box
display="grid"
gridGap={2}
gridTemplateColumns={["96px 96px", "96px 96px 96px"]}
gridAutoRows="96px"
>
<DragTilePreview />
{value.map((tile, i) => (
<DropLaunchTile didDrop={onChange} key={`${i}-${tile}`} index={i}>
<DragTile title={tile} tile={tiles[tile]} index={i} />
</DropLaunchTile>
))}
</Box>
</DndProvider>
);
}

View File

@ -9,7 +9,7 @@ import Settings from "./components/settings";
export default function ProfileScreen(props: any) {
const { ship, dark } = props;
return (
<Box height="100%" px={3} pb={3} borderRadius={1}>
<Box height="100%" px={[0,3]} pb={[0,3]} borderRadius={1}>
<Box
height="100%"
width="100%"

View File

@ -33,14 +33,14 @@ export interface LaunchState {
}
}
interface Tile {
export interface Tile {
isShown: boolean;
type: TileType;
}
type TileType = TileTypeBasic | TileTypeCustom;
interface TileTypeBasic {
export interface TileTypeBasic {
basic: {
iconUrl: string;
linkedUrl: string;