import type { EntityID } from '@wovin/core/applog' import type { ParentComponent } from 'solid-js' import type { BlockPanelDef } from '../data/block-panels' import type { SmartQuery } from '../data/smart-list' import type { BlockVM } from '../data/VMs/BlockVM' import type { FakeBlock } from './BlockTree' import type { SortableBlockItem } from './SortableBlocks' import { createLazyMemo } from '@solid-primitives/memo' import { observableArrayMap } from '@wovin/core' import { autorun, untracked } from '@wovin/core/mobx' import { Logger } from 'besonders-logger' import classNames from 'classnames' import uniq from 'lodash-es/uniq' import stringify from 'safe-stable-stringify' import { createEffect, createMemo, createSignal, Match, mergeProps, on, Show, Switch, untrack } from 'solid-js' import { BlockPanel, useDivergencePanel } from '../data/block-panels' import { useBlockContext } from '../data/block-ui-helpers' import { TAG_MIN_LENGTH } from '../data/note3-regex-constants' import { plaintextStringToTiptap, tiptapToPlaintext } from '../data/note3-utils-nodeps' import { useSearchContext } from '../data/search' import { getSmartLists, parseSmartQuery, smartQueryIsError } from '../data/smart-list' import { arrayReUseItemsIfEqual, findAllDeeplyNested } from '../data/utils-data' import { useBlk } from '../data/VMs/BlockVM' import { REL, useRel } from '../data/VMs/RelationVM' import { useAppSettings } from '../ui/app-settings' import { useCurrentThread, useEntityAt, useFocus, useIsTimeline, useKidRelations, useRawThread, useReadOnlyState, withDS } from '../ui/reactive' import { BlockItem } from './BlockContent' import { useBlockSetting } from './BlockSettings' import { AUTOCOLLAPSE_DEPTH_DEFAULT, AUTOCOLLAPSE_DEPTH_FOCUSSED_DEFAULT, AUTOCOLLAPSE_DEPTH_TIMELINE_DEFAULT, AUTOCOLLAPSE_DEPTH_TIMELINE_SMARTLIST_DEFAULT, BlockContext, MAX_DEPTH } from './BlockTree' import { KidsAsSurvey } from './KidsAsSurvey' import { KidsAsTabs } from './KidsAsTabs' import { SortableBlocks } from './SortableBlocks' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line unused-imports/no-unused-vars export const TreeItem: ParentComponent<{ blockID?: EntityID fake?: FakeBlock kidBlockIDs?: readonly EntityID[] relationID?: EntityID parentRelationID?: EntityID // TODO: from context? class?: string }> = ( _props, ) => { const props = mergeProps({ relationID: null, parentRelationID: null }, _props) // ? why const debugName = untracked(() => props.blockID ?? (props.fake ? stringify({ ...props.fake, content: props.fake.content ? tiptapToPlaintext(props.fake.content) : undefined, }) : null), ) // ?? stringify(omit(props.smartList, 'blocks'))) if (!debugName) throw ERROR(` with missing info what to render`, props) if (props.blockID && props.fake) throw ERROR(` with conflicting info what to render`, props) VERBOSE(`[TreeItem#${debugName}] created`) const rawThread = useRawThread() // const appThread = getApplogDB() const currentThread = useCurrentThread() const readOnly = useReadOnlyState() const blockID = createMemo(() => props.blockID) const isRealBlock = untrack(() => !!props.blockID) const block = isRealBlock ? useBlk(props.blockID) : props.fake const isDeleted = createMemo(() => (block as BlockVM).isDeleted) const focus = useFocus() const isTimeline = useIsTimeline() const [search] = useSearchContext() const parentContext = useBlockContext() const depth = (parentContext?.depth ?? 0) + 1 const appSettings = useAppSettings() // Get autocollapse depths from settings or use defaults const getAutocollapseDepth = () => appSettings.autocollapseDepth ?? AUTOCOLLAPSE_DEPTH_DEFAULT const getAutocollapseDepthFocussed = () => appSettings.autocollapseDepthFocussed ?? AUTOCOLLAPSE_DEPTH_FOCUSSED_DEFAULT const getAutocollapseDepthTimeline = () => appSettings.autocollapseDepthTimeline ?? AUTOCOLLAPSE_DEPTH_TIMELINE_DEFAULT const getAutocollapseDepthTimelineSmartlist = () => appSettings.autocollapseDepthTimelineSmartlist ?? AUTOCOLLAPSE_DEPTH_TIMELINE_SMARTLIST_DEFAULT // const block = useBlockVM(blockID) // const kidRelationIDs = useKidRelationIDs(props.blockID) const kidRelationIDs = isRealBlock && observableArrayMap(() => withDS( rawThread, () => useKidRelations(props.blockID).map(({ en }) => en), /* not using useKidRelationIDs because this one orders them */ ), ) const [isRelExpanded, setRelExpanded] = props.relationID ? withDS(currentThread, () => useEntityAt(props.relationID, REL.isExpanded, true)) : createSignal(true) createEffect(() => VERBOSE(` isRelExpanded=${isRelExpanded()}`)) const shouldAutoCollapse = createMemo(() => { if (parentContext?.autoExpandDepth()) return false // In timeline view, smartlists are collapsed at depth 0 if (isTimeline() && block.type === 'smartlist') { return depth > getAutocollapseDepthTimelineSmartlist() } // In timeline view, regular blocks collapse at depth 1 if (isTimeline()) { return depth > getAutocollapseDepthTimeline() } // Default autocollapse logic for focus/normal view return (block.type === 'smartlist' // if we are a smartlist... && focus() !== blockID() // ... and not focussed && !(search() && !parentContext?.smartList)) // ... and not the top smartlist in the search || parentContext?.smartList // or, if we are in a smartlist || (parentContext && !parentContext?.id) // or, if we are in a smartlist's kids (HACK way to find out) || (depth > (focus() ? getAutocollapseDepthFocussed() : getAutocollapseDepth())) // or, if we at a certain depth }) const [isExpandedOverride, setIsExpandedOverride] = createSignal(untrack(() => shouldAutoCollapse() ? false : null)) createEffect(on(() => focus(), () => { // if you focus a block that was root previously, this is needed to expand it (and vice versa) /* if (focus()) */ setIsExpandedOverride(shouldAutoCollapse() ? false : null) })) const [autoExpandDepth, setAutoExpandDepth] = createSignal(Math.max(0, parentContext?.autoExpandDepth() - 1)) createEffect(on(() => parentContext?.autoExpandDepth(), (parentAutoExpandDepth) => { setAutoExpandDepth(Math.max(0, parentAutoExpandDepth - 1)) })) const isExpanded = createMemo(() => { VERBOSE( `[Autocollapse#${debugName}]`, `isRelExpanded=${isRelExpanded()}`, `isExpandedOverride=${isExpandedOverride()}`, `depth=${depth}`, ) if (autoExpandDepth() > 0) return true return isExpandedOverride() ?? isRelExpanded() }) const setExpanded = (state: boolean) => { if (readOnly()) { setIsExpandedOverride(state) } else { setRelExpanded(state) /* if (state) */ setIsExpandedOverride(null) } if (!state && autoExpandDepth() > 0) { setAutoExpandDepth(0) } return state } const [panel, setPanelValue] = createSignal(null) const setPanel = (newPanel: BlockPanelDef) => { if (panel()) WARN(`[setPanel] but we already have a panel:`, { prev: panel(), new: newPanel }) setPanelValue(newPanel) // HACK: handle double panel } if (isRealBlock) { useDivergencePanel(props.blockID, setPanel) } // let blockSpan // onMountFx(() => { // interact(blockSpan) // .draggable({ inertia: true }) // .on('dragend', function(event) { // const prevSpeed = event._interaction.prevEvent.speed // event._interaction.prevEvent.speed = 601 // event.swipe = event.getSwipe() // event.swipe.speed = prevSpeed // if (!event.swipe) { // DEBUG('no swipe', event, event.getSwipe()) // return // } // const { angle, speed, right, left } = event.swipe // LOG('swipe', { angle, speed, right, left }) // if (right) indentBlk(relation) // if (left) outdentBlk(relation, parentRelation) // }) // .on('doubletap', function(event: PointerEvent) { // stopPropagation(event) // LOG('double', event, { setRadialPos, traceSet }) // const { clientX: screenX, clientY: screenY } = event // traceSet({ screenX, screenY }) // }) // })() const kidItemsFromRelations = createLazyMemo(function kidItemsFromRelations() { DEBUG(``, { kidRelationIDs, blockID: blockID(), block }) if (kidRelationIDs) { return kidRelationIDs.map((relationID) => { const rel = useRel(relationID) const block = useBlk(rel.block) if (!block) WARN('rel refers to missing block', { block, rel }) return ({ id: relationID, relationID, blockID: rel.block, type: block?.type, }) }) satisfies SortableBlockItem[] } }) const smartQueryOrError = createLazyMemo(() => { if (block.type !== 'smartlist') return null const parentQuery = parentContext?.smartList?.query const ourQuery = block.smartQuery?.source.trim() if (!ourQuery || ourQuery.length < TAG_MIN_LENGTH + 1) return null // HACK: should use smartQuery parsing? const tagsQueryStr = parentQuery?.source ? (parentQuery.source.includes(' ') ? `(${parentQuery.source}) AND ${ourQuery}` : `${parentQuery?.source} ${ourQuery}`) // HACK to simplify : ourQuery // ? how to join queries? const query = parseSmartQuery(tagsQueryStr) DEBUG('[smartQueryOrError]', query) return query }) const smartListItems = createLazyMemo(function smartListItems() { // if (!isRealBlock) return null DEBUG(``, { blockID: props.blockID, fake: props.fake, block, parentContext }) const [exclusiveSetting] = blockID() ? useBlockSetting(blockID(), 'smart_list/exclusive_groups') : [() => false] if (block.type === 'smartlist') { // I'm doing some reactivity magic here, I think mainly to not trigger a reactive update if the smartlist contents change // all things that come from solid here: const query = smartQueryOrError() if (smartQueryIsError(query)) return null const excludeBlocks = new Set([props.blockID]) const kidItemsFromRel = kidItemsFromRelations()?.filter(({ type }) => type === 'smartlist') // because mobx will not react to solid changes: return untrack(() => // untrack = solid, you're done here... observableArrayMap(() => { const stickySmartListBlocks = kidItemsFromRel?.map((item) => { const block = useBlk(item.blockID) if (!block.smartQuery) return item const smartLists = withDS(rawThread, () => getSmartLists(block.smartQuery, { excludeBlocks, exclusiveGroups: exclusiveSetting() })) DEBUG(``, { smartLists, excludeBlocks, exclusiveGroups: exclusiveSetting() }) smartLists?.forEach(({ blocks }) => { return blocks.forEach(blockID => excludeBlocks.add(blockID), ) }) return { ...item, smartLists } }) const items: SortableBlockItem[] = stickySmartListBlocks ?? [] const dynamicSmartLists = withDS(rawThread, () => getSmartLists(query, { excludeBlocks, exclusiveGroups: exclusiveSetting() })) for (const smartList of dynamicSmartLists) { items.push({ id: `SmartList#${props.blockID}`, fake: { content: plaintextStringToTiptap(smartList.title), // type: 'smartlist', // smartQuery: query, // ? get smartQuery() { // return query // } }, kidBlockIDs: smartList.blocks, }) } DEBUG(` =>`, { query, stickySmartListBlocks, dynamicSmartLists, items }) return items }), ) } }) const allKidItems = createLazyMemo(function kidItems(oldMemo: SortableBlockItem[] | undefined) { if (!isExpanded()) return null if (DEBUG.isEnabled) { DEBUG(``, { kidRelationIDs, blockID: blockID(), block, kidItemsFromRelations: kidItemsFromRelations(), kidBlockIDs: props.kidBlockIDs, smartListItems: smartListItems(), }) } let items: SortableBlockItem[] = [] let kidItemsFromRel = kidItemsFromRelations() if (kidItemsFromRel) { if (block.type === 'smartlist') { kidItemsFromRel = kidItemsFromRel.filter(({ type }) => type !== 'smartlist') // HACK: refactor } // (i) concat bc. https://stackoverflow.com/a/51860949 items = items.concat(kidItemsFromRel) } if (props.kidBlockIDs) { items = items.concat(props.kidBlockIDs.map(blockID => ({ id: `FakeDragRelation_${blockID}`, blockID }))) } // if (!kidRelationIDs && blockID()) { // items.push({ id: `FakeDragRelation_${blockID}`, blockID: blockID() }) // } else if (props.smartList?.blocks) { // items.push(...props.smartList.blocks.map(kidID => ({ id: `FakeDragRelation_${kidID}`, blockID: kidID }))) // } else { // if (blockIDs) return props.blockIDs.map(blockID => ({ id: blockID, blockID })) if (smartListItems()) { if (smartListItems().length === 1) { // ? && smartListItems()[0].title ==='Other') // HACK: move to getSmartList logic? smartListItems()[0].kidBlockIDs.map(blockID => items.push({ id: `FakeDragRelation_${blockID}`, blockID })) } else { items = items.concat(smartListItems()) // .map(kidID => ({ id: `FakeDragRelation-${kidID}`, blockID: kidID }))) } } // } DEBUG(` => `, items) return arrayReUseItemsIfEqual(oldMemo, items) // Re-using bc. checks by identity - https://stackoverflow.com/a/70820352 }) const likelyHasKids = createMemo(() => { // (i) this is trying to *estimate* if kids exist without needing to calculate the list if (block.type === 'smartlist') return true // we would show 'no matches' if (isExpanded()) return allKidItems().length return kidRelationIDs?.length || props.kidBlockIDs?.length }) const concatMaybeArrays = (...maybeArrays) => { DEBUG({ maybeArrays }) let returnArray = [] for (const eachMaybeArray of maybeArrays) { if (eachMaybeArray?.length) returnArray = returnArray.concat(eachMaybeArray) } return returnArray } const [tagsHiddenSetting] = isRealBlock ? useBlockSetting(props.blockID, 'smart_list/hide_parent_tags', true) : [() => true] // TODO default const hiddenTags = () => { const tags = uniq(concatMaybeArrays( parentContext?.hiddenTags(), tagsHiddenSetting() && (smartQueryOrError() && !smartQueryIsError(smartQueryOrError()) ? [...findAllDeeplyNested((smartQueryOrError() as SmartQuery).ast, t => t?.type === 'TAG')] .map(({ symbol, name }: any) => symbol + name) : null), )) DEBUG(`[hiddenTags#${props.blockID}]`, { query: smartQueryOrError(), tags }) return tags } return (
Error: {smartQueryOrError().error} ({search()?.length >= 3 ? 'no matches' : 'enter a search term to see matching blocks'})
{/* FIXME: coll? {JSON.stringify(isExpanded())} */ } { /* ยทยทยท ({kidCount()})
} > */ } {(kid) => { DEBUG(``, kid) if (kid.relationID) { const rel = useRel(kid.relationID) untrack(() => DEBUG( ` creating block: ${rel.block}`, ), ) DEBUG.isEnabled && autorun(() => untrack(() => DEBUG(` data`, { kid: { ...kid }, rel: { ...rel } })), ) return (
) } else { DEBUG('kid without relation', kid) return (
) } }}
{/* */} {/* */} = MAX_DEPTH}>
Children hidden
{ /* */ }
) }