From 9dde3f9d17366b2c9d4a03642ffc5d3169e0a505 Mon Sep 17 00:00:00 2001
From: George Francis <30405123+georgedoescode@users.noreply.github.com>
Date: Mon, 28 Oct 2024 13:18:31 +0000
Subject: [PATCH] chore(drag-and-drop): enable re-ordering of primitive array
items (#2055)
---
apps/page-builder-demo/src/app/dnd/page.tsx | 17 +-
apps/page-builder-demo/src/sanity.types.ts | 230 +++++++++---------
.../src/page-builder-demo/index.tsx | 6 +
packages/visual-editing/src/util/mutations.ts | 42 +++-
.../visual-editing/src/util/useDragEvents.ts | 33 ++-
5 files changed, 202 insertions(+), 126 deletions(-)
diff --git a/apps/page-builder-demo/src/app/dnd/page.tsx b/apps/page-builder-demo/src/app/dnd/page.tsx
index ed8072c44..424303530 100644
--- a/apps/page-builder-demo/src/app/dnd/page.tsx
+++ b/apps/page-builder-demo/src/app/dnd/page.tsx
@@ -39,6 +39,21 @@ export default async function Page() {
key={child._key}
>
{child.title}
+
+ {child.childrenStrings &&
+ child.childrenStrings.map((s: string, i: number) => (
+
+ {s}
+
+ ))}
+
))}
@@ -209,7 +224,7 @@ export default async function Page() {
{/* Inline */}
- Vertical (Flow Auto Calculated)
+ Inline
{data.children.map((child) => (
children?: Array<{
title?: string
children?: Array<{
@@ -701,6 +702,7 @@ export type DndPageQueryResult = {
title: string | null
children: Array<{
title?: string
+ childrenStrings?: Array
children?: Array<{
title?: string
children?: Array<{
@@ -716,6 +718,19 @@ export type DndPageQueryResult = {
}> | null
} | null
+// Source: ./src/app/projects/page.tsx
+// Variable: projectsPageQuery
+// Query: *[_type == "project" && defined(slug.current)]
+export type ProjectsPageQueryResult = Array<{
+ _id: string
+ _type: 'project'
+ _createdAt: string
+ _updatedAt: string
+ _rev: string
+ title?: string
+ slug?: Slug
+}>
+
// Source: ./src/app/products/page.tsx
// Variable: productsPageQuery
// Query: *[_type == "product" && defined(slug.current)]{ _id, title, description, slug, "media": media[0] }
@@ -756,115 +771,22 @@ export type ProductsPageQueryResult = Array<{
} | null
}>
-// Source: ./src/app/projects/page.tsx
-// Variable: projectsPageQuery
-// Query: *[_type == "project" && defined(slug.current)]
-export type ProjectsPageQueryResult = Array<{
- _id: string
- _type: 'project'
- _createdAt: string
- _updatedAt: string
- _rev: string
- title?: string
- slug?: Slug
-}>
-
-// Source: ./src/app/product/[slug]/page.tsx
-// Variable: productSlugsQuery
-// Query: *[_type == "product" && defined(slug.current)]{"slug": slug.current}
-export type ProductSlugsQueryResult = Array<{
+// Source: ./src/app/project/[slug]/page.tsx
+// Variable: projectSlugsQuery
+// Query: *[_type == "project" && defined(slug.current)]{"slug": slug.current}
+export type ProjectSlugsQueryResult = Array<{
slug: string | null
}>
-// Variable: productPageQuery
-// Query: *[_type == "product" && slug.current == $slug][0]
-export type ProductPageQueryResult = {
+// Variable: projectPageQuery
+// Query: *[_type == "project" && slug.current == $slug][0]
+export type ProjectPageQueryResult = {
_id: string
- _type: 'product'
+ _type: 'project'
_createdAt: string
_updatedAt: string
_rev: string
title?: string
slug?: Slug
- media?: Array<{
- asset?: {
- _ref: string
- _type: 'reference'
- _weak?: boolean
- [internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
- }
- hotspot?: SanityImageHotspot
- crop?: SanityImageCrop
- alt?: string
- _type: 'image'
- _key: string
- }>
- description?: Array<{
- children?: Array<{
- marks?: Array
- text?: string
- _type: 'span'
- _key: string
- }>
- style?: 'blockquote' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'normal'
- listItem?: 'bullet' | 'number'
- markDefs?: Array<{
- href?: string
- _type: 'link'
- _key: string
- }>
- level?: number
- _type: 'block'
- _key: string
- }>
- brandReference?: unknown
- details?: {
- materials?: string
- collectionNotes?: Array<{
- children?: Array<{
- marks?: Array
- text?: string
- _type: 'span'
- _key: string
- }>
- style?: 'blockquote' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'normal'
- listItem?: 'bullet' | 'number'
- markDefs?: Array<{
- href?: string
- _type: 'link'
- _key: string
- }>
- level?: number
- _type: 'block'
- _key: string
- }>
- performance?: Array<{
- children?: Array<{
- marks?: Array
- text?: string
- _type: 'span'
- _key: string
- }>
- style?: 'blockquote' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'normal'
- listItem?: 'bullet' | 'number'
- markDefs?: Array<{
- href?: string
- _type: 'link'
- _key: string
- }>
- level?: number
- _type: 'block'
- _key: string
- }>
- ledLifespan?: string
- certifications?: Array
- }
- variants?: Array<{
- title?: string
- price?: string
- sku?: string
- _type: 'variant'
- _key: string
- }>
} | null
// Source: ./src/app/pages/[slug]/page.tsx
@@ -1015,22 +937,102 @@ export type PageSlugsResult = Array<{
slug: string | null
}>
-// Source: ./src/app/project/[slug]/page.tsx
-// Variable: projectSlugsQuery
-// Query: *[_type == "project" && defined(slug.current)]{"slug": slug.current}
-export type ProjectSlugsQueryResult = Array<{
+// Source: ./src/app/product/[slug]/page.tsx
+// Variable: productSlugsQuery
+// Query: *[_type == "product" && defined(slug.current)]{"slug": slug.current}
+export type ProductSlugsQueryResult = Array<{
slug: string | null
}>
-// Variable: projectPageQuery
-// Query: *[_type == "project" && slug.current == $slug][0]
-export type ProjectPageQueryResult = {
+// Variable: productPageQuery
+// Query: *[_type == "product" && slug.current == $slug][0]
+export type ProductPageQueryResult = {
_id: string
- _type: 'project'
+ _type: 'product'
_createdAt: string
_updatedAt: string
_rev: string
title?: string
slug?: Slug
+ media?: Array<{
+ asset?: {
+ _ref: string
+ _type: 'reference'
+ _weak?: boolean
+ [internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
+ }
+ hotspot?: SanityImageHotspot
+ crop?: SanityImageCrop
+ alt?: string
+ _type: 'image'
+ _key: string
+ }>
+ description?: Array<{
+ children?: Array<{
+ marks?: Array
+ text?: string
+ _type: 'span'
+ _key: string
+ }>
+ style?: 'blockquote' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'normal'
+ listItem?: 'bullet' | 'number'
+ markDefs?: Array<{
+ href?: string
+ _type: 'link'
+ _key: string
+ }>
+ level?: number
+ _type: 'block'
+ _key: string
+ }>
+ brandReference?: unknown
+ details?: {
+ materials?: string
+ collectionNotes?: Array<{
+ children?: Array<{
+ marks?: Array
+ text?: string
+ _type: 'span'
+ _key: string
+ }>
+ style?: 'blockquote' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'normal'
+ listItem?: 'bullet' | 'number'
+ markDefs?: Array<{
+ href?: string
+ _type: 'link'
+ _key: string
+ }>
+ level?: number
+ _type: 'block'
+ _key: string
+ }>
+ performance?: Array<{
+ children?: Array<{
+ marks?: Array
+ text?: string
+ _type: 'span'
+ _key: string
+ }>
+ style?: 'blockquote' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'normal'
+ listItem?: 'bullet' | 'number'
+ markDefs?: Array<{
+ href?: string
+ _type: 'link'
+ _key: string
+ }>
+ level?: number
+ _type: 'block'
+ _key: string
+ }>
+ ledLifespan?: string
+ certifications?: Array
+ }
+ variants?: Array<{
+ title?: string
+ price?: string
+ sku?: string
+ _type: 'variant'
+ _key: string
+ }>
} | null
declare module '@sanity/client' {
@@ -1038,13 +1040,13 @@ declare module '@sanity/client' {
'\n *[_id == "siteSettings"][0]{\n title,\n description,\n copyrightText\n}': LayoutQueryResult
"\n *[_id == \"siteSettings\"][0]{\n frontPage->{\n _type,\n _id,\n title,\n sections[]{\n ...,\n symbol->{_type},\n 'headline': coalesce(headline, symbol->headline),\n 'tagline': coalesce(tagline, symbol->tagline),\n 'subline': coalesce(subline, symbol->subline),\n 'image': coalesce(image, symbol->image),\n product->{\n _type,\n _id,\n title,\n slug,\n \"media\": media[0]\n },\n products[]{\n _key,\n ...(@->{\n _type,\n _id,\n title,\n slug,\n \"media\": media[0]\n })\n }\n },\n style\n }\n }.frontPage\n": FrontPageQueryResult
'\n *[_type == "dndTestPage"]{\n _id,\n title,\n children\n }[0]\n': DndPageQueryResult
- '\n *[_type == "product" && defined(slug.current)]{\n _id,\n title,\n description,\n slug,\n "media": media[0]\n }\n': ProductsPageQueryResult
'*[_type == "project" && defined(slug.current)]': ProjectsPageQueryResult
- '*[_type == "product" && defined(slug.current)]{"slug": slug.current}': ProductSlugsQueryResult
- '*[_type == "product" && slug.current == $slug][0]': ProductPageQueryResult
- "\n *[_type == \"page\" && slug.current == $slug][0]{\n _type,\n _id,\n title,\n sections[]{\n ...,\n symbol->{_type},\n 'headline': coalesce(headline, symbol->headline),\n 'tagline': coalesce(tagline, symbol->tagline),\n 'subline': coalesce(subline, symbol->subline),\n 'image': coalesce(image, symbol->image),\n product->{\n _type,\n _id,\n title,\n slug,\n \"media\": media[0]\n },\n products[]{\n _key,\n ...(@->{\n _type,\n _id,\n title,\n slug,\n \"media\": media[0]\n })\n }\n },\n style\n }\n": PageQueryResult
- '*[_type == "page" && defined(slug.current)]{"slug": slug.current}': PageSlugsResult
+ '\n *[_type == "product" && defined(slug.current)]{\n _id,\n title,\n description,\n slug,\n "media": media[0]\n }\n': ProductsPageQueryResult
'*[_type == "project" && defined(slug.current)]{"slug": slug.current}': ProjectSlugsQueryResult
'*[_type == "project" && slug.current == $slug][0]': ProjectPageQueryResult
+ "\n *[_type == \"page\" && slug.current == $slug][0]{\n _type,\n _id,\n title,\n sections[]{\n ...,\n symbol->{_type},\n 'headline': coalesce(headline, symbol->headline),\n 'tagline': coalesce(tagline, symbol->tagline),\n 'subline': coalesce(subline, symbol->subline),\n 'image': coalesce(image, symbol->image),\n product->{\n _type,\n _id,\n title,\n slug,\n \"media\": media[0]\n },\n products[]{\n _key,\n ...(@->{\n _type,\n _id,\n title,\n slug,\n \"media\": media[0]\n })\n }\n },\n style\n }\n": PageQueryResult
+ '*[_type == "page" && defined(slug.current)]{"slug": slug.current}': PageSlugsResult
+ '*[_type == "product" && defined(slug.current)]{"slug": slug.current}': ProductSlugsQueryResult
+ '*[_type == "product" && slug.current == $slug][0]': ProductPageQueryResult
}
}
diff --git a/packages/@repo/sanity-schema/src/page-builder-demo/index.tsx b/packages/@repo/sanity-schema/src/page-builder-demo/index.tsx
index 6a816c0f5..f889b3160 100644
--- a/packages/@repo/sanity-schema/src/page-builder-demo/index.tsx
+++ b/packages/@repo/sanity-schema/src/page-builder-demo/index.tsx
@@ -541,6 +541,12 @@ const dndTestItemRoot = defineArrayMember({
name: 'title',
title: 'Title',
}),
+ defineField({
+ type: 'array',
+ name: 'childrenStrings',
+ title: 'Children Strings',
+ of: [{type: 'string'}],
+ }),
defineField({
type: 'array',
name: 'children',
diff --git a/packages/visual-editing/src/util/mutations.ts b/packages/visual-editing/src/util/mutations.ts
index 3ad43a263..6d58de6c7 100644
--- a/packages/visual-editing/src/util/mutations.ts
+++ b/packages/visual-editing/src/util/mutations.ts
@@ -3,11 +3,45 @@ import type {SanityNode} from '@repo/visual-editing-helpers'
export function getArrayItemKeyAndParentPath(pathOrNode: string | SanityNode): {
path: string
key: string
+ hasExplicitKey: boolean
} {
const elementPath = typeof pathOrNode === 'string' ? pathOrNode : pathOrNode.path
- const result = elementPath.match(/^(.+)\[_key=="(.+)"]$/)
- if (!result) throw new Error('Invalid path')
- const [, path, key] = result
+
+ const lastDotIndex = elementPath.lastIndexOf('.')
+ const lastPathItem = elementPath.substring(lastDotIndex + 1, elementPath.length)
+
+ if (!lastPathItem.indexOf('[')) throw new Error('Invalid path: not an array')
+
+ const lastArrayIndex = elementPath.lastIndexOf('[')
+ const path = elementPath.substring(0, lastArrayIndex)
+
+ let key
+ let hasExplicitKey
+
+ if (lastPathItem.includes('_key')) {
+ // explicit [_key="..."]
+
+ const startIndex = lastPathItem.indexOf('"') + 1
+ const endIndex = lastPathItem.indexOf('"', startIndex)
+
+ key = lastPathItem.substring(startIndex, endIndex)
+
+ hasExplicitKey = true
+ } else {
+ // indexes [int]
+ const startIndex = lastPathItem.indexOf('[') + 1
+ const endIndex = lastPathItem.indexOf(']', startIndex)
+
+ key = lastPathItem.substring(startIndex, endIndex)
+
+ hasExplicitKey = false
+ }
+
if (!path || !key) throw new Error('Invalid path')
- return {path, key}
+
+ return {
+ path,
+ key,
+ hasExplicitKey,
+ }
}
diff --git a/packages/visual-editing/src/util/useDragEvents.ts b/packages/visual-editing/src/util/useDragEvents.ts
index 264cabddc..7a182c4e3 100644
--- a/packages/visual-editing/src/util/useDragEvents.ts
+++ b/packages/visual-editing/src/util/useDragEvents.ts
@@ -40,7 +40,7 @@ export function useDragEndEvents(): {
// resolving the currently in use documents
const {node, position} = reference
// Get the key of the element that was dragged
- const {key: targetKey} = getArrayItemKeyAndParentPath(target)
+ const {key: targetKey, hasExplicitKey} = getArrayItemKeyAndParentPath(target)
// Get the key of the reference element, and path to the parent array
const {path: arrayPath, key: referenceItemKey} = getArrayItemKeyAndParentPath(node)
// Don't patch if the keys match, as this means the item was only
@@ -50,12 +50,31 @@ export function useDragEndEvents(): {
// Get the current value of the element we dragged, as we will need
// to clone this into the new position
const elementValue = getFromPath(snapshot, target.path)
- return [
- // Remove the original dragged item
- at(arrayPath, remove({_key: targetKey})),
- // Insert the cloned dragged item into its new position
- at(arrayPath, insert(elementValue, position, {_key: referenceItemKey})),
- ]
+
+ if (hasExplicitKey) {
+ return [
+ // Remove the original dragged item
+ at(arrayPath, remove({_key: targetKey})),
+ // Insert the cloned dragged item into its new position
+ at(arrayPath, insert(elementValue, position, {_key: referenceItemKey})),
+ ]
+ } else {
+ // handle reordering for primitives
+ return [
+ // Remove the original dragged item
+ at(arrayPath, remove(~~targetKey)),
+ // Insert the cloned dragged item into its new position
+ at(
+ arrayPath,
+ insert(
+ elementValue,
+ position,
+ // if target key is < reference, each item in the array's index will be one less due to the previous removal
+ referenceItemKey > targetKey ? ~~referenceItemKey - 1 : ~~referenceItemKey,
+ ),
+ ),
+ ]
+ }
})
}
}