Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Scope cut-copy-paste #215

Merged
merged 9 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getVisibility,
updateVisibility,
addCollaborator,
addPods,
pod,
repo,
repos,
Expand Down Expand Up @@ -73,6 +74,7 @@ export const resolvers = {
deleteRepo,
clearUser: () => {},
updatePod,
addPods,
deletePod,
addCollaborator,
updateVisibility,
Expand Down
84 changes: 35 additions & 49 deletions api/src/resolver_repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,56 +296,42 @@ export async function updatePod(_, { id, repoId, input }, { userId }) {
},
},
});
if (pod_found) {
// if repoId doesn't have id, create it IF input.parent exists
const pod = await prisma.pod.update({
where: {
id,
},
data: {
...input,
parent:
input.parent && input.parent !== "ROOT"
? {
connect: {
id: input.parent,
},
}
: undefined,
children: {
connect: input.children?.map((id) => ({ id })),
},
},
});
} else {
// if repoId doesn't have id, create it IF input.parent exists, otherwise throw error.
await prisma.pod.create({
data: {
id,
...input,
// Dummy index because it is a required field for historical reasons.
index: 0,
parent:
input.parent && input.parent !== "ROOT"
? {
connect: {
id: input.parent,
},
}
: undefined,
// In case of [], create will throw an error. Thus I have to pass undefined.
// children: input.children.length > 0 ? input.children : undefined,
children: {
connect: input.children?.map((id) => ({ id })),
},
repo: {
connect: {
id: repoId,
},
},
// or, return false and leave it dirty
if (!pod_found) return false;
const pod = await prisma.pod.update({
where: {
id,
},
data: {
...input,
parent:
input.parent && input.parent !== "ROOT"
? {
connect: {
id: input.parent,
},
}
: undefined,
children: {
connect: input.children?.map((id) => ({ id })),
},
});
}
},
});
return true;
}

