import { TriangleRightMini } from "@medusajs/icons" import { Checkbox, clx, Divider, Text } from "@medusajs/ui" import React, { useImperativeHandle, useMemo, useState, useEffect } from "react" export type EntityField = { id: string name: string selected?: boolean } export type Entity = { id: string name: string fields?: EntityField[] selected?: boolean } type ViewMode = "full" | "selected" type SortOrder = "asc" | "desc" type SelectorRowProps = { leftElement?: React.ReactNode expandButton?: React.ReactNode checked: boolean | "indeterminate" onCheckedChange: () => void label: string className?: string } const SelectorRow = ({ leftElement, expandButton, checked, onCheckedChange, label, className, }: SelectorRowProps) => { const isSelected = checked !== false return (
{leftElement} {expandButton} {label}
) } export type EntitySelectorTreeRef = { selectAllToggle: (selected: boolean) => void collapseAll: () => void } type EntitySelectorTreeProps = { entities: Entity[] onSelectionChange?: (selectedIds: Set) => void searchQuery: string viewMode: ViewMode sortOrder: SortOrder } export const EntitySelectorTree = React.forwardRef< EntitySelectorTreeRef, EntitySelectorTreeProps >(({ entities, onSelectionChange, searchQuery, viewMode, sortOrder }, ref) => { const [expandedEntities, setExpandedEntities] = useState>( new Set() ) const [selectedIds, setSelectedIds] = useState>(new Set()) useEffect(() => { const ids = new Set() entities.forEach((entity) => { entity.fields?.forEach((field) => { if (field.selected) { ids.add(`${entity.id}.${field.id}`) } }) }) setSelectedIds(ids) }, [entities]) const toggleExpand = (entityId: string) => { setExpandedEntities((prev) => { const next = new Set(prev) if (next.has(entityId)) { next.delete(entityId) } else { next.add(entityId) } return next }) } const getEntitySelectionState = ( entity: Entity ): true | false | "indeterminate" => { if (!entity.fields?.length) { return false } const selectedFieldsCount = entity.fields.filter((field) => selectedIds.has(`${entity.id}.${field.id}`) ).length if (selectedFieldsCount === 0) { return false } if (selectedFieldsCount === entity.fields.length) { return true } return "indeterminate" } const toggleEntitySelection = (entity: Entity) => { setSelectedIds((prev) => { const next = new Set(prev) const state = getEntitySelectionState(entity) const isSelected = state === true // Toggle all fields for this entity entity.fields?.forEach((field) => { const fieldKey = `${entity.id}.${field.id}` if (isSelected) { next.delete(fieldKey) } else { next.add(fieldKey) } }) onSelectionChange?.(next) return next }) } const toggleFieldSelection = (entityId: string, fieldId: string) => { setSelectedIds((prev) => { const next = new Set(prev) const fieldKey = `${entityId}.${fieldId}` if (next.has(fieldKey)) { next.delete(fieldKey) } else { next.add(fieldKey) } onSelectionChange?.(next) return next }) } const selectAllToggle = (selected: boolean) => { if (selected) { const allIds = new Set() entities.forEach((entity) => { entity.fields?.forEach((field) => { allIds.add(`${entity.id}.${field.id}`) }) }) setSelectedIds(allIds) onSelectionChange?.(allIds) } else { setSelectedIds(new Set()) onSelectionChange?.(new Set()) } } const collapseAll = () => setExpandedEntities(new Set()) useImperativeHandle(ref, () => ({ selectAllToggle, collapseAll, })) const filteredAndSortedEntities = useMemo(() => { let filtered = entities if (searchQuery) { const query = searchQuery.toLowerCase() filtered = entities.filter((entity) => { const matchesEntity = entity.name.toLowerCase().includes(query) const matchesFields = entity.fields?.some((field) => field.name.toLowerCase().includes(query) ) return matchesEntity || matchesFields }) } if (viewMode === "selected") { filtered = filtered.filter((entity) => { const state = getEntitySelectionState(entity) if (state === false) { return false } return true }) } const sorted = [...filtered].sort((a, b) => { const comparison = a.name.localeCompare(b.name) return sortOrder === "asc" ? comparison : -comparison }) return sorted }, [entities, searchQuery, viewMode, sortOrder, selectedIds]) return (
{filteredAndSortedEntities.length === 0 ? (
No entities matching filters
) : (
{filteredAndSortedEntities.map((entity) => { const isExpanded = expandedEntities.has(entity.id) const hasFields = entity.fields && entity.fields.length > 0 const selectionState = getEntitySelectionState(entity) return (
toggleEntitySelection(entity)} label={entity.name} className="hover:bg-ui-bg-component-hover" expandButton={ hasFields ? ( ) : null } /> {hasFields && isExpanded && (
{entity.fields!.map((field) => { const fieldKey = `${entity.id}.${field.id}` const isFieldSelected = selectedIds.has(fieldKey) return ( } checked={isFieldSelected} onCheckedChange={() => { toggleFieldSelection(entity.id, field.id) }} label={field.name} /> ) })}
)}
) })}
)}
) }) EntitySelectorTree.displayName = "EntitySelectorTree"