Skip to content

Commit

Permalink
Change sidebar tree creation to use path instead of parentId. Also move
Browse files Browse the repository at this point in the history
sidebar to web-ui
  • Loading branch information
andresgutgon committed Jul 19, 2024
1 parent 6d93bf1 commit ea7ba6f
Show file tree
Hide file tree
Showing 8 changed files with 740 additions and 7 deletions.
13 changes: 11 additions & 2 deletions packages/web-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
"scripts": {
"lint": "eslint ./src",
"tc": "tsc --noEmit",
"prettier": "prettier --write src/**/*.ts"
"prettier": "prettier --write src/**/*.ts",
"test": "vitest run",
"test:watch": "vitest",
"test:debug": "pnpm run test:watch --inspect-brk --pool threads --poolOptions.threads.singleThread"
},
"type": "module",
"main": "src/index.ts",
Expand Down Expand Up @@ -47,10 +50,16 @@
"devDependencies": {
"@latitude-data/eslint-config": "workspace:*",
"@latitude-data/typescript-config": "workspace:*",
"@testing-library/dom": "^10.3.2",
"@testing-library/react": "^16.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/react": "18.3.0",
"@types/react-dom": "18.3.0",
"autoprefixer": "^10.4.19",
"jsdom": "^24.1.0",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.4"
"tailwindcss": "^3.4.4",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.0.3"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = {
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
Expand Down
29 changes: 29 additions & 0 deletions packages/web-ui/src/sections/DocumentsSidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client'

import {
Node,
SidebarDocument,
useTree,
} from '$ui/sections/DocumentsSidebar/useTree'

function TreeNode({ node, level = 0 }: { node: Node; level?: number }) {
return (
<div key={node.id}>
<div className='flex flex-col gap-2' style={{ paddingLeft: level * 2 }}>
{node.children.map((node, idx) => (
<TreeNode key={idx} node={node} level={level + 1} />
))}
</div>
</div>
)
}

export default function DocumentTree({
documents,
}: {
documents: SidebarDocument[]
}) {
const rootNode = useTree({ documents })

return <TreeNode node={rootNode} />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { renderHook } from '@testing-library/react'
import { describe, expect, it } from 'vitest'

const FAKE_RANDOM_ID = 'RANDOM_ID'

let list = [
{ path: 'things/doc1', doumentUuid: '1' },
{ path: 'things/doc2', doumentUuid: '2' },
{ path: 'things/other-things/doc3', doumentUuid: '3' },
{ path: 'something-else/doc4', doumentUuid: '4' },
]

describe('useTree', () => {
it('should return a tree with children', async () => {
const resultImport = await import('./index')
const { useTree } = resultImport

const { result } = renderHook(() =>
useTree({
documents: list,
generateNodeId: ({ uuid }: { uuid?: string } = {}) => {
if (uuid) return uuid

return FAKE_RANDOM_ID
},
}),
)
expect(result.current.toJSON()).toEqual({
id: FAKE_RANDOM_ID,
doc: undefined,
isRoot: true,
name: 'root',
children: [
{
id: FAKE_RANDOM_ID,
doc: undefined,
name: 'things',
isRoot: false,
children: [
{
id: '1',
name: 'doc1',
isRoot: false,
doc: { path: 'things/doc1', doumentUuid: '1' },
children: [],
},
{
id: '2',
name: 'doc2',
isRoot: false,
doc: { path: 'things/doc2', doumentUuid: '2' },
children: [],
},
{
id: FAKE_RANDOM_ID,
isRoot: false,
doc: undefined,
name: 'other-things',
children: [
{
id: '3',
isRoot: false,
name: 'doc3',
doc: { path: 'things/other-things/doc3', doumentUuid: '3' },
children: [],
},
],
},
],
},
{
id: FAKE_RANDOM_ID,
isRoot: false,
doc: undefined,
name: 'something-else',
children: [
{
id: '4',
name: 'doc4',
isRoot: false,
doc: { path: 'something-else/doc4', doumentUuid: '4' },
children: [],
},
],
},
],
})
})
})
118 changes: 118 additions & 0 deletions packages/web-ui/src/sections/DocumentsSidebar/useTree/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { useMemo } from 'react'

export type SidebarDocument = {
path: string
doumentUuid: string
}

export class Node {
public id: string
public name: string
public isRoot: boolean = false
public children: Node[] = []
public doc?: SidebarDocument

constructor({
id,
doc,
children = [],
isRoot = false,
name = '',
}: {
id: string
doc?: SidebarDocument
children?: Node[]
isRoot?: boolean
name?: string
}) {
this.id = id
this.name = isRoot ? 'root' : name
this.isRoot = isRoot
this.children = children
this.doc = doc
}

// Useful for testing
toJSON(): object {
return {
id: this.id,
name: this.name,
isRoot: this.isRoot,
children: this.children.map((child) => child.toJSON()),
doc: this.doc,
}
}
}

function sortByPathDepth(a: SidebarDocument, b: SidebarDocument) {
const depth1 = (a.path.match(/\//g) || []).length
const depth2 = (b.path.match(/\//g) || []).length
return depth1 - depth2
}

export function defaultGenerateNodeUuid({ uuid }: { uuid?: string } = {}) {
if (uuid) return uuid

return Math.random().toString(36).substring(2, 15)
}

function buildTree({
root,
nodeMap,
documents,
generateNodeId,
}: {
root: Node
nodeMap: Map<string, Node>
documents: SidebarDocument[]
generateNodeId: typeof defaultGenerateNodeUuid
}) {
documents.forEach((doc) => {
const segments = doc.path.split('/')
let path = ''

segments.forEach((segment, index) => {
const isFile = index === segments.length - 1
path = path ? `${path}/${segment}` : segment

if (!nodeMap.has(path)) {
const file = isFile ? doc : undefined
const uuid = isFile ? doc.doumentUuid : undefined
const node = new Node({
id: generateNodeId({ uuid }),
doc: file,
name: segment,
})
nodeMap.set(path, node)

const parentPath = path.split('/').slice(0, -1).join('/')

// We force TypeScript to check that the parentPath is not empty
// We pre-sorted documents by path depth, so we know
// that the parent node exists
const parent = nodeMap.get(parentPath)!
parent.children.push(node)
}
})
})

return root
}

export function useTree({
documents,
generateNodeId = defaultGenerateNodeUuid,
}: {
documents: SidebarDocument[]
generateNodeId?: typeof defaultGenerateNodeUuid
}) {
return useMemo(() => {
const root = new Node({ id: generateNodeId(), children: [], isRoot: true })
const nodeMap = new Map<string, Node>()
nodeMap.set('', root)
const sorted = documents.slice().sort(sortByPathDepth)

const tree = buildTree({ root, nodeMap, documents: sorted, generateNodeId })
return tree
}, [documents])
}
1 change: 1 addition & 0 deletions packages/web-ui/src/sections/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './DocumentEditor'
export * from './DocumentsSidebar'
13 changes: 13 additions & 0 deletions packages/web-ui/vitest.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from 'vitest/config'
import { resolve } from 'path'

export default defineConfig({
resolve: {
alias: {
$ui: resolve(__dirname, './src'),
},
},
test: {
environment: 'jsdom',
},
})
Loading

0 comments on commit ea7ba6f

Please sign in to comment.