diff --git a/package.json b/package.json
index cc6f730..e810831 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"nanostores": "^0.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-dropzone": "^14.2.3",
"react-redux": "^9.1.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ba07f93..f155690 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -71,6 +71,9 @@ dependencies:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
+ react-dropzone:
+ specifier: ^14.2.3
+ version: 14.2.3(react@18.2.0)
react-redux:
specifier: ^9.1.0
version: 9.1.0(@types/react@18.2.57)(react@18.2.0)(redux@5.0.1)
@@ -4516,6 +4519,11 @@ packages:
hasBin: true
dev: true
+ /attr-accept@2.2.2:
+ resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==}
+ engines: {node: '>=4'}
+ dev: false
+
/autoprefixer@10.4.17(postcss@8.4.35):
resolution: {integrity: sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==}
engines: {node: ^10 || ^12 || >=14}
@@ -6806,6 +6814,13 @@ packages:
webpack: 5.90.3
dev: false
+ /file-selector@0.6.0:
+ resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==}
+ engines: {node: '>= 12'}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
/filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
dependencies:
@@ -11071,6 +11086,18 @@ packages:
scheduler: 0.23.0
dev: false
+ /react-dropzone@14.2.3(react@18.2.0):
+ resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==}
+ engines: {node: '>= 10.13'}
+ peerDependencies:
+ react: '>= 16.8 || 18.0.0'
+ dependencies:
+ attr-accept: 2.2.2
+ file-selector: 0.6.0
+ prop-types: 15.8.1
+ react: 18.2.0
+ dev: false
+
/react-error-overlay@6.0.11:
resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==}
dev: false
diff --git a/src/App.tsx b/src/App.tsx
index bf01b0e..3fa48b6 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,9 +1,12 @@
-import React, { ChangeEvent } from 'react';
+import React, { ChangeEvent, useEffect } from 'react';
import './App.css';
import PromptComposer from './components/PromptComposer';
import { Box, Tabs, Tab, Typography } from '@material-ui/core';
-import { $textComposition } from './lib/prompt';
+import { $composition, $library, $textComposition, Category, Composition, Library, LibraryItem, addItemToLibrary, insertIntoComposition, lassoNuggets } from './lib/prompt';
import { TextPrompt } from './components/TextPrompt';
+import { useStore } from '@nanostores/react';
+import { Op } from './lib/operator';
+import { v4 as uuid4 } from 'uuid';
interface TabPanelProps {
@@ -42,6 +45,30 @@ function a11yProps(index: number) {
function App() {
const [value, setValue] = React.useState(0);
+ const fillWithMockData = () => {
+ const libItems = [
+ { id: uuid4(), prompt: "cookie", category: Category.subject },
+ { id: uuid4(), prompt: "chocolate", category: Category.subject },
+ { id: uuid4(), prompt: "vintage photo", category: Category.vibes },
+ ] as Library;
+ const promptItems = [
+ {
+ id: uuid4(), items: [
+ { id: uuid4(), item: libItems[0] },
+ { id: uuid4(), item: libItems[1] },
+ ], op: Op.AND,
+ },
+ { id: uuid4(), item: libItems[2] },
+ ] as Composition;
+
+ $library.set(libItems);
+ $composition.set(promptItems);
+ }
+
+ useEffect(() => {
+ fillWithMockData();
+ }, []);
+
const handleChange = (event: ChangeEvent<{}>, newValue: number) => {
setValue(newValue);
};
diff --git a/src/components/NewLibraryItem.test.tsx b/src/components/NewLibraryItem.test.tsx
index eb18e41..52f304c 100644
--- a/src/components/NewLibraryItem.test.tsx
+++ b/src/components/NewLibraryItem.test.tsx
@@ -4,7 +4,6 @@ import {NewLibraryItem} from './NewLibraryItem';
import { Category, addItemToLibrary, categoryHasName } from '../lib/prompt';
jest.mock('../lib/prompt', () => ({
- Category: Category,
addItemToLibrary: jest.fn(),
categoryHasName: jest.fn(),
}));
diff --git a/src/components/NewLibraryItem.tsx b/src/components/NewLibraryItem.tsx
index 0b68081..ef32922 100644
--- a/src/components/NewLibraryItem.tsx
+++ b/src/components/NewLibraryItem.tsx
@@ -2,7 +2,7 @@ import { Button, FormControl, InputLabel, MenuItem, TextField } from "@material-
import Select, { SelectChangeEvent } from '@mui/material/Select';
import { Category, LibraryItem, addItemToLibrary, categoryHasName } from "../lib/prompt";
import { ChangeEvent, useState } from "react";
-import {v4 as uuidv4} from "uuid"
+import { v4 as uuidv4 } from "uuid"
export interface NewLibraryItemProps {
onNewCreated?: () => void;
@@ -47,8 +47,8 @@ export function NewLibraryItem(props: NewLibraryItemProps) {
return (
-
-
+
+
Category
-
-
-
-
+
+
{categoryHasName(category) ? (Name) : <>>}
{categoryHasName(category) ? () : <>>}
-
-
-
-
+
+
Prompt
-
-
+
+
)
}
\ No newline at end of file
diff --git a/src/components/Nugget.tsx b/src/components/Nugget.tsx
index f9221b4..db5b939 100644
--- a/src/components/Nugget.tsx
+++ b/src/components/Nugget.tsx
@@ -1,34 +1,93 @@
import { Button, ButtonGroup, Chip, Divider } from '@material-ui/core';
-import React, { Component, useState } from 'react';
+import React, { Component, DragEvent, useEffect, useState } from 'react';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowUp';
-
+import { $composition, Nugget as NuggetType, decreaseNuggetScore, increaseNuggetScore } from '../lib/prompt';
import "./Nugget.css";
-import { Nugget as NuggetType, decreaseNuggetScore, increaseNuggetScore } from '../lib/prompt';
+import "./PromptItem.css"
+import { $sourceItem, cancelDrop, completeDrop, isPromptItemDropTarget, startDrag, startHoverOver } from '../store/prompt-dnd';
+import { PromptItemProps } from './PromptItem';
+import { useStore } from '@nanostores/react';
-export interface NuggetProps {
- nugget : NuggetType,
+export interface NuggetProps extends PromptItemProps {
+ nugget: NuggetType,
}
-export default function Nugget(props : NuggetProps) {
- const {nugget} = props;
+export default function Nugget(props: NuggetProps) {
+
+ const { nugget,
+ onDragStart,
+ onDragOver,
+ onDragEnd,
+ onDrop,
+ onMouseEnter,
+ onMouseLeave,
+ } = props;
const scoreDisp = nugget.score > 0 ? "+" + nugget.score : nugget.score;
-
+
+ const sourceItem = useStore($sourceItem);
+ const composition = useStore($composition)
+ const thisId = `prompt-item-${nugget.id}`
+
+ const handleOnDragStart = () => {
+ onDragStart ? onDragStart(nugget) : null;
+ }
+
+ const handleOnMouseEnter = () => {
+ if (!sourceItem) {
+ return;
+ }
+ startHoverOver(nugget);
+ }
+
+ const handleOnMouseLeave = () => {
+ onMouseLeave ? onMouseLeave(nugget) : null;
+ }
+
+ const handleOnDragOver = ($e: DragEvent) => {
+ if (!sourceItem) return;
+ // extract the prompt item's ID:
+ const targetId = $e.currentTarget.getAttribute("data-promptitem-id");
+ if (sourceItem.id == targetId) return;
+ console.log("current target id: %s", targetId);
+ const promptItem = composition.find(i => (targetId === i.id));
+ if (!promptItem) {
+ console.warn("Could not find promptitem with ID %s", targetId);
+ console.log(composition.map(c => c.id));
+ return;
+ }
+ startHoverOver(promptItem);
+ }
+
+ const handleOnDragEnd = () => {
+ completeDrop();
+ }
+
return (
-
+
{nugget.item.name || nugget.item.prompt}
{scoreDisp}
-
-
-
-
+
+
+
+
);
diff --git a/src/components/Operation.tsx b/src/components/Operation.tsx
index 9b91081..3e14995 100644
--- a/src/components/Operation.tsx
+++ b/src/components/Operation.tsx
@@ -1,17 +1,20 @@
import { Menu, MenuItem } from "@material-ui/core";
-import React, { Children, ReactNode } from 'react';
+import React, { Children, DragEvent, ReactNode, useEffect } from 'react';
import "./Operation.css";
import { Op } from "../lib/operator";
import { v4 as randomUUID } from "uuid";
-import { Operation as OperationType, changeOperationOp } from "../lib/prompt";
+import { $composition, Operation as OperationType, changeOperationOp } from "../lib/prompt";
import Nugget from "./Nugget";
+import { PromptItemProps } from "./PromptItem";
+import { useStore } from "@nanostores/react";
+import { $sourceItem, cancelDrop, completeDrop, startHoverOver } from "../store/prompt-dnd";
-interface OperationProps {
- operation : OperationType
+interface OperationProps extends PromptItemProps {
+ operation: OperationType
}
-function Operation(props : OperationProps) {
- const {operation} = props;
+function Operation(props: OperationProps) {
+ const { operation, onDragStart, onDragOver, onDragEnd, onDrop, onMouseEnter, onMouseLeave } = props;
const [contextMenu, setContextMenu] = React.useState<{
mouseX: number;
@@ -19,7 +22,44 @@ function Operation(props : OperationProps) {
} | null>(null);
const [id,] = React.useState(randomUUID);
+ const sourceItem = useStore($sourceItem);
+ const handleOnDragStart = () => {
+ onDragStart ? onDragStart(operation) : null;
+ }
+ const composition = useStore($composition);
+
+
+ const handleOnMouseEnter = () => {
+ if (!sourceItem) {
+ return;
+ }
+ startHoverOver(operation);
+ }
+
+
+ const handleOnMouseLeave = () => {
+ onMouseLeave ? onMouseLeave(operation) : null;
+ }
+
+ const handleOnDragOver = ($e: DragEvent) => {
+ if (!sourceItem) return;
+ // extract the prompt item's ID:
+ const targetId = $e.currentTarget.getAttribute("data-promptitem-id");
+ if (sourceItem.id == targetId) return;
+ console.log("current target id: %s", targetId);
+ const promptItem = composition.find(i => (targetId === i.id));
+ if (!promptItem) {
+ console.warn("Could not find promptitem with ID %s", targetId);
+ console.log(composition.map(c => c.id));
+ return;
+ }
+ startHoverOver(promptItem);
+ }
+
+ const handleOnDragEnd = () => {
+ completeDrop();
+ }
const handleContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
setContextMenu(
@@ -45,7 +85,17 @@ function Operation(props : OperationProps) {
}
return (
-
+
{operation.op}
{
diff --git a/src/components/PromptComposer.tsx b/src/components/PromptComposer.tsx
index a52cd28..a49384e 100644
--- a/src/components/PromptComposer.tsx
+++ b/src/components/PromptComposer.tsx
@@ -3,13 +3,15 @@ import Masonry from '@mui/lab/Masonry';
import AddIcon from '@mui/icons-material/Add';
import "./PromptComposer.css";
import { PromptLibrary } from './PromptLibrary';
-import React, { useState } from 'react';
-import { $slottedComposition, LibraryItem, PromptItem, insertIntoComposition } from '../lib/prompt';
+import React, { useEffect, useState } from 'react';
+import { $slottedComposition, LibraryItem, PromptItem, addToOperation, insertIntoComposition, itemIsNugget, itemIsOperation, lassoNuggets } from '../lib/prompt';
import { Category } from '@mui/icons-material';
import { useStore } from '@nanostores/react'
import Nugget from './Nugget';
import { Stack } from '@mui/material';
-import { Operation } from './Operation';
+import { Op, Operation } from './Operation';
+import { PromptItemProps } from './PromptItem';
+import { $dragDropState, $dropCandidate, $isDragInProgress, $sourceItem, completeDrop, endHoverOver, startDrag, startHoverOver } from '../store/prompt-dnd';
export interface PromptComposerProps {
@@ -32,8 +34,45 @@ export default function PromptComposer(props: PromptComposerProps) {
const slottedComposition = useStore($slottedComposition);
- const promptItemFactory = (promptItem : PromptItem, key : string) => {
- return "op" in promptItem ?
:
+ const promptItemFactory = (promptItem: PromptItem, key: string) => {
+
+ const callbacks = {
+ onDragStart: (item: PromptItem) => {
+ if (itemIsNugget(promptItem)) {
+ startDrag(item);
+ }
+ // TODO: operation
+ },
+ onDrop: (item: PromptItem) => {
+ const dnd = useStore($dragDropState);
+ const isDragInProgress = useStore($isDragInProgress);
+ const dropCandidate = useStore($dropCandidate);
+ const sourceItem = useStore($sourceItem);
+ if (!(sourceItem && dropCandidate)) {
+ return;
+ }
+ if (itemIsNugget(dropCandidate) && itemIsNugget(sourceItem)) {
+ // TODO: show a pop-up to select the operator.
+ lassoNuggets(dropCandidate.id, sourceItem.id, Op.AND);
+ }
+ if (itemIsNugget(sourceItem) && itemIsOperation(dropCandidate)) {
+ addToOperation(sourceItem.id, dropCandidate.id);
+ }
+ completeDrop();
+ },
+ onDragEnd: (item: PromptItem) => {
+
+ },
+ onMouseEnter: (item: PromptItem) => {
+ },
+ onMouseLeave: (item: PromptItem) => {
+ endHoverOver();
+ },
+ } as PromptItemProps;
+
+ return ("op" in promptItem ?
+
+ :
)
}
return (
@@ -51,10 +90,10 @@ export default function PromptComposer(props: PromptComposerProps) {
{
slottedComposition.map((itemCol, i) => (
-
- {itemCol.map((promptItem, j) => promptItemFactory(promptItem, `item-${j}-${j}`))}
-
- ))
+
+ {itemCol.map((promptItem, j) => promptItemFactory(promptItem, `item-${j}-${j}`))}
+
+ ))
}
diff --git a/src/components/PromptItem.css b/src/components/PromptItem.css
new file mode 100644
index 0000000..80993f6
--- /dev/null
+++ b/src/components/PromptItem.css
@@ -0,0 +1,7 @@
+.prompt-item .drag-target-highlight {
+ border: 1px solid red;
+}
+
+.prompt-item .drag-target-hover {
+ border: 1px solid blue;
+}
\ No newline at end of file
diff --git a/src/components/PromptItem.tsx b/src/components/PromptItem.tsx
new file mode 100644
index 0000000..15f5bad
--- /dev/null
+++ b/src/components/PromptItem.tsx
@@ -0,0 +1,22 @@
+import {PromptItem as PIType} from "../lib/prompt"
+
+/**
+ * NOTE: Drag-n-drop rules!
+ *
+ * - Nuggets can be dragged and dropped into...
+ * - another nugget
+ * - when this happens, an Operation is created.
+ * - an operation
+ * - when this happens, the Nugget is added to the operation.
+ * - Operations can be dragged, but only to reorder.
+ * - Nuggets can also be dragged to be re-ordered.
+ */
+
+export interface PromptItemProps {
+ onDragStart?: (item : PIType) => void,
+ onDragEnd?: (item: PIType) => void,
+ onDragOver?: (item: PIType) => void,
+ onDrop?: (item : PIType) => void,
+ onMouseEnter?: (item : PIType) => void,
+ onMouseLeave?: (item : PIType) => void,
+}
\ No newline at end of file
diff --git a/src/lib/prompt.tsx b/src/lib/prompt.tsx
index 0500e46..1c80448 100644
--- a/src/lib/prompt.tsx
+++ b/src/lib/prompt.tsx
@@ -1,4 +1,4 @@
-import { v4 as randomUUID } from "uuid";
+import { v4 as randomUUID, v4 as uuidv4 } from "uuid";
import { Op } from "./operator";
import { atom, computed } from "nanostores";
@@ -47,6 +47,14 @@ export type Operation = IdAble & {
export type PromptItem = Operation | Nugget
+export function itemIsOperation(item: PromptItem): boolean {
+ return "op" in item;
+}
+
+export function itemIsNugget(item: PromptItem): boolean {
+ return !itemIsOperation(item);
+}
+
export type Composition = Array
;
export const $library = atom([])
@@ -103,6 +111,51 @@ export function changeOperationOp(operationId: Id, op: Op) {
]);
}
+/**
+ * utility method to remove & return a prompt item by ID
+ * @param id PromptItem ID
+ */
+export function popPromptItem (id : Id) {
+ const found = $composition.get().find(item => item.id === id);
+ $composition.set($composition.get().filter(item => item.id !== id));
+ return found;
+}
+
+
+export function lassoNuggets (n1id : Id, n2id : Id, op : Op) {
+ const n1 = popPromptItem(n1id);
+ const n2 = popPromptItem(n2id);
+ $composition.set([...$composition.get(), {
+ id: uuidv4(),
+ op,
+ items: [n1, n2],
+ } as Operation])
+}
+
+export function addToOperation(nId : Id, opId : Id) {
+ const comp = $composition.get();
+ const nugget = comp.find(i => i.id === nId);
+ $composition.set($composition.get().map(i => {
+ if (![nId, opId].includes(i.id)) {
+ return i;
+ }
+ // if this is the nugget...
+ if (i.id === nId) {
+ return null;
+ }
+ // if this is the operation...
+ if (i.id == opId && "op" in i) {
+ return {
+ ...i,
+ items: [
+ ...i.items, nugget
+ ]
+ }
+ }
+ return i;
+ }).filter(i => i != null) as Composition);
+}
+
export function nuggetToText(nugget: Nugget) {
const absScore = Math.abs(nugget.score);
const neg = nugget.score < 0;
diff --git a/src/store/prompt-dnd.test.tsx b/src/store/prompt-dnd.test.tsx
new file mode 100644
index 0000000..48aa644
--- /dev/null
+++ b/src/store/prompt-dnd.test.tsx
@@ -0,0 +1,73 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { $dragDropState, startDrag, startHoverOver, endHoverOver, isPromptItemDragSource, isPromptItemDropCandidate, isPromptItemDropTarget, completeDrop } from "./prompt-dnd";
+import { Category, Library, LibraryItem, Nugget } from "../lib/prompt";
+import { v4 as uuid4 } from "uuid";
+
+describe("drag and drop", () => {
+
+ beforeEach(() => {
+ $dragDropState.set({});
+ });
+
+ const source = {
+ id: uuid4(),
+ score: 0,
+ item: {
+ id: uuid4(),
+ prompt: "zany",
+ category: Category.vibes,
+ } as LibraryItem
+ } as Nugget;
+
+ const target = {
+ id: uuid4(),
+ score: 0,
+ item: {
+ id: uuid4(),
+ prompt: "wild",
+ category: Category.vibes,
+ } as LibraryItem
+ } as Nugget;
+
+ it("should start drag", () => {
+ startDrag(source);
+ expect($dragDropState.get().currentSourceId).toEqual(source.id);
+ });
+
+ it("should start hover over", () => {
+ startHoverOver(target);
+ expect($dragDropState.get().currentDropCandidateId).toEqual(target.id);
+ });
+
+ it("should end hover over", () => {
+ endHoverOver();
+ expect($dragDropState.get().currentDropCandidateId).toEqual(null);
+ });
+
+ it("should check if item is a drag source", () => {
+ startDrag(source)
+ expect(isPromptItemDragSource($dragDropState, source)).toBeTruthy();
+ });
+
+ it("should check if item is a drop candidate", () => {
+ startDrag(source);
+ startHoverOver(target);
+ expect(isPromptItemDropCandidate($dragDropState, target)).toBeTruthy();
+ });
+
+ it("should check if item is a drop target", () => {
+ startDrag(source);
+ expect(isPromptItemDropTarget($dragDropState, target)).toBeTruthy();
+ });
+
+ it("should complete drop", () => {
+ startDrag(source);
+ startHoverOver(target);
+ endHoverOver();
+ completeDrop();
+ expect($dragDropState.get().currentSourceId).toBeFalsy();
+ expect($dragDropState.get().currentDropCandidateId).toBeFalsy();
+ });
+});
diff --git a/src/store/prompt-dnd.tsx b/src/store/prompt-dnd.tsx
new file mode 100644
index 0000000..cbdb8ea
--- /dev/null
+++ b/src/store/prompt-dnd.tsx
@@ -0,0 +1,83 @@
+import { Atom, atom, computed } from "nanostores"
+import { $composition, Nugget, Operation, PromptItem, addToOperation, itemIsNugget, itemIsOperation, lassoNuggets } from "../lib/prompt";
+import { Op } from "../lib/operator";
+
+export type DragDropState = {
+ currentSourceId?: string | null,
+ currentDropCandidateId?: string | null,
+}
+
+export const $dragDropState = atom({});
+
+$dragDropState.subscribe((value) => {
+ console.log("drag-n-drop: %x", value);
+})
+
+export const $sourceItem = computed($dragDropState, (dnd) => {
+ const comp = $composition.get()
+ return comp.find(i => i.id === dnd.currentSourceId);
+});
+
+export const $dropCandidate = computed($dragDropState, (dnd) => {
+ const comp = $composition.get()
+ return comp.find(i => i.id === dnd.currentDropCandidateId);
+});
+
+export function startDrag(item: PromptItem) {
+ $dragDropState.set({ ...$dragDropState.get(), currentSourceId: item.id });
+}
+
+export function startHoverOver(item: PromptItem) {
+ $dragDropState.set({ ...$dragDropState.get(), currentDropCandidateId: item.id });
+}
+
+export function endHoverOver() {
+ $dragDropState.set({ ...$dragDropState.get(), currentDropCandidateId: null })
+}
+
+export const $isDragInProgress = computed($dragDropState, (dragDropState) => true);
+
+export function isPromptItemDragSource($dds: Atom, promptItem: PromptItem) {
+ return $dds.get().currentSourceId === promptItem.id;
+}
+
+export function isPromptItemDropTarget($dds: Atom, promptItem: PromptItem) {
+ return $dds.get().currentSourceId && $dds.get().currentSourceId !== promptItem.id;
+}
+
+export function isPromptItemDropCandidate($dds: Atom, promptItem: PromptItem) {
+ return $dds.get().currentDropCandidateId === promptItem.id;
+}
+
+export function cancelDrop() {
+ $dragDropState.set({});
+};
+
+export function completeDrop() {
+ const source = $sourceItem.get();
+ const target = $dropCandidate.get();
+ if (!(source && target)) return;
+ if (itemIsOperation(target)) {
+ const nSource = source as Nugget;
+ const nTarget = target as Operation;
+ const c1 = nSource.item.category;
+ const c2 = nTarget.items[0].item.category;
+ if (c1 != c2) {
+ console.error("Category mismatch: cannot drop a %s into %s", c1, c2);
+ } else {
+ addToOperation(source.id, target.id);
+ }
+ }
+ else if (itemIsNugget(target) && itemIsNugget(source)) {
+ const nTarget = target as Nugget;
+ const nSource = source as Nugget;
+ const c1 = nSource.item.category;
+ const c2 = nTarget.item.category;
+ if (c1 != c2) {
+ console.error("Category mismatch: cannot drop a %s into %s", c1, c2);
+ } else {
+ lassoNuggets(source.id, target.id, Op.AND)
+ }
+ }
+ cancelDrop();
+}