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"