import { ArrayField as ArrayFieldType, SchemaFieldProps, VevProps } from '@vev/utils'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { SilkeBox } from '../../silke-box'; import { SilkeButton } from '../../silke-button'; import { SilkeIcon } from '../../silke-icon'; import { SilkeList } from '../../silke-list'; import { SilkeText, SilkeTextSmall } from '../../silke-text'; import { ArrayFieldModal } from './array-field-modal'; import { SilkeOverflowMenu } from '../../silke-overflow-menu'; import { generateImage, generateTitle, getImageFieldUrl } from './utils'; import { isString, startCase } from 'lodash'; import { getTitle } from './utils/schema-util'; import { getSchemaDefaultValue } from '../schema-utils'; import type { SilkeListItemData } from '../../silke-list/types'; /** * Checks if `of` is a single field definition object (e.g. `{ type: 'string' }`) * as opposed to a string literal like `'string'` or an array of VevProps. */ function isSingleFieldOf(of: any): of is VevProps { return of && typeof of === 'object' && !Array.isArray(of) && 'type' in of; } /** * Normalizes `schema.of` — converts the single-field shorthand `{ type: 'string' }` * into the primitive string `'string'` for code paths that expect it, while preserving * the full field definition for modal rendering. */ function getNormalizedOf(schema: ArrayFieldType): VevProps[] | 'string' | 'number' | 'object' { if (isSingleFieldOf(schema.of)) { if (schema.of.type === 'string' || schema.of.type === 'number') { return schema.of.type; } return [schema.of]; } return schema.of as any; } /** Resolves a list item icon: emoji icon for image URLs, silke icon name for fallback. */ function resolveItemIcon( item: any, fieldSchema: VevProps | VevProps[] | string | undefined, ): any { // Check preview/custom image first const imageUrl = generateImage(item, fieldSchema); if (imageUrl && isString(imageUrl)) { return { type: 2, value: { url: imageUrl } }; } // Check if any sub-field is an image field if (typeof item === 'object' && item) { const fieldUrl = getImageFieldUrl(item, fieldSchema); if (isString(fieldUrl)) return { type: 2, value: { url: fieldUrl } }; if (fieldUrl === true) return 'image'; } return undefined; } /** Finds the field schema for a given array item (handles multi-type `of`). */ function getFieldSchema( item: any, schema: ArrayFieldType, ): { fieldSchema: VevProps | VevProps[] | string | undefined; schemaType: string | undefined } { const [schemaType] = typeof item === 'object' && item ? Object.keys(item) : []; const normalizedOf = getNormalizedOf(schema); const fieldSchema = Array.isArray(normalizedOf) ? normalizedOf.find((s: VevProps) => s.name === schemaType) : normalizedOf === 'object' && schema.preview ? schema : normalizedOf; return { fieldSchema, schemaType }; } const ArrayField = ({ value = [], schema, disabled, readonly, onChange, }: SchemaFieldProps) => { const [selected, setSelected] = useState(undefined); const [schemaIndex, setSchemaIndex] = useState(0); const headerRef = useRef(null); const valueRef = useRef(value); useEffect(() => { valueRef.current = value; }, [value]); const addItem = useCallback( (defaultValue: any) => { const newValue = [...valueRef.current, defaultValue]; valueRef.current = newValue; onChange(newValue); setSelected(newValue.length - 1); }, [onChange], ); const handleDeleteItem = useCallback(() => { const update = value.filter((_, i) => i !== selected); onChange(update); setSelected(undefined); }, [onChange, value, selected]); if (!Array.isArray(value)) value = []; const isMultiType = Array.isArray(schema.of) && !isSingleFieldOf(schema.of) && schema.of.length > 1; const listItems: SilkeListItemData[] = useMemo( () => value.map((item, i) => { const { fieldSchema, schemaType } = getFieldSchema(item, schema); return { label: typeof item === 'string' ? item : generateTitle(item, fieldSchema) || '', icon: resolveItemIcon(item, fieldSchema), active: selected === i, children: , onClick: () => { if (isMultiType) { const schemaValues = (schema.of as VevProps[]).map((s) => s.name); setSchemaIndex(schemaValues.indexOf(schemaType!) || 0); } setSelected(i); }, }; }), [value, schema, selected, isMultiType], ); const handleAddDefault = useCallback(() => { const normalizedOf = getNormalizedOf(schema); const defaultValue = Array.isArray(normalizedOf) ? getSchemaDefaultValue(normalizedOf[0]) : normalizedOf === 'object' && 'fields' in schema ? getSchemaDefaultValue(schema.fields) : normalizedOf === 'number' ? 0 : ''; addItem(defaultValue); }, [schema, addItem]); return ( {getTitle(schema)} {isMultiType ? ( ({ label: item.title || startCase(item.name), onClick: () => { setSchemaIndex(i); addItem(getSchemaDefaultValue((schema.of as VevProps[])[i])); }, }))} /> ) : ( )} {schema.description && {schema.description}} {selected !== undefined && ( setSelected(undefined)} /> )} {!value.length && ( No data )} {value.length > 0 && ( { const reordered = (sorted as SilkeListItemData[]).map((item) => { const idx = listItems.indexOf(item); return value[idx]; }); onChange(reordered); }} /> )} ); }; export default ArrayField;