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, + ), + ), + ] + } }) } }