Merge pull request #360 from revoltchat/rework/categories-kanban

This commit is contained in:
Paul Makles 2021-10-31 16:38:10 +00:00 committed by GitHub
commit 92597ab1cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 699 additions and 152 deletions

View file

@ -43,6 +43,7 @@
"dependencies": { "dependencies": {
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"klaw": "^3.0.0", "klaw": "^3.0.0",
"react-beautiful-dnd": "^13.1.0",
"sirv-cli": "^1.0.14", "sirv-cli": "^1.0.14",
"vite": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b" "vite": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b"
}, },
@ -81,6 +82,7 @@
"@types/node": "^15.12.4", "@types/node": "^15.12.4",
"@types/preact-i18n": "^2.3.0", "@types/preact-i18n": "^2.3.0",
"@types/prismjs": "^1.16.5", "@types/prismjs": "^1.16.5",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-helmet": "^6.1.1", "@types/react-helmet": "^6.1.1",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "^5.1.7",
"@types/react-scroll": "^1.8.2", "@types/react-scroll": "^1.8.2",

View file

@ -48,11 +48,11 @@ export type UploadState =
| { type: "none" } | { type: "none" }
| { type: "attached"; files: File[] } | { type: "attached"; files: File[] }
| { | {
type: "uploading"; type: "uploading";
files: File[]; files: File[];
percent: number; percent: number;
cancel: CancelTokenSource; cancel: CancelTokenSource;
} }
| { type: "sending"; files: File[] } | { type: "sending"; files: File[] }
| { type: "failed"; files: File[]; error: string }; | { type: "failed"; files: File[]; error: string };
@ -173,9 +173,9 @@ export default observer(({ channel }: Props) => {
const text = const text =
action === "quote" action === "quote"
? `${content ? `${content
.split("\n") .split("\n")
.map((x) => `> ${x}`) .map((x) => `> ${x}`)
.join("\n")}\n\n` .join("\n")}\n\n`
: `${content} `; : `${content} `;
if (!draft || draft.length === 0) { if (!draft || draft.length === 0) {
@ -225,8 +225,8 @@ export default observer(({ channel }: Props) => {
toReplace == "" toReplace == ""
? msg.content.toString() + newText ? msg.content.toString() + newText
: msg.content : msg.content
.toString() .toString()
.replace(new RegExp(toReplace, flags), newText); .replace(new RegExp(toReplace, flags), newText);
if (newContent != msg.content) { if (newContent != msg.content) {
if (newContent.length == 0) { if (newContent.length == 0) {
@ -305,10 +305,10 @@ export default observer(({ channel }: Props) => {
files, files,
percent: Math.round( percent: Math.round(
(i * 100 + (100 * e.loaded) / e.total) / (i * 100 + (100 * e.loaded) / e.total) /
Math.min( Math.min(
files.length, files.length,
CAN_UPLOAD_AT_ONCE, CAN_UPLOAD_AT_ONCE,
), ),
), ),
cancel, cancel,
}), }),
@ -398,6 +398,7 @@ export default observer(({ channel }: Props) => {
} }
} }
// TODO: change to useDebounceCallback
// eslint-disable-next-line // eslint-disable-next-line
const debouncedStopTyping = useCallback( const debouncedStopTyping = useCallback(
debounce(stopTyping as (...args: unknown[]) => void, 1000), debounce(stopTyping as (...args: unknown[]) => void, 1000),
@ -553,13 +554,13 @@ export default observer(({ channel }: Props) => {
placeholder={ placeholder={
channel.channel_type === "DirectMessage" channel.channel_type === "DirectMessage"
? translate("app.main.channel.message_who", { ? translate("app.main.channel.message_who", {
person: channel.recipient?.username, person: channel.recipient?.username,
}) })
: channel.channel_type === "SavedMessages" : channel.channel_type === "SavedMessages"
? translate("app.main.channel.message_saved") ? translate("app.main.channel.message_saved")
: translate("app.main.channel.message_where", { : translate("app.main.channel.message_where", {
channel_name: channel.name ?? undefined, channel_name: channel.name ?? undefined,
}) })
} }
disabled={ disabled={
uploadState.type === "uploading" || uploadState.type === "uploading" ||

View file

@ -0,0 +1,32 @@
import { Check, CloudUpload } from "@styled-icons/boxicons-regular";
import { Pencil } from "@styled-icons/boxicons-solid";
import styled from "styled-components";
const StatusBase = styled.div`
gap: 4px;
padding: 4px;
display: flex;
align-items: center;
text-transform: capitalize;
`;
export type EditStatus = "saved" | "editing" | "saving";
interface Props {
status: EditStatus;
}
export default function SaveStatus({ status }: Props) {
return (
<StatusBase>
{status === "saved" ? (
<Check size={20} />
) : status === "editing" ? (
<Pencil size={20} />
) : (
<CloudUpload size={20} />
)}
{/* FIXME: add i18n */}
<span>{status}</span>
</StatusBase>
);
}

View file

@ -2,6 +2,7 @@ import { Prompt } from "react-router";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import type { Attachment } from "revolt-api/types/Autumn"; import type { Attachment } from "revolt-api/types/Autumn";
import { Bot } from "revolt-api/types/Bots"; import { Bot } from "revolt-api/types/Bots";
import { TextChannel, VoiceChannel } from "revolt-api/types/Channels";
import type { EmbedImage } from "revolt-api/types/January"; import type { EmbedImage } from "revolt-api/types/January";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages"; import { Message } from "revolt.js/dist/maps/Messages";
@ -42,7 +43,12 @@ export type Screen =
| { type: "leave_server"; target: Server } | { type: "leave_server"; target: Server }
| { type: "delete_server"; target: Server } | { type: "delete_server"; target: Server }
| { type: "delete_channel"; target: Channel } | { type: "delete_channel"; target: Channel }
| { type: "delete_bot"; target: string; name: string; cb: () => void } | {
type: "delete_bot";
target: string;
name: string;
cb?: () => void;
}
| { type: "delete_message"; target: Message } | { type: "delete_message"; target: Message }
| { | {
type: "create_invite"; type: "create_invite";
@ -52,7 +58,11 @@ export type Screen =
| { type: "ban_member"; target: Server; user: User } | { type: "ban_member"; target: Server; user: User }
| { type: "unfriend_user"; target: User } | { type: "unfriend_user"; target: User }
| { type: "block_user"; target: User } | { type: "block_user"; target: User }
| { type: "create_channel"; target: Server } | {
type: "create_channel";
target: Server;
cb?: (channel: TextChannel | VoiceChannel) => void;
}
| { type: "create_category"; target: Server } | { type: "create_category"; target: Server }
)) ))
| ({ id: "special_input" } & ( | ({ id: "special_input" } & (

View file

@ -1,5 +1,6 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { TextChannel, VoiceChannel } from "revolt-api/types/Channels";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";
import { Message as MessageI } from "revolt.js/dist/maps/Messages"; import { Message as MessageI } from "revolt.js/dist/maps/Messages";
import { Server } from "revolt.js/dist/maps/Servers"; import { Server } from "revolt.js/dist/maps/Servers";
@ -60,7 +61,7 @@ type SpecialProps = { onClose: () => void } & (
| { type: "leave_server"; target: Server } | { type: "leave_server"; target: Server }
| { type: "delete_server"; target: Server } | { type: "delete_server"; target: Server }
| { type: "delete_channel"; target: Channel } | { type: "delete_channel"; target: Channel }
| { type: "delete_bot"; target: string; name: string; cb: () => void } | { type: "delete_bot"; target: string; name: string; cb?: () => void }
| { type: "delete_message"; target: MessageI } | { type: "delete_message"; target: MessageI }
| { | {
type: "create_invite"; type: "create_invite";
@ -70,7 +71,11 @@ type SpecialProps = { onClose: () => void } & (
| { type: "ban_member"; target: Server; user: User } | { type: "ban_member"; target: Server; user: User }
| { type: "unfriend_user"; target: User } | { type: "unfriend_user"; target: User }
| { type: "block_user"; target: User } | { type: "block_user"; target: User }
| { type: "create_channel"; target: Server } | {
type: "create_channel";
target: Server;
cb?: (channel: TextChannel | VoiceChannel) => void;
}
| { type: "create_category"; target: Server } | { type: "create_category"; target: Server }
); );
@ -158,7 +163,7 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
break; break;
case "delete_bot": case "delete_bot":
client.bots.delete(props.target); client.bots.delete(props.target);
props.cb(); props.cb?.();
break; break;
} }
@ -424,9 +429,14 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
nonce: ulid(), nonce: ulid(),
}); });
history.push( if (props.cb) {
`/server/${props.target._id}/channel/${channel._id}`, props.cb(channel);
); } else {
history.push(
`/server/${props.target._id}/channel/${channel._id}`,
);
}
onClose(); onClose();
} catch (err) { } catch (err) {
setError(takeError(err)); setError(takeError(err));
@ -472,7 +482,6 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
} }
case "create_category": { case "create_category": {
const [name, setName] = useState(""); const [name, setName] = useState("");
const history = useHistory();
return ( return (
<PromptModal <PromptModal

View file

@ -1,3 +1,7 @@
import isEqual from "lodash.isequal";
import { Inputs, useCallback, useEffect, useRef } from "preact/hooks";
export function debounce(cb: (...args: unknown[]) => void, duration: number) { export function debounce(cb: (...args: unknown[]) => void, duration: number) {
// Store the timer variable. // Store the timer variable.
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
@ -13,3 +17,60 @@ export function debounce(cb: (...args: unknown[]) => void, duration: number) {
}, duration); }, duration);
}; };
} }
export function useDebounceCallback(
cb: (...args: unknown[]) => void,
inputs: Inputs,
duration = 1000,
) {
// eslint-disable-next-line
return useCallback(
debounce(cb as (...args: unknown[]) => void, duration),
inputs,
);
}
export function useAutosaveCallback(
cb: (...args: unknown[]) => void,
inputs: Inputs,
duration = 1000,
) {
const ref = useRef(cb);
// eslint-disable-next-line
const callback = useCallback(
debounce(() => ref.current(), duration),
[],
);
useEffect(() => {
ref.current = cb;
callback();
// eslint-disable-next-line
}, [cb, callback, ...inputs]);
}
export function useAutosave<T>(
cb: () => void,
dependency: T,
initialValue: T,
onBeginChange?: () => void,
duration?: number,
) {
if (onBeginChange) {
// eslint-disable-next-line
useEffect(
() => {
!isEqual(dependency, initialValue) && onBeginChange();
},
// eslint-disable-next-line
[dependency],
);
}
return useAutosaveCallback(
() => !isEqual(dependency, initialValue) && cb(),
[dependency],
duration,
);
}

56
src/lib/dnd.ts Normal file
View file

@ -0,0 +1,56 @@
import {
Draggable as rbdDraggable,
DraggableProps as rbdDraggableProps,
DraggableProvided as rbdDraggableProvided,
DraggableProvidedDraggableProps as rbdDraggableProvidedDraggableProps,
DraggableProvidedDragHandleProps as rbdDraggableProvidedDragHandleProps,
DraggableRubric,
DraggableStateSnapshot,
Droppable as rbdDroppable,
DroppableProps,
DroppableProvided,
DroppableStateSnapshot,
} from "react-beautiful-dnd";
export type DraggableProvidedDraggableProps = Omit<
rbdDraggableProvidedDraggableProps,
"style" | "onTransitionEnd"
> & {
style?: string;
onTransitionEnd?: JSX.TransitionEventHandler<HTMLElement>;
};
export type DraggableProvidedDragHandleProps = Omit<
rbdDraggableProvidedDragHandleProps,
"onDragStart"
> & {
onDragStart?: JSX.DragEventHandler<HTMLElement>;
};
export type DraggableProvided = rbdDraggableProvided & {
draggableProps: DraggableProvidedDraggableProps;
dragHandleProps?: DraggableProvidedDragHandleProps | undefined;
};
export type DraggableChildrenFn = (
provided: DraggableProvided,
snapshot: DraggableStateSnapshot,
rubric: DraggableRubric,
) => JSX.Element;
export type DraggableProps = Omit<rbdDraggableProps, "children"> & {
children: DraggableChildrenFn;
};
export const Draggable = rbdDraggable as unknown as (
props: DraggableProps,
) => JSX.Element;
export const Droppable = rbdDroppable as unknown as (
props: Omit<DroppableProps, "children"> & {
children(
provided: DroppableProvided,
snapshot: DroppableStateSnapshot,
): JSX.Element;
},
) => JSX.Element;

View file

@ -50,6 +50,7 @@ export default observer(() => {
title: ( title: (
<Text id="app.settings.server_pages.categories.title" /> <Text id="app.settings.server_pages.categories.title" />
), ),
hideTitle: true,
}, },
{ {
id: "members", id: "members",

View file

@ -1,139 +1,460 @@
import isEqual from "lodash.isequal"; import { Plus, X } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext } from "react-beautiful-dnd";
import { TextChannel, VoiceChannel } from "revolt-api/types/Channels";
import { Category } from "revolt-api/types/Servers"; import { Category } from "revolt-api/types/Servers";
import { Server } from "revolt.js/dist/maps/Servers"; import { Server } from "revolt.js/dist/maps/Servers";
import styled, { css } from "styled-components";
import { ulid } from "ulid"; import { ulid } from "ulid";
import { useState } from "preact/hooks"; import { Text } from "preact-i18n";
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import { useAutosave } from "../../../lib/debounce";
import { Draggable, Droppable } from "../../../lib/dnd";
import { noop } from "../../../lib/js";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import ChannelIcon from "../../../components/common/ChannelIcon"; import ChannelIcon from "../../../components/common/ChannelIcon";
import Button from "../../../components/ui/Button"; import SaveStatus, { EditStatus } from "../../../components/ui/SaveStatus";
import ComboBox from "../../../components/ui/ComboBox";
import InputBox from "../../../components/ui/InputBox"; const KanbanEntry = styled.div`
import Tip from "../../../components/ui/Tip"; padding: 2px 4px;
> .inner {
display: flex;
align-items: center;
gap: 4px;
height: 40px;
padding: 8px;
flex-shrink: 0;
font-size: 0.9em;
background: var(--primary-background);
img {
flex-shrink: 0;
}
span {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
`;
const KanbanList = styled.div<{ last: boolean }>`
${(props) =>
!props.last &&
css`
padding-inline-end: 4px;
`}
> .inner {
width: 180px;
display: flex;
flex-shrink: 0;
overflow-y: auto;
padding-bottom: 2px;
flex-direction: column;
background: var(--secondary-background);
input {
width: 100%;
height: 100%;
border: none;
font-size: 1em;
text-align: center;
background: transparent;
color: var(--foreground);
}
> [data-rbd-droppable-id] {
min-height: 24px;
}
}
`;
const Row = styled.div`
gap: 2px;
margin: 4px;
display: flex;
> :first-child {
flex-grow: 1;
}
`;
const KanbanListHeader = styled.div`
height: 34px;
display: grid;
min-width: 34px;
place-items: center;
cursor: pointer !important;
transition: 0.2s ease background-color;
&:hover {
background: var(--background);
}
`;
const KanbanBoard = styled.div`
display: flex;
flex-direction: row;
`;
const FullSize = styled.div`
flex-grow: 1;
min-height: 0;
> * {
height: 100%;
overflow-x: scroll;
}
`;
const Header = styled.div`
display: flex;
h1 {
flex-grow: 1;
}
`;
interface Props { interface Props {
server: Server; server: Server;
} }
// ! FIXME: really bad code
export const Categories = observer(({ server }: Props) => { export const Categories = observer(({ server }: Props) => {
const channels = server.channels.filter((x) => typeof x !== "undefined"); const [status, setStatus] = useState<EditStatus>("saved");
const [categories, setCategories] = useState<Category[]>(
server.categories ?? [],
);
const [cats, setCats] = useState<Category[]>(server.categories ?? []); useAutosave(
const [name, setName] = useState(""); async () => {
setStatus("saving");
await server.edit({ categories });
setStatus("saved");
},
categories,
server.categories,
() => setStatus("editing"),
);
const defaultCategory = useMemo(() => {
return {
title: "Uncategorized",
channels: [...server.channels]
.filter((x) => x)
.map((x) => x!._id)
.filter(
(x) => !categories.find((cat) => cat.channels.includes(x)),
),
id: "none",
};
}, [categories, server.channels]);
return ( return (
<div> <>
<Tip warning>This section is under construction.</Tip> <Header>
<p> <h1>
<Button <Text id={`app.settings.server_pages.categories.title`} />
contrast </h1>
disabled={isEqual(server.categories ?? [], cats)} <SaveStatus status={status} />
onClick={() => server.edit({ categories: cats })}> </Header>
save categories <DragDropContext
</Button> onDragEnd={(target) => {
</p> const { destination, source, draggableId, type } = target;
<h2>categories</h2>
{cats.map((category) => ( if (
<div style={{ background: "var(--hover)" }} key={category.id}> !destination ||
<InputBox (destination.droppableId === source.droppableId &&
value={category.title} destination.index === source.index)
onChange={(e) => ) {
setCats( return;
cats.map((y) => }
y.id === category.id
? { if (type === "column") {
...y, if (destination.index === 0) return;
title: e.currentTarget.value,
} // Remove from array.
: y, const cat = categories.find(
), (x) => x.id === draggableId,
) );
} const arr = categories.filter(
contrast (x) => x.id !== draggableId,
/> );
<Button
contrast // Insert at new position.
onClick={() => arr.splice(destination.index - 1, 0, cat!);
setCats(cats.filter((x) => x.id !== category.id)) setCategories(arr);
}> } else {
delete {category.title} setCategories(
</Button> categories.map((category) => {
</div> if (category.id === destination.droppableId) {
))} const channels = category.channels.filter(
<h2>create new</h2> (x) => x !== draggableId,
<p> );
<InputBox
value={name} channels.splice(
onChange={(e) => setName(e.currentTarget.value)} destination.index,
contrast 0,
/> draggableId,
<Button );
contrast
onClick={() => { return {
setName(""); ...category,
setCats([ channels,
...cats, };
{ } else if (category.id === source.droppableId) {
id: ulid(), return {
title: name, ...category,
channels: [], channels: category.channels.filter(
}, (x) => x !== draggableId,
]); ),
}}> };
create }
</Button>
</p> return category;
<h2>channels</h2> }),
{channels.map((channel) => { );
return ( }
<div }}>
key={channel!._id} <FullSize>
style={{ <Droppable
display: "flex", droppableId="categories"
gap: "12px", direction="horizontal"
alignItems: "center", type="column">
}}> {(provided) => (
<div style={{ flexShrink: 0 }}> <div
<ChannelIcon target={channel} size={24} />{" "} ref={provided.innerRef}
<span>{channel!.name}</span> {...provided.droppableProps}>
</div> <KanbanBoard>
<ComboBox <ListElement
style={{ flexGrow: 1 }} category={defaultCategory}
value={ server={server}
cats.find((x) => index={0}
x.channels.includes(channel!._id), addChannel={noop}
)?.id ?? "none" />
} {categories.map((category, index) => (
onChange={(e) => <ListElement
setCats( draggable
cats.map((x) => { category={category}
return { server={server}
...x, index={index + 1}
channels: [ key={category.id}
...x.channels.filter( setTitle={(title) => {
(y) => y !== channel!._id, setCategories(
), categories.map((x) =>
...(e.currentTarget.value === x.id === category.id
x.id ? {
? [channel!._id] ...x,
: []), title,
], }
}; : x,
}), ),
) );
}> }}
<option value="none">Uncategorised</option> deleteSelf={() =>
{cats.map((x) => ( setCategories(
<option key={x.id} value={x.id}> categories.filter(
{x.title} (x) =>
</option> x.id !==
))} category.id,
</ComboBox> ),
</div> )
); }
})} addChannel={(channel) => {
</div> setCategories(
categories.map((x) =>
x.id === category.id
? {
...x,
channels: [
...x.channels,
channel._id,
],
}
: x,
),
);
}}
/>
))}
<KanbanList last>
<div class="inner">
<KanbanListHeader
onClick={() =>
setCategories([
...categories,
{
id: ulid(),
title: "New Category",
channels: [],
},
])
}>
<Plus size={24} />
</KanbanListHeader>
</div>
</KanbanList>
{provided.placeholder}
</KanbanBoard>
</div>
)}
</Droppable>
</FullSize>
</DragDropContext>
</>
); );
}); });
function ListElement({
category,
server,
index,
setTitle,
deleteSelf,
addChannel,
draggable,
}: {
category: Category;
server: Server;
index: number;
setTitle?: (title: string) => void;
deleteSelf?: () => void;
addChannel: (channel: TextChannel | VoiceChannel) => void;
draggable?: boolean;
}) {
const { openScreen } = useIntermediate();
const [editing, setEditing] = useState<string>();
const startEditing = () => setTitle && setEditing(category.title);
const save = useCallback(() => {
setEditing(undefined);
setTitle!(editing!);
}, [editing, setTitle]);
useEffect(() => {
if (!editing) return;
function onClick(ev: MouseEvent) {
if ((ev.target as HTMLElement)?.id !== category.id) {
save();
}
}
document.addEventListener("mousedown", onClick);
return () => document.removeEventListener("mousedown", onClick);
}, [editing, category.id, save]);
return (
<Draggable
isDragDisabled={!draggable}
key={category.id}
draggableId={category.id}
index={index}>
{(provided) => (
<div {...provided.draggableProps} ref={provided.innerRef}>
<KanbanList last={false} key={category.id}>
<div class="inner">
<Row>
<KanbanListHeader {...provided.dragHandleProps}>
{editing ? (
<input
value={editing}
onChange={(e) =>
setEditing(
e.currentTarget.value,
)
}
onKeyDown={(e) =>
e.key === "Enter" && save()
}
id={category.id}
/>
) : (
<span onClick={startEditing}>
{category.title}
</span>
)}
</KanbanListHeader>
{deleteSelf && (
<KanbanListHeader onClick={deleteSelf}>
<X size={24} />
</KanbanListHeader>
)}
</Row>
<Droppable
droppableId={category.id}
key={category.id}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}>
{category.channels.map((x, index) => {
const channel =
server.client.channels.get(x);
if (!channel) return null;
return (
<Draggable
key={x}
draggableId={x}
index={index}>
{(provided) => (
<div
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={
provided.innerRef
}>
<KanbanEntry>
<div class="inner">
<ChannelIcon
target={
channel
}
size={
24
}
/>
<span>
{
channel.name
}
</span>
</div>
</KanbanEntry>
</div>
)}
</Draggable>
);
})}
{provided.placeholder}
</div>
)}
</Droppable>
<KanbanListHeader
onClick={() =>
openScreen({
id: "special_prompt",
type: "create_channel",
target: server,
cb: addChannel,
})
}>
<Plus size={24} />
</KanbanListHeader>
</div>
</KanbanList>
</div>
)}
</Draggable>
);
}

View file

@ -1398,6 +1398,13 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
"@types/react-beautiful-dnd@^13.1.2":
version "13.1.2"
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz#510405abb09f493afdfd898bf83995dc6385c130"
integrity sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg==
dependencies:
"@types/react" "*"
"@types/react-helmet@^6.1.1": "@types/react-helmet@^6.1.1":
version "6.1.2" version "6.1.2"
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.2.tgz#e9d7d16b29e4ec5716711c52c35c3cec45819eac" resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.2.tgz#e9d7d16b29e4ec5716711c52c35c3cec45819eac"
@ -1981,6 +1988,13 @@ crypto-random-string@^2.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
css-box-model@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
dependencies:
tiny-invariant "^1.0.6"
css-color-keywords@^1.0.0: css-color-keywords@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05"
@ -3069,6 +3083,11 @@ mdurl@^1.0.1:
sdp-transform "^2.14.1" sdp-transform "^2.14.1"
supports-color "^8.1.1" supports-color "^8.1.1"
memoize-one@^5.1.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
merge-stream@^2.0.0: merge-stream@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@ -3365,6 +3384,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
raf-schd@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
randombytes@^2.1.0: randombytes@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@ -3372,6 +3396,19 @@ randombytes@^2.1.0:
dependencies: dependencies:
safe-buffer "^5.1.0" safe-buffer "^5.1.0"
react-beautiful-dnd@^13.1.0:
version "13.1.0"
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d"
integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==
dependencies:
"@babel/runtime" "^7.9.2"
css-box-model "^1.2.0"
memoize-one "^5.1.1"
raf-schd "^4.0.2"
react-redux "^7.2.0"
redux "^4.0.4"
use-memo-one "^1.1.1"
react-device-detect@^1.17.0: react-device-detect@^1.17.0:
version "1.17.0" version "1.17.0"
resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-1.17.0.tgz#a00b4fd6880cebfab3fd8a42a79dc0290cdddca9" resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-1.17.0.tgz#a00b4fd6880cebfab3fd8a42a79dc0290cdddca9"
@ -3409,6 +3446,18 @@ react-overlapping-panels@1.2.2:
resolved "https://registry.yarnpkg.com/react-overlapping-panels/-/react-overlapping-panels-1.2.2.tgz#16b60ed60045a7fa40bcf321de113c655f6e0acd" resolved "https://registry.yarnpkg.com/react-overlapping-panels/-/react-overlapping-panels-1.2.2.tgz#16b60ed60045a7fa40bcf321de113c655f6e0acd"
integrity sha512-jZ8ZT4tnqM2YQF91Ct+9dLk7rSjnNiudxzgKlsaVfgwEjdBAWtE8nWJX9d2jDZZ9qimWgg43u5+SF6U+ELjyKQ== integrity sha512-jZ8ZT4tnqM2YQF91Ct+9dLk7rSjnNiudxzgKlsaVfgwEjdBAWtE8nWJX9d2jDZZ9qimWgg43u5+SF6U+ELjyKQ==
react-redux@^7.2.0:
version "7.2.5"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.5.tgz#213c1b05aa1187d9c940ddfc0b29450957f6a3b8"
integrity sha512-Dt29bNyBsbQaysp6s/dN0gUodcq+dVKKER8Qv82UrpeygwYeX1raTtil7O/fftw/rFqzaf6gJhDZRkkZnn6bjg==
dependencies:
"@babel/runtime" "^7.12.1"
"@types/react-redux" "^7.1.16"
hoist-non-react-statics "^3.3.2"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^16.13.1"
react-redux@^7.2.4: react-redux@^7.2.4:
version "7.2.4" version "7.2.4"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
@ -3484,7 +3533,7 @@ readdirp@~3.6.0:
dependencies: dependencies:
picomatch "^2.2.1" picomatch "^2.2.1"
redux@^4.0.0, redux@^4.1.0: redux@^4.0.0, redux@^4.0.4, redux@^4.1.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.1.tgz#76f1c439bb42043f985fbd9bf21990e60bd67f47" resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.1.tgz#76f1c439bb42043f985fbd9bf21990e60bd67f47"
integrity sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw== integrity sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==
@ -3963,7 +4012,7 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
tiny-invariant@^1.0.2: tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
@ -4115,6 +4164,11 @@ uri-js@^4.2.2:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
use-memo-one@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
use-resize-observer@^7.0.0: use-resize-observer@^7.0.0:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-7.1.0.tgz#709ea7540fbe0a60ceae41ee2bef933d7782e4d4" resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-7.1.0.tgz#709ea7540fbe0a60ceae41ee2bef933d7782e4d4"