Skip to content

Commit

Permalink
Directory path to file search (#80)
Browse files Browse the repository at this point in the history
* added directory path information to file search
  • Loading branch information
johan-lindell authored Oct 11, 2024
1 parent b9e6bc2 commit 9ec2a4f
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 29 deletions.
14 changes: 11 additions & 3 deletions components/header/navbar/searchpage/FileCard.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React from 'react'
import Image from 'next/image'
import { MdOutlineFolder } from 'react-icons/md'

type FileCardProps = {
id: string
name: string
thumbnailLink?: string | null
folderPath?: string
}
const FileCard = ({ id, name, thumbnailLink }: FileCardProps) => {
const FileCard = ({ id, name, thumbnailLink, folderPath }: FileCardProps) => {
return (
<a
className="flex items-center p-2 border-b border-lightGray border-opacity-50 hover:bg-lightGray hover:bg-opacity-10 w-full"
className="flex items-center p-2 border-b border-lightGray border-opacity-50 hover:bg-lightGray hover:bg-opacity-10 no-underline"
href={`/api/drive/private/download?fileId=${id}&fileName=${name}`}
>
{thumbnailLink && (
Expand All @@ -21,7 +23,13 @@ const FileCard = ({ id, name, thumbnailLink }: FileCardProps) => {
style={{ width: 130, height: 'auto', marginTop: 5, marginBottom: 5 }}
/>
)}
<p className="text-white ml-2">{name}</p>
<div className="text-white ml-2">
<div className="flex flex-row items-center opacity-80 text-sm mb-2">
<MdOutlineFolder />
{folderPath}
</div>
{name}
</div>
</a>
)
}
Expand Down
76 changes: 53 additions & 23 deletions components/header/navbar/searchpage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React, { useEffect, useRef, useReducer } from 'react'
import Column from '@components/Column'
import { searchDrive, searchSiteContent } from '@lib/strapi/search'
import { titleToAnchor, debounce } from '@utils/helpers'
import {
getDriveDirectories,
searchDrive,
searchSiteContent,
} from '@lib/strapi/search'
import { titleToAnchor, debounce, buildFolderPaths } from '@utils/helpers'
import ListCard from './ListCard'
import SearchBar from './SearchBar'
import { MdCancel } from 'react-icons/md'
Expand All @@ -20,8 +24,52 @@ const SearchOverlay = ({
setSideMenuOpen,
}: SearchOverlayProps) => {
const [state, dispatch] = useReducer(searchReducer, initialState)
const { query, fileSearch, searching, isFetched, fileResults, results } =
state
const {
query,
fileSearch,
searching,
isFetched,
fileResults,
results,
folderPaths,
} = state

useEffect(() => {
// Add class to body to disable interaction with main page
document.body.classList.add('overflow-hidden')
document.body.style.pointerEvents = 'none'

//search folder directories on mount and map folder id to path
const getDirectories = async () => {
try {
const res = await getDriveDirectories()
if (res?.files) {
const folderPaths = buildFolderPaths(res.files)
dispatch({
type: 'SET_FOLDER_PATHS',
payload: folderPaths,
})
}
} catch (error) {
console.error(error)
}
}
getDirectories()

return () => {
document.body.classList.remove('overflow-hidden')
document.body.style.pointerEvents = 'auto'
}
}, [])

const contentReturned =
results?.pageData.length > 0 ||
results?.sectionData.length > 0 ||
results?.privatePageData.length > 0 ||
results?.privateSectionData.length > 0

const filesReturned =
fileResults.files?.length && fileResults.files.length > 0

const clearResults = () => {
dispatch({ type: 'CLEAR_RESULTS' })
Expand Down Expand Up @@ -100,25 +148,6 @@ const SearchOverlay = ({
handleSearch(query, !fileSearch)
}

// Add class to body to disable interaction with main page
useEffect(() => {
document.body.classList.add('overflow-hidden')
document.body.style.pointerEvents = 'none'
return () => {
document.body.classList.remove('overflow-hidden')
document.body.style.pointerEvents = 'auto'
}
}, [])

const contentReturned =
results?.pageData.length > 0 ||
results?.sectionData.length > 0 ||
results?.privatePageData.length > 0 ||
results?.privateSectionData.length > 0

const filesReturned =
fileResults.files?.length && fileResults.files.length > 0

return (
<div className="z-50 fixed inset-0 bg-gray-800 bg-opacity-90 flex items-center justify-center bg-darkgray md:p-10 p-5 pt-12 pointer-events-auto overflow-y-auto">
<Column className="prose w-full h-full">
Expand Down Expand Up @@ -269,6 +298,7 @@ const SearchOverlay = ({
id={file.id}
name={file.name}
thumbnailLink={file.thumbnailLink}
folderPath={folderPaths.get(file.parents?.[0] ?? '')}
/>
)
)}
Expand Down
11 changes: 10 additions & 1 deletion components/header/navbar/searchpage/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type State = {
isFetched: boolean
fileResults: drive_v3.Schema$FileList
results: SearchData
folderPaths: Map<string, string>
}

type Action =
Expand All @@ -19,6 +20,7 @@ type Action =
| { type: 'CLEAR_RESULTS' }
| { type: 'TOGGLE_SEARCH_TYPE' }
| { type: 'INPUT_CHANGED'; payload: string }
| { type: 'SET_FOLDER_PATHS'; payload: Map<string, string> }

export const initialState: State = {
query: '',
Expand All @@ -32,6 +34,7 @@ export const initialState: State = {
privateSectionData: [],
privatePageData: [],
},
folderPaths: new Map(),
}

export const searchReducer = (state: State, action: Action): State => {
Expand All @@ -48,6 +51,8 @@ export const searchReducer = (state: State, action: Action): State => {
return { ...state, results: action.payload }
case 'TOGGLE_SEARCH_TYPE':
return { ...state, fileSearch: !state.fileSearch, isFetched: false }
case 'SET_FOLDER_PATHS':
return { ...state, folderPaths: action.payload }
case 'INPUT_CHANGED':
return {
...state,
Expand All @@ -56,7 +61,11 @@ export const searchReducer = (state: State, action: Action): State => {
searching: action.payload.length >= 2,
}
case 'CLEAR_RESULTS':
return { ...initialState, fileSearch: state.fileSearch }
return {
...initialState,
fileSearch: state.fileSearch,
folderPaths: state.folderPaths,
}
default:
return state
}
Expand Down
34 changes: 32 additions & 2 deletions lib/google/drive.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { drive_v3, google } from 'googleapis'

import { GaxiosResponse } from 'gaxios'
export default class Drive {
private drive: drive_v3.Drive

Expand Down Expand Up @@ -35,7 +35,7 @@ export default class Drive {
async searchFiles(searchParam: string, pageToken?: string, pageSize = 10) {
const searchResults = await this.drive.files.list({
q: `(name contains '${searchParam}' or fullText contains '${searchParam}') and mimeType != 'application/vnd.google-apps.folder' and trashed = false`,
fields: 'nextPageToken, files(id, name, thumbnailLink)',
fields: 'nextPageToken, files(id, name, thumbnailLink, parents)',
supportsAllDrives: true,
includeItemsFromAllDrives: true,
driveId: process.env.SHARED_GOOGLE_DRIVE_ID,
Expand All @@ -45,6 +45,36 @@ export default class Drive {
})
return searchResults.data ?? { files: [] }
}

//search all folders from shared drive
async getAllDirectories(): Promise<{ files: drive_v3.Schema$File[] }> {
const allFiles: drive_v3.Schema$File[] = []
let pageToken: string | undefined = undefined
try {
do {
const response: GaxiosResponse<drive_v3.Schema$FileList> =
await this.drive.files.list({
q: `mimeType = 'application/vnd.google-apps.folder' and trashed = false and name != 'Public' and name != 'Private'`,
fields: 'nextPageToken, files(id, name, parents)',
supportsAllDrives: true,
includeItemsFromAllDrives: true,
driveId: process.env.SHARED_GOOGLE_DRIVE_ID,
corpora: 'drive',
pageSize: 1000,
pageToken: pageToken,
})
const searchResults = response.data

allFiles.push(...(searchResults.files || []))
pageToken = searchResults.nextPageToken || undefined
} while (pageToken)

return { files: allFiles }
} catch (error) {
console.error('Error fetching directories:', error)
throw error
}
}
}

function createDrive(
Expand Down
16 changes: 16 additions & 0 deletions lib/strapi/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ export const searchDrive = async (
}
}

export const getDriveDirectories =
async (): Promise<drive_v3.Schema$FileList | null> => {
try {
const session = await getServerSession(authOptions)
const drive = session && session.user ? privateDrive : publicDrive

if (!drive) {
throw new Error('Drive instance is not available')
}
return drive.getAllDirectories()
} catch (err) {
console.error('Error searching files:', err)
return null
}
}

const fetchPublicData = async (searchParam: string) => {
const query = qs.stringify({
populate: {
Expand Down
61 changes: 61 additions & 0 deletions utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { drive_v3 } from 'googleapis'
import { marked } from 'marked'

export const getDateLong = (date: Date | string): string =>
Expand Down Expand Up @@ -61,3 +62,63 @@ export const debounce = (
}, delay)
}
}

export const buildFolderPaths = (folders: drive_v3.Schema$File[]) => {
// Step 1: Create a map of folderId to folder object for quick lookup
const folderMap = new Map<string, drive_v3.Schema$File>()
for (const folder of folders ?? []) {
folder.id && folderMap.set(folder.id, folder)
}

// Step 2: Initialize a path cache
const pathMap = new Map<string, string>()

// Step 3: Define a recursive function with memoization
function getFullPath(
folderId: string,
visited: Set<string> = new Set()
): string {
// Check if the path is already computed
if (pathMap.has(folderId)) {
return pathMap.get(folderId)!
}

// Detect cycles
if (visited.has(folderId)) {
// Cycle detected, return the folder's name to avoid infinite recursion
const folder = folderMap.get(folderId)
return folder ? '/' + folder.name : ''
}
visited.add(folderId)

// Retrieve the folder
const folder = folderMap.get(folderId)
if (!folder) {
// Folder not found, return empty path or handle as needed
return ''
}

let fullPath: string
if (!folder.parents || folder.parents.length === 0) {
// Root folder
fullPath = '/' + folder.name
} else {
// Recursive call to get parent's full path
const parentPath = getFullPath(folder.parents[0], visited)
fullPath = parentPath + '/' + folder.name
}

// Step 4: Cache the computed full path
pathMap.set(folderId, fullPath)
visited.delete(folderId)
return fullPath
}

// Step 5: Iterate over all folders to compute full paths
for (const folder of folders ?? []) {
folder.id && getFullPath(folder.id)
}

// Step 6: Return the map of folderId to full path string
return pathMap
}

0 comments on commit 9ec2a4f

Please sign in to comment.