Skip to content

Commit

Permalink
🐛 Fix slug input #2022 (#2023)
Browse files Browse the repository at this point in the history
  • Loading branch information
fernandolucchesi authored Dec 15, 2023
1 parent b58c779 commit 1d67c84
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 36 deletions.
49 changes: 22 additions & 27 deletions sanityv3/schemas/components/SlugInput/index.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,21 @@
import React, { type FormEvent, useCallback, useMemo } from 'react'
import type { Path, SanityDocument, SlugInputProps, SlugParent, SlugSourceContext, SlugSourceFn } from 'sanity'
import { Box, Button, Card, Flex, Stack, TextInput } from '@sanity/ui'
import { useCallback, useMemo } from 'react'
import {
getValueAtPath,
ObjectInputProps,
PatchEvent,
Path,
SanityDocument,
set,
setIfMissing,
SlugParent,
SlugSchemaType,
SlugSourceContext,
SlugSourceFn,
SlugValue,
unset,
useFormBuilder,
} from 'sanity'
import { PatchEvent, set, setIfMissing, unset, useFormValue } from 'sanity'
import { slugify } from './utils/slugify'
import { useAsync } from './utils/useAsync'
import { SlugContext, useSlugContext } from './utils/useSlugContext'
import { slugify as sanitySlugify } from './utils/slugify'
import * as PathUtils from './utils/paths'

/**
*
* @hidden
* @beta
*/
export type SlugInputProps = ObjectInputProps<SlugValue, SlugSchemaType>

function getSlugSourceContext(valuePath: Path, document: SanityDocument, context: SlugContext): SlugSourceContext {
const parentPath = valuePath.slice(0, -1)
const parent = getValueAtPath(document, parentPath) as SlugParent
const parent = PathUtils.get(document, parentPath) as SlugParent
return { parentPath, parent, ...context }
}

Expand All @@ -39,25 +27,29 @@ async function getNewFromSource(
): Promise<string | undefined> {
return typeof source === 'function'
? source(document, context)
: (getValueAtPath(document, source as Path) as string | undefined)
: (PathUtils.get(document, source) as string | undefined)
}

/**
*
* @hidden
* @beta
*/
export function SlugInput(props: SlugInputProps) {
const { getDocument } = useFormBuilder().__internal
const getFormValue = useFormValue([])
const { path, value, schemaType, validation, onChange, readOnly, elementProps } = props
const sourceField = schemaType.options?.source
const errors = useMemo(() => validation.filter((item) => item.level === 'error'), [validation])

const slugContext = useSlugContext()

const updateSlug = useCallback(
async (nextSlug: any) => {
(nextSlug: string) => {
if (!nextSlug) {
onChange(PatchEvent.from(unset([])))
return
}

onChange(PatchEvent.from([setIfMissing({ _type: schemaType.name }), set(nextSlug, ['current'])]))
},
[onChange, schemaType.name],
Expand All @@ -68,16 +60,19 @@ export function SlugInput(props: SlugInputProps) {
return Promise.reject(new Error(`Source is missing. Check source on type "${schemaType.name}" in schema`))
}

const doc = getDocument() || ({ _type: schemaType.name } as SanityDocument)
const doc = (getFormValue as SanityDocument) || ({ _type: schemaType.name } as SanityDocument)
const sourceContext = getSlugSourceContext(path, doc, slugContext)
return getNewFromSource(sourceField, doc, sourceContext)
.then((newFromSource) => sanitySlugify(newFromSource || '', schemaType, sourceContext))
.then((newFromSource) => slugify(newFromSource || '', schemaType, sourceContext))
.then((newSlug) => updateSlug(newSlug))
}, [path, updateSlug, getDocument, schemaType, slugContext])
}, [sourceField, getFormValue, schemaType, path, slugContext, updateSlug])

const isUpdating = generateState?.status === 'pending'

const handleChange = useCallback((event: any) => updateSlug(event.currentTarget.value), [updateSlug])
const handleChange = React.useCallback(
(event: FormEvent<HTMLInputElement>) => updateSlug(event.currentTarget.value),
[updateSlug],
)

return (
<Stack space={3}>
Expand Down
91 changes: 91 additions & 0 deletions sanityv3/schemas/components/SlugInput/utils/paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { IndexTuple, isIndexSegment, isIndexTuple, isKeySegment, KeyedSegment, Path, PathSegment } from 'sanity'

const rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g
const reKeySegment = /_key\s*==\s*['"](.*)['"]/

export const FOCUS_TERMINATOR = '$'

export function get<R>(obj: unknown, path: Path | string): R | undefined
export function get<R>(obj: unknown, path: Path | string, defaultValue: R): R
export function get(obj: unknown, path: Path | string, defaultVal?: unknown): unknown {
const select = typeof path === 'string' ? fromString(path) : path
if (!Array.isArray(select)) {
throw new Error('Path must be an array or a string')
}

let acc: unknown | undefined = obj
for (let i = 0; i < select.length; i++) {
const segment = select[i]
if (isIndexSegment(segment)) {
if (!Array.isArray(acc)) {
return defaultVal
}

acc = acc[segment]
}

if (isKeySegment(segment)) {
if (!Array.isArray(acc)) {
return defaultVal
}

acc = acc.find((item) => item._key === segment._key)
}

if (typeof segment === 'string') {
acc =
typeof acc === 'object' && acc !== null
? ((acc as Record<string, unknown>)[segment] as Record<string, unknown>)
: undefined
}

if (typeof acc === 'undefined') {
return defaultVal
}
}

return acc
}

export function fromString(path: string): Path {
if (typeof path !== 'string') {
throw new Error('Path is not a string')
}

const segments = path.match(rePropName)
if (!segments) {
throw new Error('Invalid path string')
}

return segments.map(normalizePathSegment)
}

function normalizePathSegment(segment: string): PathSegment {
if (isIndexSegment(segment)) {
return normalizeIndexSegment(segment)
}

if (isKeySegment(segment)) {
return normalizeKeySegment(segment)
}

if (isIndexTuple(segment)) {
return normalizeIndexTupleSegment(segment)
}

return segment
}

function normalizeIndexSegment(segment: string): PathSegment {
return Number(segment.replace(/[^\d]/g, ''))
}

function normalizeKeySegment(segment: string): KeyedSegment {
const segments = segment.match(reKeySegment)
return { _key: segments![1] }
}

function normalizeIndexTupleSegment(segment: string): IndexTuple {
const [from, to] = segment.split(':').map((seg) => (seg === '' ? seg : Number(seg)))
return [from, to]
}
11 changes: 2 additions & 9 deletions sanityv3/schemas/components/SlugInput/utils/slugify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,12 @@ import speakingurl from 'speakingurl'
// Fallback slugify function if not defined in field options
const defaultSlugify = (value: FIXME, type: SlugSchemaType): string => {
const maxLength = type.options?.maxLength
const slugifyOpts = {
truncate: typeof maxLength === 'number' ? maxLength : 200,
symbols: true,
}
const slugifyOpts = { truncate: typeof maxLength === 'number' ? maxLength : 200, symbols: true }
return value ? speakingurl(value, slugifyOpts) : ''
}

// eslint-disable-next-line require-await
export async function slugify(
sourceValue: FIXME,
type: SlugSchemaType,
context: SlugSourceContext,
): Promise<string> {
export async function slugify(sourceValue: FIXME, type: SlugSchemaType, context: SlugSourceContext): Promise<string> {
if (!sourceValue) {
return sourceValue
}
Expand Down

0 comments on commit 1d67c84

Please sign in to comment.