Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement search feature through Dataproxy #2198

Merged
merged 12 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
{
"relativeDepth": 1,
"aliases": [
{ "alias": "dataproxy", "matcher": "^dataproxy" },
{ "alias": "components", "matcher": "^components" },
{ "alias": "config", "matcher": "^config" },
{ "alias": "containers", "matcher": "^containers" },
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"homepage": "https://github.com/cozy/cozy-home#readme",
"dependencies": {
"@sentry/react": "7.119.0",
"comlink": "4.4.1",
"cozy-client": "^48.25.0",
"cozy-device-helper": "2.7.0",
"cozy-doctypes": "1.83.8",
Expand All @@ -56,6 +57,7 @@
"leaflet": "1.7.1",
"localforage": "^1.10.0",
"lodash": "4.17.21",
"mime-types": "2.1.35",
"moment": "2.29.4",
"papaparse": "5.3.1",
"prop-types": "^15.8.1",
Expand Down
17 changes: 11 additions & 6 deletions src/assistant/ResultMenu/ResultMenuContent.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react'

import flag from 'cozy-flags'
import List from 'cozy-ui/transpiled/react/List'
import Circle from 'cozy-ui/transpiled/react/Circle'
import PaperplaneIcon from 'cozy-ui/transpiled/react/Icons/Paperplane'
Expand All @@ -9,13 +8,15 @@ import ListItemSkeleton from 'cozy-ui/transpiled/react/Skeletons/ListItemSkeleto
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'
import { useBreakpoints } from 'cozy-ui/transpiled/react/providers/Breakpoints'

import { useDataProxy } from 'dataproxy/DataProxyProvider'

import { useSearch } from '../Search/SearchProvider'
import ResultMenuItem from './ResultMenuItem'

const SearchResult = () => {
const { isLoading, results } = useSearch()
const { isLoading, results, searchValue } = useSearch()

if (isLoading)
if (isLoading && !results?.length)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c'est dû au fait que le result peut être [] même pendant un loading ? Est-ce qu'on pourrait plutôt avoir plus simplement un tableau, ou null, si on est !loading ou loading ? C'est basé sur client.query donc on hérite du comportement de cozy-client ?

return (
<>
<ListItemSkeleton hasSecondary />
Expand All @@ -26,10 +27,12 @@ const SearchResult = () => {

return results.map((result, idx) => (
<ResultMenuItem
key={idx}
icon={<Icon icon={result.icon} size={32} />}
key={result.id || idx}
icon={result.icon}
primaryText={result.primary}
secondaryText={result.secondary}
query={searchValue}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

j'aurai gardé searchValue en nom de prop 🤔 query ça fait penser au queryDef de cozy-client

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a eu exactement cette discussion avec @paultranvan y a quelques jours qui a opté pour query car c'est le terme généralement utilisé. Mais j'ai pas d'objection forte pour changer le nom.

highlightQuery="true"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attention on devrait probablement avoir ={true} ici non ?

onClick={result.onClick}
/>
))
Expand All @@ -39,6 +42,7 @@ const ResultMenuContent = ({ onClick }) => {
const { t } = useI18n()
const { isMobile } = useBreakpoints()
const { searchValue } = useSearch()
const { dataProxyServicesAvailable } = useDataProxy()

return (
<List>
Expand All @@ -49,10 +53,11 @@ const ResultMenuContent = ({ onClick }) => {
</Circle>
}
primaryText={searchValue}
query={searchValue}
secondaryText={t('assistant.search.result')}
onClick={onClick}
/>
{flag('cozy.assistant.withSearchResult') && <SearchResult />}
{dataProxyServicesAvailable && <SearchResult />}
</List>
)
}
Expand Down
37 changes: 34 additions & 3 deletions src/assistant/ResultMenu/ResultMenuItem.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
import React from 'react'

import AppIcon from 'cozy-ui/transpiled/react/AppIcon'
import Icon from 'cozy-ui/transpiled/react/Icon'
import ListItem from 'cozy-ui/transpiled/react/ListItem'
import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'
import ListItemText from 'cozy-ui/transpiled/react/ListItemText'
import SuggestionItemTextHighlighted from './SuggestionItemTextHighlighted'

const ResultMenuItem = ({
icon,
primaryText,
secondaryText,
onClick,
query,
highlightQuery = false
}) => {
const iconComponent =
icon.type === 'component' ? (
<Icon icon={icon.component} size={32} />
) : icon.type === 'app' ? (
<AppIcon app={icon.app} />
) : (
icon
)

const primary = highlightQuery ? (
<SuggestionItemTextHighlighted text={primaryText} query={query} />
) : (
primaryText
)

const secondary = highlightQuery ? (
<SuggestionItemTextHighlighted text={secondaryText} query={query} />
) : (
secondaryText
)

const ResultMenuItem = ({ icon, primaryText, secondaryText, onClick }) => {
return (
<ListItem button size="small" onClick={onClick}>
<ListItemIcon>{icon}</ListItemIcon>
<ListItemText primary={primaryText} secondary={secondaryText} />
<ListItemIcon>{iconComponent}</ListItemIcon>
<ListItemText primary={primary} secondary={secondary} />
</ListItem>
)
}
Expand Down
119 changes: 119 additions & 0 deletions src/assistant/ResultMenu/SuggestionItemTextHighlighted.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* Code copied and adapted from cozy-drive
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

est-ce la manière la plus optimisé et moderne de le faire ? De quand date le code sur cozy-drive ? Est-ce qu'il n'y a pas des outils côté Mui pour faire ça plus simplement ?

Par ailleurs, comme on utilise la même chose à deux endroits, on devrait peut-être faire un compose cozy-ui

enfin, pour verrouiller l'approche, on pourrait peut-être rajouter des tests...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Par ailleurs, comme on utilise la même chose à deux endroits, on devrait peut-être faire un compose cozy-ui

Je suis allé au plus simple car j'imagine qu'on va bientôt remplacer la recherche de drive par la celle de dataproxy.

Ok pour les tests, j'ajouterai ça dans les prochaines PR

*
* See source: https://github.com/cozy/cozy-drive/blob/fbe2df67199683b23a40f476ccdacb00ee027459/src/modules/search/components/SuggestionItemTextHighlighted.jsx
*/

import React from 'react'

const normalizeString = str =>
str
.toString()
.toLowerCase()
.replace(/\//g, ' ')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.split(' ')

/**
* Add <b> on part that equlas query into each result
*
* @param {Array} searchResult - list of results
* @param {string} query - search input
* @returns list of results with the query highlighted
*/
const highlightQueryTerms = (searchResult, query) => {
const normalizedQueryTerms = normalizeString(query)
const normalizedResultTerms = normalizeString(searchResult)

const matchedIntervals = []
const spacerLength = 1
let currentIndex = 0

normalizedResultTerms.forEach(resultTerm => {
normalizedQueryTerms.forEach(queryTerm => {
const index = resultTerm.indexOf(queryTerm)
if (index >= 0) {
matchedIntervals.push({
from: currentIndex + index,
to: currentIndex + index + queryTerm.length
})
}
})

currentIndex += resultTerm.length + spacerLength
})

// matchedIntervals can overlap, so we merge them.
// - sort the intervals by starting index
// - add the first interval to the stack
// - for every interval,
// - - add it to the stack if it doesn't overlap with the stack top
// - - or extend the stack top if the start overlaps and the new interval's top is bigger
const mergedIntervals = matchedIntervals
.sort((intervalA, intervalB) => intervalA.from > intervalB.from)
.reduce((computedIntervals, newInterval) => {
if (
computedIntervals.length === 0 ||
computedIntervals[computedIntervals.length - 1].to < newInterval.from
) {
computedIntervals.push(newInterval)
} else if (
computedIntervals[computedIntervals.length - 1].to < newInterval.to
) {
computedIntervals[computedIntervals.length - 1].to = newInterval.to
}

return computedIntervals
}, [])

// create an array containing the entire search result, with special characters, and the intervals surrounded y `<b>` tags
const slicedOriginalResult =
mergedIntervals.length > 0
? [<span key="0">{searchResult.slice(0, mergedIntervals[0].from)}</span>]
: searchResult

for (let i = 0, l = mergedIntervals.length; i < l; ++i) {
slicedOriginalResult.push(
<span className="u-primaryColor">
{searchResult.slice(mergedIntervals[i].from, mergedIntervals[i].to)}
</span>
)
if (i + 1 < l)
slicedOriginalResult.push(
<span>
{searchResult.slice(
mergedIntervals[i].to,
mergedIntervals[i + 1].from
)}
</span>
)
}

if (mergedIntervals.length > 0)
slicedOriginalResult.push(
<span>
{searchResult.slice(
mergedIntervals[mergedIntervals.length - 1].to,
searchResult.length
)}
</span>
)

return slicedOriginalResult
}

const SuggestionItemTextHighlighted = ({ text, query }) => {
if (!text) return null

const textHighlighted = highlightQueryTerms(text, query)
if (Array.isArray(textHighlighted)) {
return textHighlighted.map((item, idx) => ({
...item,
key: idx
}))
}
return textHighlighted
}

export default SuggestionItemTextHighlighted
28 changes: 28 additions & 0 deletions src/assistant/Search/EncryptedFolderIcon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'

const EncryptedFolderIcon = props => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on devrait passer SVGO dessus et potentiellement avoir ça dans cozy-ui non ?

return (
<svg
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
opacity="0.34"
fillRule="evenodd"
clipRule="evenodd"
d="M12.9657 1C13.5206 1 14.2876 1.3125 14.6803 1.6995L16 3H30.0059C31.1072 3 32 3.89498 32 4.997V27.003C32 28.1059 31.1107 29 29.9983 29H2.00174C0.896211 29 0 28.1001 0 27.0088V1.99653C0 1.44616 0.448999 1 1.00472 1H12.9657Z"
fill="#297EF2"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 1C0.447715 1 0 1.44772 0 2V6C0 6.55228 0.447716 7 1 7H13.5858C13.851 7 14.1054 6.89464 14.2929 6.70711L15.7071 5.29289C15.8946 5.10536 16.149 5 16.4142 5H32C32 3.89543 31.1046 3 30 3H16.4142C16.149 3 15.8946 2.89464 15.7071 2.70711L14.2929 1.29289C14.1054 1.10536 13.851 1 13.5858 1H1ZM10 15.9954V24.0046C10 24.5543 10.4558 25 11.0025 25H20.9975C21.5512 25 22 24.5443 22 24.0046V15.9954C22 15.4457 21.5561 15 21 15H20V13C20 10.794 18.2053 9 16 9C13.794 9 12 10.794 12 13V15H11C10.4477 15 10 15.4557 10 15.9954ZM16 11C14.8968 11 14 12.1215 14 13.5V15H18V13.5C18 12.1215 17.1028 11 16 11ZM17.5 19C17.5 18.172 16.8265 17.5 16 17.5C15.172 17.5 14.5 18.172 14.5 19C14.5 19.552 14.803 20.032 15.25 20.29V22.75C15.25 23.1625 15.586 23.5 16 23.5C16.4125 23.5 16.75 23.1625 16.75 22.75V20.29C17.1955 20.032 17.5 19.552 17.5 19Z"
fill="#297EF2"
/>
</svg>
)
}

export default EncryptedFolderIcon
37 changes: 37 additions & 0 deletions src/assistant/Search/getFileMimetype.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on peut peut-être rajouter un test ou deux sur ça ?

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import mime from 'mime-types'

const getMimetypeFromFilename = name => {
return mime.lookup(name) || 'application/octet-stream'
}

const mappingMimetypeSubtype = {
word: 'text',
text: 'text',
zip: 'zip',
pdf: 'pdf',
spreadsheet: 'sheet',
excel: 'sheet',
sheet: 'sheet',
presentation: 'slide',
powerpoint: 'slide'
}

export const getFileMimetype =
collection =>
(mime = '', name = '') => {
const mimetype =
mime === 'application/octet-stream'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on peut peut-être mettre application/octet-stream dans un CONST vu qu'on s'en sert plusieurs fois.

A propos des Mime, on n'a pas des helpers cozy-client (model ?) qui peuvent servir ?

? getMimetypeFromFilename(name.toLowerCase())
: mime
const [type, subtype] = mimetype.split('/')
if (collection[type]) {
return type
}
if (type === 'application') {
const existingType = subtype.match(
Object.keys(mappingMimetypeSubtype).join('|')
)
return existingType ? mappingMimetypeSubtype[existingType[0]] : undefined
}
return undefined
}
Loading
Loading