Add multi selection to tile grouping
This commit is contained in:
parent
68a084ebc9
commit
71b8aeec6c
@ -31,6 +31,7 @@
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.unset": "^4.5.2",
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"pepjs": "^0.5.3",
|
||||
"polygon-clipping": "^0.15.2",
|
||||
|
@ -22,7 +22,7 @@ function DiceTiles({ dice, onDiceSelect, selectedDice, onDone }) {
|
||||
minHeight: layout.screenSize === "large" ? "600px" : "400px",
|
||||
}}
|
||||
gap={2}
|
||||
columns={layout.gridTemplate}
|
||||
columns={`repeat${layout.tileGridColumns}, 1fr`}
|
||||
>
|
||||
{dice.map((dice) => (
|
||||
<DiceTile
|
||||
|
@ -16,8 +16,12 @@ function MapTileGroup({ group, maps, isSelected, onSelect, onDoubleClick }) {
|
||||
onSelect={() => onSelect(group.id)}
|
||||
onDoubleClick={onDoubleClick}
|
||||
>
|
||||
<Grid columns={layout.groupGridTemplate} p={2} sx={{ gridGap: 2 }}>
|
||||
{maps.slice(0, 16).map((map) => (
|
||||
<Grid
|
||||
columns={`repeat(${layout.groupGridColumns}, 1fr)`}
|
||||
p={2}
|
||||
sx={{ gridGap: 2 }}
|
||||
>
|
||||
{maps.slice(0, 9).map((map) => (
|
||||
<MapTileImage
|
||||
sx={{ borderRadius: "8px" }}
|
||||
map={map}
|
||||
|
@ -9,7 +9,7 @@ import { getGroupItems } from "../../helpers/group";
|
||||
|
||||
import { useGroup } from "../../contexts/GroupContext";
|
||||
|
||||
function MapTiles({ maps, onMapEdit, onMapSelect, subgroup }) {
|
||||
function MapTiles({ maps, onMapEdit, onMapSelect, subgroup, columns }) {
|
||||
const {
|
||||
groups,
|
||||
selectedGroupIds,
|
||||
@ -60,11 +60,13 @@ function MapTiles({ maps, onMapEdit, onMapSelect, subgroup }) {
|
||||
return (
|
||||
<SortableTiles
|
||||
groups={subgroup ? openGroupItems : groups}
|
||||
selectedGroupIds={selectedGroupIds}
|
||||
onGroupChange={onGroupsChange}
|
||||
renderTile={renderTile}
|
||||
onTileSelect={onGroupSelect}
|
||||
disableGrouping={subgroup}
|
||||
openGroupId={openGroupId}
|
||||
columns={columns}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -4,25 +4,23 @@ import { useDroppable } from "@dnd-kit/core";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { animated, useSpring } from "react-spring";
|
||||
|
||||
function SortableTile({ id, disableGrouping, hidden, children, type }) {
|
||||
function SortableTile({ id, disableGrouping, hidden, children, isDragging }) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
isDragging,
|
||||
setDroppableNodeRef,
|
||||
setDraggableNodeRef,
|
||||
over,
|
||||
active,
|
||||
} = useSortable({ id, data: { type } });
|
||||
} = useSortable({ id });
|
||||
const { setNodeRef: setGroupNodeRef } = useDroppable({
|
||||
id: `__group__${id}`,
|
||||
disabled: disableGrouping || active?.data?.current?.type === "group",
|
||||
disabled: disableGrouping,
|
||||
});
|
||||
|
||||
const dragStyle = {
|
||||
cursor: "pointer",
|
||||
opacity: isDragging ? 0.25 : undefined,
|
||||
zIndex: isDragging ? 100 : undefined,
|
||||
};
|
||||
|
||||
// Sort div left aligned
|
||||
|
@ -12,17 +12,20 @@ import {
|
||||
import { SortableContext, arrayMove } from "@dnd-kit/sortable";
|
||||
import { animated, useSpring, config } from "react-spring";
|
||||
|
||||
import { combineGroups, moveGroups } from "../../helpers/group";
|
||||
import { moveGroups } from "../../helpers/group";
|
||||
import { keyBy } from "../../helpers/shared";
|
||||
|
||||
import SortableTile from "./SortableTile";
|
||||
|
||||
function SortableTiles({
|
||||
groups,
|
||||
selectedGroupIds,
|
||||
onGroupChange,
|
||||
renderTile,
|
||||
onTileSelect,
|
||||
disableGrouping,
|
||||
openGroupId,
|
||||
columns,
|
||||
}) {
|
||||
const mouseSensor = useSensor(MouseSensor, {
|
||||
activationConstraint: { delay: 250, tolerance: 5 },
|
||||
@ -39,8 +42,10 @@ function SortableTiles({
|
||||
function handleDragStart({ active, over }) {
|
||||
setDragId(active.id);
|
||||
setOverId(over?.id);
|
||||
if (!selectedGroupIds.includes(active.id)) {
|
||||
onTileSelect(active.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragOver({ over }) {
|
||||
setOverId(over?.id);
|
||||
@ -58,16 +63,24 @@ function SortableTiles({
|
||||
if (overId === active.id) {
|
||||
return;
|
||||
}
|
||||
const activeGroupIndex = groups.findIndex(
|
||||
(group) => group.id === active.id
|
||||
);
|
||||
|
||||
let newGroups = groups;
|
||||
const overGroupIndex = groups.findIndex((group) => group.id === overId);
|
||||
onGroupChange(moveGroups(groups, overGroupIndex, activeGroupIndex));
|
||||
const selectedGroupIndices = selectedGroupIds.map((groupId) =>
|
||||
groups.findIndex((group) => group.id === groupId)
|
||||
);
|
||||
onGroupChange(
|
||||
moveGroups(newGroups, overGroupIndex, selectedGroupIndices)
|
||||
);
|
||||
onTileSelect();
|
||||
} else if (active.id !== over.id) {
|
||||
const oldIndex = groups.findIndex((group) => group.id === active.id);
|
||||
const newIndex = groups.findIndex((group) => group.id === over.id);
|
||||
onGroupChange(arrayMove(groups, oldIndex, newIndex));
|
||||
let newGroups = groups;
|
||||
for (let groupId of selectedGroupIds) {
|
||||
const oldIndex = newGroups.findIndex((group) => group.id === groupId);
|
||||
const newIndex = newGroups.findIndex((group) => group.id === over.id);
|
||||
newGroups = arrayMove(newGroups, oldIndex, newIndex);
|
||||
}
|
||||
onGroupChange(newGroups);
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,19 +92,88 @@ function SortableTiles({
|
||||
const overGroupId =
|
||||
overId && overId.startsWith("__group__") && overId.slice(9);
|
||||
|
||||
function renderSortableGroup(group) {
|
||||
function renderSortableGroup(group, selectedGroups) {
|
||||
if (overGroupId === group.id && dragId && group.id !== dragId) {
|
||||
// If dragging over a group render a preview of that group
|
||||
return renderTile(
|
||||
combineGroups(
|
||||
group,
|
||||
groups.find((group) => group.id === dragId)
|
||||
)
|
||||
);
|
||||
const previewGroup = moveGroups(
|
||||
[group, ...selectedGroups],
|
||||
0,
|
||||
selectedGroups.map((_, i) => i + 1)
|
||||
)[0];
|
||||
return renderTile(previewGroup);
|
||||
}
|
||||
return renderTile(group);
|
||||
}
|
||||
|
||||
function renderDragOverlays() {
|
||||
let selectedIndices = selectedGroupIds.map((groupId) =>
|
||||
groups.findIndex((group) => group.id === groupId)
|
||||
);
|
||||
let activeIndex = groups.findIndex((group) => group.id === dragId);
|
||||
|
||||
const coords = selectedIndices.map((index) => ({
|
||||
x: index % columns,
|
||||
y: Math.floor(index / columns),
|
||||
}));
|
||||
const activeCoord = {
|
||||
x: activeIndex % columns,
|
||||
y: Math.floor(activeIndex / columns),
|
||||
};
|
||||
|
||||
const relativeCoords = coords.map(({ x, y }) => ({
|
||||
x: x - activeCoord.x,
|
||||
y: y - activeCoord.y,
|
||||
}));
|
||||
|
||||
return selectedGroupIds.map((groupId, index) => (
|
||||
<DragOverlay dropAnimation={null} key={groupId}>
|
||||
<div
|
||||
style={{
|
||||
transform: `translate(${relativeCoords[index].x * 100}%, ${
|
||||
relativeCoords[index].y * 100
|
||||
}%)`,
|
||||
}}
|
||||
>
|
||||
<animated.div style={dragBounce}>
|
||||
{renderTile(groups.find((group) => group.id === groupId))}
|
||||
</animated.div>
|
||||
</div>
|
||||
</DragOverlay>
|
||||
));
|
||||
}
|
||||
|
||||
function renderTiles() {
|
||||
const groupsByIds = keyBy(groups, "id");
|
||||
const selectedGroupIdsSet = new Set(selectedGroupIds);
|
||||
let selectedGroups = [];
|
||||
let hasSelectedContainerGroup = false;
|
||||
for (let groupId of selectedGroupIds) {
|
||||
const group = groupsByIds[groupId];
|
||||
if (group) {
|
||||
selectedGroups.push(group);
|
||||
if (group.type === "group") {
|
||||
hasSelectedContainerGroup = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return groups.map((group) => {
|
||||
const isDragging = dragId && selectedGroupIdsSet.has(group.id);
|
||||
const disableTileGrouping =
|
||||
disableGrouping || isDragging || hasSelectedContainerGroup;
|
||||
return (
|
||||
<SortableTile
|
||||
id={group.id}
|
||||
key={group.id}
|
||||
disableGrouping={disableTileGrouping}
|
||||
hidden={group.id === openGroupId}
|
||||
isDragging={isDragging}
|
||||
>
|
||||
{renderSortableGroup(group, selectedGroups)}
|
||||
</SortableTile>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
onDragStart={handleDragStart}
|
||||
@ -101,27 +183,8 @@ function SortableTiles({
|
||||
collisionDetection={closestCenter}
|
||||
>
|
||||
<SortableContext items={groups}>
|
||||
{groups.map((group) => (
|
||||
<SortableTile
|
||||
id={group.id}
|
||||
key={group.id}
|
||||
disableGrouping={disableGrouping}
|
||||
hidden={group.id === openGroupId}
|
||||
type={group.type}
|
||||
>
|
||||
{renderSortableGroup(group)}
|
||||
</SortableTile>
|
||||
))}
|
||||
{createPortal(
|
||||
<DragOverlay dropAnimation={null}>
|
||||
{dragId && (
|
||||
<animated.div style={dragBounce}>
|
||||
{renderTile(groups.find((group) => group.id === dragId))}
|
||||
</animated.div>
|
||||
)}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)}
|
||||
{renderTiles()}
|
||||
{createPortal(dragId && renderDragOverlays(), document.body)}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
|
@ -6,7 +6,7 @@ import { useGroup } from "../../contexts/GroupContext";
|
||||
|
||||
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
||||
|
||||
function TilesContainer({ children }) {
|
||||
function TilesContainer({ columns, children }) {
|
||||
const { onGroupSelect } = useGroup();
|
||||
|
||||
const layout = useResponsiveLayout();
|
||||
@ -31,7 +31,7 @@ function TilesContainer({ children }) {
|
||||
overflow: "hidden",
|
||||
}}
|
||||
gap={2}
|
||||
columns={layout.gridTemplate}
|
||||
columns={`repeat(${columns}, 1fr)`}
|
||||
>
|
||||
{children}
|
||||
</Grid>
|
||||
|
@ -6,9 +6,7 @@ import SimpleBar from "simplebar-react";
|
||||
|
||||
import { useGroup } from "../../contexts/GroupContext";
|
||||
|
||||
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
||||
|
||||
function TilesOverlay({ children }) {
|
||||
function TilesOverlay({ columns, children }) {
|
||||
const { openGroupId, onGroupClose, onGroupSelect } = useGroup();
|
||||
|
||||
const openAnimation = useSpring({
|
||||
@ -23,8 +21,6 @@ function TilesOverlay({ children }) {
|
||||
setContinerSize(size);
|
||||
}
|
||||
|
||||
const layout = useResponsiveLayout();
|
||||
|
||||
return (
|
||||
<>
|
||||
{openGroupId && (
|
||||
@ -92,7 +88,7 @@ function TilesOverlay({ children }) {
|
||||
overflow: "hidden",
|
||||
}}
|
||||
gap={2}
|
||||
columns={layout.groupGridTemplate}
|
||||
columns={`repeat(${columns}, 1fr)`}
|
||||
px={3}
|
||||
>
|
||||
{children}
|
||||
|
@ -23,8 +23,12 @@ function TokenTileGroup({
|
||||
onDoubleClick={onDoubleClick}
|
||||
columns="1fr 1fr"
|
||||
>
|
||||
<Grid columns={layout.groupGridTemplate} p={2} sx={{ gridGap: 2 }}>
|
||||
{tokens.slice(0, 16).map((token) => (
|
||||
<Grid
|
||||
columns={`repeat(${layout.groupGridColumns}, 1fr)`}
|
||||
p={2}
|
||||
sx={{ gridGap: 2 }}
|
||||
>
|
||||
{tokens.slice(0, 9).map((token) => (
|
||||
<TokenTileImage
|
||||
sx={{ borderRadius: "8px" }}
|
||||
token={token}
|
||||
|
@ -9,7 +9,7 @@ import { getGroupItems } from "../../helpers/group";
|
||||
|
||||
import { useGroup } from "../../contexts/GroupContext";
|
||||
|
||||
function TokenTiles({ tokens, onTokenEdit, subgroup }) {
|
||||
function TokenTiles({ tokens, onTokenEdit, subgroup, columns }) {
|
||||
const {
|
||||
groups,
|
||||
selectedGroupIds,
|
||||
@ -65,11 +65,13 @@ function TokenTiles({ tokens, onTokenEdit, subgroup }) {
|
||||
return (
|
||||
<SortableTiles
|
||||
groups={subgroup ? openGroupItems : groups}
|
||||
selectedGroupIds={selectedGroupIds}
|
||||
onGroupChange={onGroupsChange}
|
||||
renderTile={renderTile}
|
||||
onTileSelect={onGroupSelect}
|
||||
disableGrouping={subgroup}
|
||||
openGroupId={openGroupId}
|
||||
columns={columns}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import { decode } from "@msgpack/msgpack";
|
||||
import { useAuth } from "./AuthContext";
|
||||
import { useDatabase } from "./DatabaseContext";
|
||||
|
||||
import { applyObservableChange } from "../helpers/dexie";
|
||||
|
||||
const MapDataContext = React.createContext();
|
||||
|
||||
const defaultMapState = {
|
||||
@ -207,7 +209,9 @@ export function MapDataProvider({ children }) {
|
||||
}
|
||||
if (change.table === "groups") {
|
||||
if (change.type === 2 && change.key === "maps") {
|
||||
setMapGroups(change.obj.items);
|
||||
const group = applyObservableChange(change);
|
||||
const groups = group.items.filter((item) => item !== null);
|
||||
setMapGroups(groups);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import { decode } from "@msgpack/msgpack";
|
||||
import { useAuth } from "./AuthContext";
|
||||
import { useDatabase } from "./DatabaseContext";
|
||||
|
||||
import { applyObservableChange } from "../helpers/dexie";
|
||||
|
||||
const TokenDataContext = React.createContext();
|
||||
|
||||
export function TokenDataProvider({ children }) {
|
||||
@ -139,7 +141,9 @@ export function TokenDataProvider({ children }) {
|
||||
}
|
||||
if (change.table === "groups") {
|
||||
if (change.type === 2 && change.key === "tokens") {
|
||||
setTokenGroups(change.obj.items);
|
||||
const group = applyObservableChange(change);
|
||||
const groups = group.items.filter((item) => item !== null);
|
||||
setTokenGroups(groups);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
19
src/helpers/dexie.js
Normal file
19
src/helpers/dexie.js
Normal file
@ -0,0 +1,19 @@
|
||||
import set from "lodash.set";
|
||||
import unset from "lodash.unset";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
|
||||
export function applyObservableChange(change) {
|
||||
// Custom application of dexie change to fix issue with array indices being wrong
|
||||
// https://github.com/dfahlander/Dexie.js/issues/1176
|
||||
// TODO: Fix dexie observable source
|
||||
let obj = cloneDeep(change.oldObj);
|
||||
const changes = Object.entries(change.mods).reverse();
|
||||
for (let [key, value] of changes) {
|
||||
if (value === null) {
|
||||
unset(obj, key);
|
||||
} else {
|
||||
obj = set(obj, key, value);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
@ -99,19 +99,33 @@ export function combineGroups(a, b) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutably move group at `bIndex` into `aIndex`
|
||||
* Immutably move group at indices `from` into index `to`
|
||||
* @param {Group[]} groups
|
||||
* @param {number} aIndex
|
||||
* @param {number} bIndex
|
||||
* @param {number} to
|
||||
* @param {number[]} from
|
||||
* @returns {Group[]}
|
||||
*/
|
||||
export function moveGroups(groups, aIndex, bIndex) {
|
||||
const aGroup = groups[aIndex];
|
||||
const bGroup = groups[bIndex];
|
||||
const newGroup = combineGroups(aGroup, bGroup);
|
||||
export function moveGroups(groups, to, from) {
|
||||
const newGroups = cloneDeep(groups);
|
||||
newGroups[aIndex] = newGroup;
|
||||
newGroups.splice(bIndex, 1);
|
||||
|
||||
const toGroup = newGroups[to];
|
||||
let fromGroups = [];
|
||||
for (let i of from) {
|
||||
fromGroups.push(newGroups[i]);
|
||||
}
|
||||
|
||||
let combined = toGroup;
|
||||
for (let fromGroup of fromGroups) {
|
||||
combined = combineGroups(combined, fromGroup);
|
||||
}
|
||||
|
||||
// Replace and remove old groups
|
||||
newGroups[to] = combined;
|
||||
for (let fromGroup of fromGroups) {
|
||||
const i = newGroups.findIndex((group) => group.id === fromGroup.id);
|
||||
newGroups.splice(i, 1);
|
||||
}
|
||||
|
||||
return newGroups;
|
||||
}
|
||||
|
||||
|
@ -21,13 +21,9 @@ function useResponsiveLayout() {
|
||||
? "medium"
|
||||
: "large";
|
||||
|
||||
const gridTemplate = isLargeScreen
|
||||
? "1fr 1fr 1fr 1fr"
|
||||
: isMediumScreen
|
||||
? "1fr 1fr 1fr"
|
||||
: "1fr 1fr";
|
||||
const tileGridColumns = isLargeScreen ? 4 : isMediumScreen ? 3 : 2;
|
||||
|
||||
const groupGridTemplate = isLargeScreen ? "1fr 1fr 1fr" : "1fr 1fr";
|
||||
const groupGridColumns = isLargeScreen ? 3 : 2;
|
||||
|
||||
const tileContainerHeight = isLargeScreen ? "600px" : "400px";
|
||||
|
||||
@ -35,9 +31,9 @@ function useResponsiveLayout() {
|
||||
screenSize,
|
||||
modalSize,
|
||||
tileSize,
|
||||
gridTemplate,
|
||||
tileGridColumns,
|
||||
tileContainerHeight,
|
||||
groupGridTemplate,
|
||||
groupGridColumns,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -259,19 +259,21 @@ function SelectMapModal({
|
||||
onGroupsSelect={setSelectedGroupIds}
|
||||
disabled={!isOpen}
|
||||
>
|
||||
<TilesContainer>
|
||||
<TilesContainer columns={layout.tileGridColumns}>
|
||||
<MapTiles
|
||||
maps={maps}
|
||||
onMapEdit={() => setIsEditModalOpen(true)}
|
||||
onMapSelect={handleMapSelect}
|
||||
columns={layout.tileGridColumns}
|
||||
/>
|
||||
</TilesContainer>
|
||||
<TilesOverlay>
|
||||
<TilesOverlay columns={layout.groupGridColumns}>
|
||||
<MapTiles
|
||||
maps={maps}
|
||||
onMapEdit={() => setIsEditModalOpen(true)}
|
||||
onMapSelect={handleMapSelect}
|
||||
subgroup
|
||||
columns={layout.groupGridColumns}
|
||||
/>
|
||||
</TilesOverlay>
|
||||
</GroupProvider>
|
||||
|
@ -200,17 +200,19 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
onGroupsSelect={setSelectedGroupIds}
|
||||
disabled={!isOpen}
|
||||
>
|
||||
<TilesContainer>
|
||||
<TilesContainer columns={layout.tileGridColumns}>
|
||||
<TokenTiles
|
||||
tokens={tokens}
|
||||
onTokenEdit={() => setIsEditModalOpen(true)}
|
||||
columns={layout.tileGridColumns}
|
||||
/>
|
||||
</TilesContainer>
|
||||
<TilesOverlay>
|
||||
<TilesOverlay columns={layout.groupGridColumns}>
|
||||
<TokenTiles
|
||||
tokens={tokens}
|
||||
onTokenEdit={() => setIsEditModalOpen(true)}
|
||||
subgroup
|
||||
columns={layout.groupGridColumns}
|
||||
/>
|
||||
</TilesOverlay>
|
||||
</GroupProvider>
|
||||
|
10
yarn.lock
10
yarn.lock
@ -5724,6 +5724,11 @@ entities@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
|
||||
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
|
||||
|
||||
err-code@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
|
||||
integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==
|
||||
|
||||
err-code@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920"
|
||||
@ -8621,6 +8626,11 @@ lodash.uniq@^4.5.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||
|
||||
lodash.unset@^4.5.2:
|
||||
version "4.5.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.unset/-/lodash.unset-4.5.2.tgz#370d1d3e85b72a7e1b0cdf2d272121306f23e4ed"
|
||||
integrity sha1-Nw0dPoW3Kn4bDN8tJyEhMG8j5O0=
|
||||
|
||||
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.5:
|
||||
version "4.17.20"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||
|
Loading…
Reference in New Issue
Block a user