export async function addPods(_, { repoId, pods }, { userId }) {
if (!userId) throw new Error("Not authenticated.");
await ensureRepoEditAccess({ repoId, userId });
// notice: we keep the field "children", "parent", "repo" empty when first insertion the repo. Because if we insist on filling them, we must specify children, parent and repo by prisma.create.pod. Regardless of what order we insert them, we can't make sure both children and parent exist in the DB, the insertion must fail.
// Here, we first insert all pods and ignore their any relationship, then the relationship will be updated by updateAllPods because we don't clean the dirty tag of them next.
await prisma.pod.createMany({
data: pods.map((pod) => {
const res = { ...pod, id: pod.id, index: 0, parent: undefined, repoId };
if (res.children) delete res.children;
return res;
}),
});

return true;
}
Expand Down
1 change: 1 addition & 0 deletions api/src/typedefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export const typeDefs = gql`
updateRepo(id: ID, name: String): Boolean
deleteRepo(id: ID): Boolean
deletePod(id: String, toDelete: [String]): Boolean
addPods(repoId: String, pods: [PodInput]): Boolean
updatePod(id: String, repoId: String, input: PodInput): Boolean
clearUser: Boolean
clearRepo: Boolean
Expand Down
67 changes: 42 additions & 25 deletions ui/src/components/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,9 @@ function usePaste(reactFlowWrapper) {
const pasteEnd = useStore(store, (state) => state.pasteEnd);
const cancelPaste = useStore(store, (state) => state.cancelPaste);
const isPasting = useStore(store, (state) => state.isPasting);
const isCutting = useStore(store, (state) => state.isCutting);
const isGuest = useStore(store, (state) => state.role === "GUEST");

const isPaneFocused = useStore(store, (state) => state.isPaneFocused);
const resetSelection = useStore(store, (state) => state.resetSelection);

useEffect(() => {
Expand All @@ -228,12 +229,17 @@ function usePaste(reactFlowWrapper) {
onPasteMove(position);
};
const mouseClick = (event) => {
pasteEnd();
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const position = reactFlowInstance.project({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top,
});
pasteEnd(position, false);
};
const keyDown = (event) => {
if (event.key !== "Escape") return;
// delete the temporary node
cancelPaste();
cancelPaste(false);
//clear the pasting state
event.preventDefault();
};
Expand All @@ -259,20 +265,14 @@ function usePaste(reactFlowWrapper) {
const handlePaste = useCallback(
(event) => {
// avoid duplicated pastes
if (isPasting || isGuest) return;

// only paste when the pane is focused
if (
event.target?.className !== "react-flow__pane" &&
document.activeElement?.className !== "react-flow__pane"
)
return;
// check if the pane is focused
if (isPasting || isCutting || isGuest || !isPaneFocused) return;

try {
// the user clipboard data is unpreditable, may have application/json
// from other source that can't be parsed by us, use try-catch here.
const playload = event.clipboardData.getData("application/json");
const data = JSON.parse(playload);
const payload = event.clipboardData.getData("application/json");
const data = JSON.parse(payload);
if (data?.type !== "pod") {
return;
}
Expand All @@ -288,7 +288,7 @@ function usePaste(reactFlowWrapper) {
];

const position = reactFlowInstance.project({ x: posX, y: posY });
pasteBegin(position, data.data);
pasteBegin(position, data.data, false);
} catch (e) {
console.log("paste error", e);
}
Expand All @@ -300,6 +300,7 @@ function usePaste(reactFlowWrapper) {
reactFlowInstance,
reactFlowWrapper,
resetSelection,
isPaneFocused,
]
);

Expand All @@ -321,6 +322,9 @@ function useCut(reactFlowWrapper) {
const onCutMove = useStore(store, (state) => state.onCutMove);
const cancelCut = useStore(store, (state) => state.cancelCut);
const isCutting = useStore(store, (state) => state.isCutting);
const isPasting = useStore(store, (state) => state.isPasting);
const isGuest = useStore(store, (state) => state.role === "GUEST");
const apolloClient = useApolloClient();

useEffect(() => {
if (!reactFlowWrapper.current) return;
Expand All @@ -335,7 +339,12 @@ function useCut(reactFlowWrapper) {
onCutMove(position);
};
const mouseClick = (event) => {
cutEnd();
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const position = reactFlowInstance.project({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top,
});
cutEnd(position, reactFlowInstance);
};
const keyDown = (event) => {
if (event.key !== "Escape") return;
Expand All @@ -358,6 +367,8 @@ function useCut(reactFlowWrapper) {
cancelCut,
cutEnd,
isCutting,
isPasting,
apolloClient,
onCutMove,
reactFlowInstance,
reactFlowWrapper,
Expand Down Expand Up @@ -431,33 +442,41 @@ function CanvasImpl() {
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
const shareOpen = useStore(store, (state) => state.shareOpen);
const setShareOpen = useStore(store, (state) => state.setShareOpen);
const setPaneFocus = useStore(store, (state) => state.setPaneFocus);
const setPaneBlur = useStore(store, (state) => state.setPaneBlur);

const [showContextMenu, setShowContextMenu] = useState(false);
const [points, setPoints] = useState({ x: 0, y: 0 });
const [client, setClient] = useState({ x: 0, y: 0 });

const onPaneContextMenu = (event) => {
console.log("onPaneContextMenu", event);
event.preventDefault();
setShowContextMenu(true);
setPoints({ x: event.pageX, y: event.pageY });
setClient({ x: event.clientX, y: event.clientY });
};

useEffect(() => {
const handleClick = (e) => {
const handleClick = (event) => {
setShowContextMenu(false);
const target = event.target;
// set the pane focused only when the clicked target is pane or the copy buttons on a pod
// then we can paste right after click on the copy buttons
if (
target.className === "react-flow__pane" ||
target.classList?.contains("copy-button") ||
target.parentElement?.classList?.contains("copy-button")
) {
setPaneFocus();
} else {
setPaneBlur();
}
};
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("click", handleClick);
};
}, [setShowContextMenu]);

const onPaneClick = (event) => {
// focus
event.target.tabIndex = 0;
};
}, [setShowContextMenu, setPaneFocus, setPaneBlur]);

const getScopeAtPos = useStore(store, (state) => state.getScopeAtPos);

Expand Down Expand Up @@ -504,8 +523,6 @@ function CanvasImpl() {
removeDragHighlight();
}
}}
onPaneClick={onPaneClick}
// onPaneMouseMove={onPaneMouseMove}
attributionPosition="top-right"
maxZoom={10}
minZoom={0.1}
Expand Down
8 changes: 4 additions & 4 deletions ui/src/components/CanvasContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,30 +46,30 @@ export function CanvasContextMenu(props) {
<MenuList className="paneContextMenu">
{!isGuest && (
<MenuItem onClick={props.addCode} sx={ItemStyle}>
<ListItemIcon>
<ListItemIcon sx={{ color: "inherit" }}>
<CodeIcon />
</ListItemIcon>
<ListItemText>New Code</ListItemText>
</MenuItem>
)}
{!isGuest && (
<MenuItem onClick={props.addRich} sx={ItemStyle}>
<ListItemIcon>
<ListItemIcon sx={{ color: "inherit" }}>
<NoteIcon />
</ListItemIcon>
<ListItemText>New Note</ListItemText>
</MenuItem>
)}
{!isGuest && (
<MenuItem onClick={props.addScope} sx={ItemStyle}>
<ListItemIcon>
<ListItemIcon sx={{ color: "inherit" }}>
<PostAddIcon />
</ListItemIcon>
<ListItemText>New Scope</ListItemText>
</MenuItem>
)}
<MenuItem onClick={flipShowLineNumbers} sx={ItemStyle}>
<ListItemIcon>
<ListItemIcon sx={{ color: "inherit" }}>
<FormatListNumberedIcon />
</ListItemIcon>
<ListItemText>
Expand Down
22 changes: 8 additions & 14 deletions ui/src/components/nodes/Code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export const CodeNode = memo<NodeProps>(function ({
const setPodName = useStore(store, (state) => state.setPodName);
const setPodGeo = useStore(store, (state) => state.setPodGeo);
const getPod = useStore(store, (state) => state.getPod);
const clonePod = useStore(store, (state) => state.clonePod);
const pod = getPod(id);
const isGuest = useStore(store, (state) => state.role === "GUEST");
const isPodFocused = useStore(store, (state) => state.pods[id]?.focus);
Expand All @@ -252,7 +253,7 @@ export const CodeNode = memo<NodeProps>(function ({
);
const inputRef = useRef<HTMLInputElement>(null);
const updateView = useStore(store, (state) => state.updateView);
const isCutting = useStore(store, (state) => state.cuttingId === id);
const isCutting = useStore(store, (state) => state.cuttingIds.has(id));

const showResult = useStore(
store,
Expand All @@ -264,6 +265,7 @@ export const CodeNode = memo<NodeProps>(function ({
state.pods[id]?.stderr
);
const nodesMap = useStore(store, (state) => state.ydoc.getMap<Node>("pods"));
const setPaneFocus = useStore(store, (state) => state.setPaneFocus);
const onResize = useCallback(
(e, data) => {
const { size } = data;
Expand Down Expand Up @@ -298,7 +300,7 @@ export const CodeNode = memo<NodeProps>(function ({

const onCopy = useCallback(
(clipboardData: any) => {
const pod = getPod(id);
const pod = clonePod(id);
if (!pod) return;
clipboardData.setData("text/plain", pod.content);
clipboardData.setData(
Expand All @@ -308,8 +310,9 @@ export const CodeNode = memo<NodeProps>(function ({
data: pod,
})
);
setPaneFocus();
},
[getPod, id]
[clonePod, id]
);

const cutBegin = useStore(store, (state) => state.cutBegin);
Expand Down Expand Up @@ -471,15 +474,6 @@ export const CodeNode = memo<NodeProps>(function ({
justifyContent: "center",
}}
className="nodrag"
onClick={(e) => {
const pane = document.getElementsByClassName(
"react-flow__pane"
)[0] as HTMLElement;
if (pane) {
pane.tabIndex = 0;
pane.focus();
}
}}
>
{!isGuest && (
<Tooltip title="Run (shift-enter)">
Expand All @@ -499,8 +493,8 @@ export const CodeNode = memo<NodeProps>(function ({
options={{ debug: true, format: "text/plain", onCopy } as any}
>
<Tooltip title="Copy">
<IconButton size="small">
<ContentCopyIcon fontSize="inherit" />
<IconButton size="small" className="copy-button">
<ContentCopyIcon fontSize="inherit" className="copy-button" />
</IconButton>
</Tooltip>
</CopyToClipboard>
Expand Down
Loading