Skip to content

Commit

Permalink
fix(search): Improve speed of search suggestions
Browse files Browse the repository at this point in the history
By preventing every notes fetching url, loading only once a note is clicked
  • Loading branch information
trollepierre committed Aug 29, 2022
1 parent 0121212 commit 282c108
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 39 deletions.
11 changes: 9 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"no-console": 1,
"no-param-reassign": "error",
"react-hooks/exhaustive-deps": "error"

},
"globals": {
"fixture": false
Expand All @@ -13,5 +12,13 @@
"react": {
"version": "detect"
}
}
},
"overrides": [
{
"files": ["*.spec.js[x]"],
"rules": {
"react/display-name": "off"
}
}
]
}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## ✨ Features

* Upgrade [email protected] to be able to call onSelect function
* Improve speed of search suggestion by preventing fetch notes url until click
* Update cozy-stack-client and cozy-pouch-link to sync with cozy-client version
* Update cozy-ui
- Modify Viewers to handle [68.0.0 BC](https://github.com/cozy/cozy-ui/releases/tag/v68.0.0)
Expand Down
12 changes: 4 additions & 8 deletions src/drive/web/modules/services/components/SuggestionProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import FuzzyPathSearch from '../FuzzyPathSearch'
import { withClient } from 'cozy-client'

import { TYPE_DIRECTORY, makeNormalizedFile } from './helpers'
import { getIconUrl } from './iconContext'

class SuggestionProvider extends React.Component {
componentDidMount() {
Expand Down Expand Up @@ -38,7 +37,7 @@ class SuggestionProvider extends React.Component {
title: result.name,
subtitle: result.path,
term: result.name,
onSelect: 'open:' + result.url,
onSelect: result.onSelect || 'open:' + result.url,
icon: result.icon
}))
},
Expand All @@ -54,7 +53,7 @@ class SuggestionProvider extends React.Component {
return new Promise(async resolve => {
const resp = await cozy.client.fetchJSON(
'GET',
`/data/io.cozy.files/_all_docs?include_docs=true`
'/data/io.cozy.files/_all_docs?include_docs=true'
)
const files = resp.rows
// TODO: fix me
Expand All @@ -73,11 +72,8 @@ class SuggestionProvider extends React.Component {
.filter(notInTrash)
.filter(notOrphans)

const normalizedFiles = await Promise.all(
normalizedFilesPrevious.map(
async file =>
await makeNormalizedFile(client, folders, file, getIconUrl)
)
const normalizedFiles = normalizedFilesPrevious.map(file =>
makeNormalizedFile(client, folders, file)
)

this.fuzzyPathSearch = new FuzzyPathSearch(normalizedFiles)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react'
import { render, waitFor } from '@testing-library/react'
import SuggestionProvider from './SuggestionProvider'
import { dummyFile, dummyNote } from 'test/dummies/dummyFile'

const makeFileWithDoc = file => ({ ...file, doc: file })
const parentFolder = makeFileWithDoc(dummyFile({ _id: 'id-file' }))
const folder = makeFileWithDoc(dummyFile({ dir_id: 'id-file' }))
const note = makeFileWithDoc(
dummyNote({
dir_id: 'id-file',
name: 'name.cozy-note'
})
)
const mockClient = {
fetchJSON: jest.fn().mockReturnValue({ rows: [parentFolder, folder, note] })
}
const mockIntentAttributesClient = 'intent-attributes-client'

jest.mock('cozy-client', () => ({
...jest.requireActual('cozy-client'),
withClient: Component => () => {
const intent = {
_id: 'id_intent',
attributes: { client: mockIntentAttributesClient }
}
return <Component client={mockClient} intent={intent}></Component>
}
}))
jest.mock('./iconContext', () => ({ getIconUrl: () => 'iconUrl' }))

describe('SuggestionProvider', () => {
let events = {}
let event

beforeEach(() => {
window.cozy.client = mockClient
window.addEventListener = jest.fn((event, callback) => {
events[event] = callback
})
window.parent.postMessage = jest.fn()
event = {
origin: mockIntentAttributesClient,
data: { query: 'name', id: 'id' }
}
})

it('should query all files to display fuzzy suggestion', () => {
// Given
render(<SuggestionProvider />)

// When
events.message(event)

// Then
expect(mockClient.fetchJSON).toHaveBeenCalledWith(
'GET',
'/data/io.cozy.files/_all_docs?include_docs=true'
)
})

it('should provide onSelect with open url when file is not a note + and function when it is a note', async () => {
// Given
render(<SuggestionProvider />)

// When
events.message(event)

// Then
await waitFor(() => {
expect(window.parent.postMessage).toHaveBeenCalledWith(
{
id: 'id',
suggestions: [
{
icon: 'iconUrl',
id: 'id-file',
onSelect: 'open:http://localhost/#/folder/id-file',
subtitle: '/path',
term: 'name',
title: 'name'
},
{
icon: 'iconUrl',
id: 'id-file',
onSelect: 'id_note:id-file',
subtitle: '/path',
term: 'name.cozy-note',
title: 'name.cozy-note'
}
],
type: 'intent-id_intent:data'
},
'intent-attributes-client'
)
})
})
})
20 changes: 17 additions & 3 deletions src/drive/web/modules/services/components/helpers.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
import { models } from 'cozy-client'
import { getIconUrl } from './iconContext'

export const TYPE_DIRECTORY = 'directory'

export const makeNormalizedFile = async (client, folders, file, getIconUrl) => {
/**
* Normalize file for Front usage in <AutoSuggestion> component inside <SearchBar>
*
* To reduce API call, the fetching of Note URL has been delayed
* inside an onSelect function called only if provided to <SearchBar>
* see https://github.com/cozy/cozy-drive/pull/2663#discussion_r938671963
*
* @param {CozyClient} client - cozy client instance
* @param {[IOCozyFile]} folders - all the folders returned by API
* @param {IOCozyFile} file - file to normalize
* @returns file with normalized field to be used in AutoSuggestion
*/
export const makeNormalizedFile = (client, folders, file) => {
const isDir = file.type === TYPE_DIRECTORY
const dirId = isDir ? file._id : file.dir_id
const urlToFolder = `${window.location.origin}/#/folder/${dirId}`

let path, url
let path, url, onSelect
if (isDir) {
path = file.path
url = urlToFolder
} else {
const parentDir = folders.find(folder => folder._id === file.dir_id)
path = parentDir && parentDir.path ? parentDir.path : ''
if (models.file.isNote(file)) {
url = await models.note.fetchURL(client, file)
onSelect = `id_note:${file.id}`
} else {
url = `${urlToFolder}/file/${file._id}`
}
Expand All @@ -26,6 +39,7 @@ export const makeNormalizedFile = async (client, folders, file, getIconUrl) => {
name: file.name,
path,
url,
onSelect,
icon: getIconUrl(file)
}
}
62 changes: 36 additions & 26 deletions src/drive/web/modules/services/components/helpers.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createMockClient, models } from 'cozy-client'

import { makeNormalizedFile, TYPE_DIRECTORY } from './helpers'
import { getIconUrl } from './iconContext'

jest.mock('./iconContext', () => ({ getIconUrl: () => 'iconUrl' }))
models.note.fetchURL = jest.fn(() => 'noteUrl')
Expand All @@ -19,7 +18,7 @@ const noteFileProps = {
}

describe('makeNormalizedFile', () => {
it('should return correct values for a directory', async () => {
it('should return correct values for a directory', () => {
const folders = []
const file = {
_id: 'fileId',
Expand All @@ -28,22 +27,18 @@ describe('makeNormalizedFile', () => {
name: 'fileName'
}

const normalizedFile = await makeNormalizedFile(
client,
folders,
file,
getIconUrl
)
const normalizedFile = makeNormalizedFile(client, folders, file)

expect(normalizedFile).toMatchObject({
expect(normalizedFile).toEqual({
icon: 'iconUrl',
id: 'fileId',
name: 'fileName',
path: 'filePath',
url: 'http://localhost/#/folder/fileId'
})
})

it('should return correct values for a file', async () => {
it('should return correct values for a file', () => {
const folders = [{ _id: 'folderId', path: 'folderPath' }]
const file = {
_id: 'fileId',
Expand All @@ -52,43 +47,58 @@ describe('makeNormalizedFile', () => {
name: 'fileName'
}

const normalizedFile = await makeNormalizedFile(
client,
folders,
file,
getIconUrl
)
const normalizedFile = makeNormalizedFile(client, folders, file)

expect(normalizedFile).toMatchObject({
expect(normalizedFile).toEqual({
icon: 'iconUrl',
id: 'fileId',
name: 'fileName',
path: 'folderPath',
url: 'http://localhost/#/folder/folderId/file/fileId'
})
})

it('should return correct values for a note', async () => {
it('should return correct values for a note with on Select function - better for performance', () => {
const folders = [{ _id: 'folderId', path: 'folderPath' }]
const file = {
_id: 'fileId',
id: 'noteId',
dir_id: 'folderId',
type: 'file',
name: 'fileName',
...noteFileProps
}

const normalizedFile = await makeNormalizedFile(
client,
folders,
file,
getIconUrl
)
const normalizedFile = makeNormalizedFile(client, folders, file)

expect(normalizedFile).toMatchObject({
expect(normalizedFile).toEqual({
icon: 'iconUrl',
id: 'fileId',
name: 'note.cozy-note',
path: 'folderPath',
url: 'noteUrl'
onSelect: 'id_note:noteId'
})
})

it('should not return filled onSelect for a note without metadata', () => {
const folders = [{ _id: 'folderId', path: 'folderPath' }]
const file = {
_id: 'fileId',
id: 'noteId',
dir_id: 'folderId',
type: 'file',
name: 'note.cozy-note'
}

const normalizedFile = makeNormalizedFile(client, folders, file)

expect(normalizedFile).toEqual({
icon: 'iconUrl',
id: 'fileId',
name: 'note.cozy-note',
path: 'folderPath',
onSelect: undefined,
url: 'http://localhost/#/folder/folderId/file/fileId'
})
})
})
37 changes: 37 additions & 0 deletions test/dummies/dummyFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// eslint-disable-next-line no-unused-vars
const { IOCozyFile } = require('cozy-client/dist/types')

/**
* Create a dummy file, with overridden value of given param
*
* @param {Partial<IOCozyFile>} [file={}] - optional file with value to keep
* @returns {IOCozyFile} a dummy file
*/
export const dummyFile = file => ({
_id: 'id-file',
_type: 'doctype-file',
name: 'name',
id: 'id-file',
icon: 'icon',
path: '/path',
type: 'directory',
...file
})

/**
* Create a dummy note, with overridden value of given param
*
* @param {Partial<IOCozyFile>} [note={}]
* @returns {IOCozyFile} a dummy note
*/
export const dummyNote = note => ({
...dummyFile(),
type: 'file',
metadata: {
content: '',
schema: '',
title: '',
version: ''
},
...note
})

0 comments on commit 282c108

Please sign in to comment.