Repository: Thinkmill/keystatic
Pull request #226 (2023-04-06 00:22:23)

Fix pasting reversing
(moving the elements in reverse is what actually fixes it)

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: b15ef65315fbaba0aea537d529c2802a6a512c25

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 6 packages</summary>

  | Name                           | Type  |
| ------------------------------ | ----- |
| @keystatic/core                | Patch |
| @keystatic/test-next-app       | Patch |
| @keystatic/test-astro          | Patch |
| localization                   | Patch |
| @keystatic/test-next-pages-dir | Patch |
| @keystatic/remix-test-app      | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/Thinkmill/keystatic/new/fix-pasting-reversing?filename=.changeset/good-moons-hear.md&value=---%0A%22%40keystatic%2Ftest-next-app%22%3A%20patch%0A%22%40keystatic%2Fcore%22%3A%20patch%0A---%0A%0AFix%20pasting%20reversing%0A)


vercel[bot]: [vc]: #RE3lESPcOrDYJsmiYgPls7kK2pt7uExWTWX+4Pv0/zI=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJrZXlzdGF0aWMiLCJyb290RGlyZWN0b3J5Ijoia2V5c3RhdGljIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3RoaW5rbWlsbC9rZXlzdGF0aWMvRFR2eVdhVXNXa1hlSGY0RWpxcFB1NDd4d2V1RSIsIm5leHRDb21taXRTdGF0dXMiOiJQRU5ESU5HIn0seyJuYW1lIjoidm91c3NvaXIiLCJyb290RGlyZWN0b3J5IjoiZGVzaWduLXN5c3RlbS9kb2NzIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3RoaW5rbWlsbC92b3Vzc29pci85UEpRUGFkaVhNZlp6TGY0alVYZVF1Nk1QVnZpIiwibmV4dENvbW1pdFN0YXR1cyI6IlBFTkRJTkcifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **keystatic** | 🔄 Building ([Inspect](https://vercel.com/thinkmill/keystatic/DTvyWaUsWkXeHf4EjqpPu47xweuE)) |  | Apr 6, 2023 0:22am |
| **voussoir** | 🔄 Building ([Inspect](https://vercel.com/thinkmill/voussoir/9PJQPadiXMfZzLf4jUXeQu6MPVvi)) |  | Apr 6, 2023 0:22am |




Patch:
diff --git a/.changeset/eighty-pandas-chew.md b/.changeset/eighty-pandas-chew.md
new file mode 100644
index 00000000..d142539d
--- /dev/null
+++ b/.changeset/eighty-pandas-chew.md
@@ -0,0 +1,5 @@
+---
+'@keystatic/core': patch
+---
+
+Fix pasting in some cases reversing the order of the pasted elements
diff --git a/packages/keystatic/DocumentEditor/blockquote/blockquote-ui.tsx b/packages/keystatic/DocumentEditor/blockquote/blockquote-ui.tsx
index a0aa8c1f..dd3263f0 100644
--- a/packages/keystatic/DocumentEditor/blockquote/blockquote-ui.tsx
+++ b/packages/keystatic/DocumentEditor/blockquote/blockquote-ui.tsx
@@ -5,7 +5,7 @@ import { ReactEditor, RenderElementProps } from 'slate-react';
 import { ActionButton } from '@voussoir/button';
 import { quoteIcon } from '@voussoir/icon/icons/quoteIcon';
 import { Icon } from '@voussoir/icon';
-import { css } from '@voussoir/style';
+import { css, tokenSchema } from '@voussoir/style';
 import { TooltipTrigger, Tooltip } from '@voussoir/tooltip';
 import { Kbd, Text } from '@voussoir/typography';
 
@@ -33,9 +33,12 @@ export const BlockquoteElement = ({
   return (
     <blockquote
       className={css({
-        fontFamily: 'serif',
-        fontSize: '1.25em',
-        marginInline: '2em',
+        borderInlineStart: `0.25em solid ${tokenSchema.color.border.neutral}`,
+        paddingInline: '1em',
+        margin: '2em',
+        marginLeft: 0,
+        marginRight: 0,
+        position: 'relative',
       })}
       {...attributes}
     >
diff --git a/packages/keystatic/DocumentEditor/editor.tsx b/packages/keystatic/DocumentEditor/editor.tsx
index 9b45a303..e87c0195 100644
--- a/packages/keystatic/DocumentEditor/editor.tsx
+++ b/packages/keystatic/DocumentEditor/editor.tsx
@@ -268,13 +268,14 @@ function withBlocksSchema(editor: Editor): Editor {
         );
         return;
       }
-
-      for (const [index, childNode] of node.children.entries()) {
+      let didUpdate = false;
+      for (const [index, childNode] of [...node.children.entries()].reverse()) {
         const childPath = [...path, index];
         if (info.kind === 'inlines') {
           if (!Text.isText(childNode) && isBlock(childNode)) {
             handleNodeInInvalidPosition(editor, [childNode, childPath], path);
-            return;
+            didUpdate = true;
+            continue;
           }
         } else {
           if (!isBlock(childNode)) {
@@ -283,14 +284,19 @@ function withBlocksSchema(editor: Editor): Editor {
               { type: info.blockToWrapInlinesIn, children: [] },
               { at: childPath }
             );
-            return;
+            didUpdate = true;
+            continue;
           }
           if (!info.allowedChildren.has(childNode.type)) {
             handleNodeInInvalidPosition(editor, [childNode, childPath], path);
-            return;
+            didUpdate = true;
+            continue;
           }
         }
       }
+      if (didUpdate) {
+        return;
+      }
     }
     normalizeNode([node, path]);
   };
@@ -300,16 +306,16 @@ function withBlocksSchema(editor: Editor): Editor {
 function handleNodeInInvalidPosition(
   editor: Editor,
   [node, path]: NodeEntry<Block>,
-  parentPath: Path
+  ancestorPath: Path
 ) {
   const nodeType = node.type;
   const childNodeInfo = editorSchema[nodeType];
   // the parent of a block will never be an inline so this casting is okay
-  const parentNode = Node.get(editor, parentPath) as Block | Editor;
+  const ancestorNode = Node.get(editor, ancestorPath) as Block | Editor;
 
-  const parentNodeType = Editor.isEditor(parentNode)
+  const parentNodeType = Editor.isEditor(ancestorNode)
     ? 'editor'
-    : parentNode.type;
+    : ancestorNode.type;
 
   const parentNodeInfo = editorSchema[parentNodeType];
 
@@ -336,21 +342,21 @@ function handleNodeInInvalidPosition(
     return;
   }
 
-  const info = editorSchema[parentNode.type || 'editor'];
+  const info = editorSchema[ancestorNode.type || 'editor'];
   if (info?.kind === 'blocks' && info.allowedChildren.has(nodeType)) {
-    if (parentPath.length === 0) {
+    if (ancestorPath.length === 0) {
       Transforms.moveNodes(editor, { at: path, to: [path[0] + 1] });
     } else {
-      Transforms.moveNodes(editor, { at: path, to: Path.next(parentPath) });
+      Transforms.moveNodes(editor, { at: path, to: Path.next(ancestorPath) });
     }
     return;
   }
-  if (Editor.isEditor(parentNode)) {
+  if (Editor.isEditor(ancestorNode)) {
     Transforms.moveNodes(editor, { at: path, to: [path[0] + 1] });
     Transforms.unwrapNodes(editor, { at: [path[0] + 1] });
     return;
   }
-  handleNodeInInvalidPosition(editor, [node, path], parentPath.slice(0, -1));
+  handleNodeInInvalidPosition(editor, [node, path], ancestorPath.slice(0, -1));
 }
 
 // to print the editor schema in Graphviz if you want to visualize it
diff --git a/packages/keystatic/app/shell/data.tsx b/packages/keystatic/app/shell/data.tsx
index f4fe5d78..c56d3c5a 100644
--- a/packages/keystatic/app/shell/data.tsx
+++ b/packages/keystatic/app/shell/data.tsx
@@ -220,7 +220,7 @@ export function GitHubAppShellProvider(props: {
   }, [allTreeData, props.config]);
 
   useEffect(() => {
-    if (error?.response.status === 401) {
+    if (error?.response?.status === 401) {
       if (isGitHubConfig(props.config)) {
         window.location.href = `/api/keystatic/github/login?from=${router.params.join(
           '/'


Test patch:
diff --git a/packages/keystatic/DocumentEditor/block-schema.test.tsx b/packages/keystatic/DocumentEditor/block-schema.test.tsx
new file mode 100644
index 00000000..a615c1aa
--- /dev/null
+++ b/packages/keystatic/DocumentEditor/block-schema.test.tsx
@@ -0,0 +1,40 @@
+/** @jest-environment jsdom */
+/** @jsxRuntime classic */
+/** @jsx jsx */
+import { jsx, makeEditor } from './tests/utils';
+
+test('multiple elements in the wrong position have their order maintained', () => {
+  const layouts = Array.from('abcd').map(char => (
+    <layout layout={[1]}>
+      <layout-area>
+        <paragraph>
+          <text>{char}</text>
+        </paragraph>
+      </layout-area>
+    </layout>
+  ));
+  const editor = makeEditor(
+    <editor>
+      <blockquote>{layouts}</blockquote>
+      <paragraph>
+        <text />
+      </paragraph>
+    </editor>,
+    { normalization: 'normalize' }
+  );
+  expect(editor).toEqualEditor(
+    makeEditor(
+      <editor>
+        <blockquote>
+          <paragraph>
+            <text />
+          </paragraph>
+        </blockquote>
+        {layouts}
+        <paragraph>
+          <text />
+        </paragraph>
+      </editor>
+    )
+  );
+});
diff --git a/packages/keystatic/DocumentEditor/pasting/pasting-within-editor.test.tsx b/packages/keystatic/DocumentEditor/pasting/pasting-within-editor.test.tsx
new file mode 100644
index 00000000..f81100c3
--- /dev/null
+++ b/packages/keystatic/DocumentEditor/pasting/pasting-within-editor.test.tsx
@@ -0,0 +1,99 @@
+/** @jest-environment jsdom */
+/** @jsxRuntime classic */
+/** @jsx jsx */
+import { Transforms } from 'slate';
+import { makeEditor, jsx } from '../tests/utils';
+
+test('blockquote pasting', () => {
+  const editor = makeEditor(
+    <editor>
+      <blockquote>
+        <paragraph>
+          <text>
+            <anchor />a
+          </text>
+        </paragraph>
+      </blockquote>
+      <blockquote>
+        <paragraph>
+          <text>b</text>
+        </paragraph>
+      </blockquote>
+      <blockquote>
+        <paragraph>
+          <text>c</text>
+        </paragraph>
+      </blockquote>
+      <blockquote>
+        <paragraph>
+          <text>
+            d<focus />
+          </text>
+        </paragraph>
+      </blockquote>
+      <paragraph>
+        <text />
+      </paragraph>
+    </editor>
+  );
+  const fragment = editor.getFragment();
+  Transforms.collapse(editor, { edge: 'end' });
+  editor.insertFragment(fragment);
+  expect(editor).toMatchInlineSnapshot(`
+    <editor>
+      <blockquote>
+        <paragraph>
+          <text>
+            a
+          </text>
+        </paragraph>
+      </blockquote>
+      <blockquote>
+        <paragraph>
+          <text>
+            b
+          </text>
+        </paragraph>
+      </blockquote>
+      <blockquote>
+        <paragraph>
+          <text>
+            c
+          </text>
+        </paragraph>
+      </blockquote>
+      <blockquote>
+        <paragraph>
+          <text>
+            da
+          </text>
+        </paragraph>
+      </blockquote>
+      <blockquote>
+        <paragraph>
+          <text>
+            b
+          </text>
+        </paragraph>
+      </blockquote>
+      <blockquote>
+        <paragraph>
+          <text>
+            c
+          </text>
+        </paragraph>
+      </blockquote>
+      <blockquote>
+        <paragraph>
+          <text>
+            d
+            <cursor />
+          </text>
+        </paragraph>
+      </blockquote>
+      <paragraph>
+        <text />
+      </paragraph>
+    </editor>
+  `);
+});
diff --git a/packages/keystatic/DocumentEditor/tests/jsx/namespace.d.ts b/packages/keystatic/DocumentEditor/tests/jsx/namespace.d.ts
index ed7cda2f..aa02b229 100644
--- a/packages/keystatic/DocumentEditor/tests/jsx/namespace.d.ts
+++ b/packages/keystatic/DocumentEditor/tests/jsx/namespace.d.ts
@@ -3,7 +3,7 @@ import { Mark } from '../../utils';
 
 export const __jsx: any;
 
-type Children = Node | string | (Node | string)[];
+type Children = Node | string | Children[];
 
 type Elements = {
   [Key in Element['type']]: Omit<