Skip to content

Commit

Permalink
Merge pull request #427 from ynput/AY-4592_Filter-products-by-task-in…
Browse files Browse the repository at this point in the history
…-Browser

Browser: Filter products/versions list by taskType
  • Loading branch information
Innders authored May 17, 2024
2 parents 015147c + 6530119 commit dd8bfd2
Show file tree
Hide file tree
Showing 16 changed files with 223 additions and 90 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@rjsf/core": "^4.2.0",
"@rtk-query/graphql-request-base-query": "^2.2.0",
"@uiw/react-textarea-code-editor": "^2.1.9",
"@ynput/ayon-react-components": "^1.0.11",
"@ynput/ayon-react-components": "^1.0.13",
"axios": "^1.6.3",
"chart.js": "^4.2.0",
"date-fns": "^3.6.0",
Expand Down
32 changes: 32 additions & 0 deletions src/components/CategorySelect/CategorySelect.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { DefaultValueTemplate, Dropdown, Icon } from '@ynput/ayon-react-components'

const IconsTemplate = ({ value, selected, isOpen, options, ...props }) => {
return (
<DefaultValueTemplate
{...props}
{...{ value, selected, isOpen }}
valueStyle={{ display: 'flex' }}
>
{options
?.filter((option) => selected.includes(option.value))
.map((option) => (
<Icon icon={option.icon} key={option.value} />
))}
</DefaultValueTemplate>
)
}

const CategorySelect = ({ truncateAt = 3, ...props }) => {
return (
<Dropdown
{...props}
valueTemplate={(value, selected, isOpen) =>
selected.length >= truncateAt ? (
<IconsTemplate {...props} {...{ value, selected, isOpen }} />
) : null
}
/>
)
}

export default CategorySelect
Empty file.
37 changes: 27 additions & 10 deletions src/containers/taskList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Column } from 'primereact/column'

import EntityDetail from './DetailsDialog'
import { CellWithIcon } from '/src/components/icons'
import { setFocusedTasks, setPairing, setUri } from '/src/features/context'
import { setFocusedTasks, setPairing, setUri, updateBrowserFilters } from '/src/features/context'
import { toast } from 'react-toastify'
import { useGetTasksQuery } from '/src/services/getTasks'
import useCreateContext from '../hooks/useCreateContext'
Expand Down Expand Up @@ -94,6 +94,32 @@ const TaskList = ({ style = {}, autoSelect = false }) => {
dispatch(setFocusedTasks({ ids: [taskId] }))
}

const handleFilterProductsBySelected = (selected = []) => {
// get taskTypes based on selected tasks
const taskTypes = selected.map(
(taskId) => tasksData.find((task) => task.data.id === taskId)?.data?.taskType,
)

// filter out duplicates
const uniqueTaskTypes = [...new Set(taskTypes)]

dispatch(updateBrowserFilters({ productTaskTypes: uniqueTaskTypes }))
}

// CONTEXT MENU
const ctxMenuItems = (selected = []) => [
{
label: `Filter products by task${selected.length > 1 ? 's' : ''}`,
icon: 'filter_list',
command: () => handleFilterProductsBySelected(selected),
},
{
label: 'Detail',
command: () => setShowDetail(true),
icon: 'database',
},
]

const onContextMenu = (event) => {
let newFocused = [...focusedTasks]
const itemId = event.node.data.id
Expand All @@ -107,15 +133,6 @@ const TaskList = ({ style = {}, autoSelect = false }) => {
ctxMenuShow(event.originalEvent, ctxMenuItems(newFocused))
}

// CONTEXT MENU
const ctxMenuItems = () => [
{
label: 'Detail',
command: () => setShowDetail(true),
icon: 'database',
},
]

const [ctxMenuShow] = useCreateContext([])

// create 10 dummy rows
Expand Down
17 changes: 15 additions & 2 deletions src/features/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ const initialState = {
expandedFolders: {},
expandedProducts: {},
expandedRepresentations: {},
filters: {
browser: {
productTaskTypes: [],
},
},
focused: {
type: null,
folders: [],
Expand Down Expand Up @@ -52,6 +57,7 @@ const localStorageKeys = [
'focused.editor',
'selectedVersions',
'uri',
'filters.browser.productTaskTypes',
]

const initialStateWithLocalStorage = cloneDeep(initialState)
Expand Down Expand Up @@ -278,6 +284,11 @@ const reducers = {
payload: 'type',
},
},
updateBrowserFilters: {
'filters.browser.productTaskTypes': {
payload: 'productTaskTypes',
},
},
}

// we use this function to update the state with the reducer values
Expand Down Expand Up @@ -373,6 +384,9 @@ const contextSlice = createSlice({
onUriNavigate: (state, action) => {
updateStateWithReducer(reducers.onUriNavigate, state, action)
},
updateBrowserFilters: (state, action) => {
updateStateWithReducer(reducers.updateBrowserFilters, state, action)
},
onFocusChanged: (state, action) => {
state.focused.lastFocused = action.payload
},
Expand Down Expand Up @@ -462,6 +476,7 @@ export const {
setMenuOpen,
toggleMenuOpen,
onUriNavigate,
updateBrowserFilters,
onCommentImageOpen,
onFilePreviewClose,
} = contextSlice.actions
Expand Down Expand Up @@ -508,6 +523,4 @@ Object.entries(reducers).forEach(([reducerKey, reducerStates]) => {
Object.assign(contextLocalItems, middleware)
})

console.log(contextLocalItems)

export { contextLocalItems }
142 changes: 78 additions & 64 deletions src/hooks/useSearchFilter.js
Original file line number Diff line number Diff line change
@@ -1,79 +1,93 @@
import { useMemo, useState } from 'react'
import { StringParam, useQueryParam, withDefault } from 'use-query-params'

export const filterByFieldsAndValues = ({
filters = [],
data = [],
fields = [],
matchesAll = false,
}) => {
// create keywords that are used for searching
const dataWithKeywords = data.map((item) => ({
...item,
keywords: Object.entries(item).flatMap(([k, v]) => {
if (fields.includes(k)) {
if (typeof v === 'string') {
return v?.toLowerCase()
} else if (Array.isArray(v)) {
return v?.flatMap((v) => v.toLowerCase())
} else if (typeof v === 'boolean' && v) {
return k.toLowerCase()
} else return []
} else if (v && typeof v === 'object') {
return Object.entries(v).flatMap(([k2, v2]) => {
return fields.includes(`${k}.${k2}`) && v2 ? v2.toString().toLowerCase() : []
})
} else return []
}),
}))

if (filters?.length && dataWithKeywords) {
return dataWithKeywords.filter((item) => {
const matchingKeys = []
const inverseMatchingKeys = []
item.keywords?.forEach((key) => {
filters.forEach((filter) => {
let lowerFilter = filter.toLowerCase()
// if lowerFilter has a ! at the start do opposite
if (lowerFilter[0] === '!') {
lowerFilter = lowerFilter.slice(1)
// if key includes the lowerFilter without the ! it's not a match
if (
key.includes(lowerFilter) &&
!inverseMatchingKeys.includes(lowerFilter) &&
lowerFilter.length > 2
) {
inverseMatchingKeys.push(lowerFilter)
} else if (!matchingKeys.includes(lowerFilter)) {
matchingKeys.push(lowerFilter)
}
} else {
if (key.includes(lowerFilter) && !matchingKeys.includes(lowerFilter))
matchingKeys.push(lowerFilter)
}
})
})

// if there are any inverse matches return false
if (inverseMatchingKeys.length) return false

if (matchesAll) return matchingKeys.length >= filters.length
else return matchingKeys.length > 0
})
} else return data
}

const useSearchFilter = (fields = [], data = [], id) => {
let key = 'search'
id && (key += '-' + id)
const [search, setSearch] = useQueryParam(key, withDefault(StringParam, ''))
const [searchLocal, setSearchLocal] = useState('')
const searchString = id ? search : searchLocal

// create keywords that are used for searching
const dataWithKeywords = useMemo(
() =>
data.map((user) => ({
...user,
keywords: Object.entries(user).flatMap(([k, v]) => {
if (fields.includes(k)) {
if (typeof v === 'string') {
return v?.toLowerCase()
} else if (Array.isArray(v)) {
return v?.flatMap((v) => v.toLowerCase())
} else if (typeof v === 'boolean' && v) {
return k.toLowerCase()
} else return []
} else if (v && typeof v === 'object') {
return Object.entries(v).flatMap(([k2, v2]) =>
fields.includes(`${k}.${k2}`) && v2 ? v2?.toString().toLowerCase() : [],
)
} else return []
}),
})),
[data],
)

let filteredData = useMemo(() => {
// separate into array by ,
const searchArray = searchString?.split(',').reduce((acc, cur) => {
if (cur.trim() === '') return acc
else {
acc.push(cur.trim().toLowerCase())
return acc
}
}, [])

if (searchArray?.length && dataWithKeywords) {
return dataWithKeywords.filter((user) => {
const matchingKeys = []
const inverseMatchingKeys = []
user.keywords?.forEach((key) => {
searchArray.forEach((split) => {
// if split has a ! at the start do opposite
if (split[0] === '!') {
split = split.slice(1)
// if key includes the split without the ! it's not a match
if (key.includes(split) && !inverseMatchingKeys.includes(split) && split.length > 2) {
inverseMatchingKeys.push(split)
} else if (!matchingKeys.includes(split)) {
matchingKeys.push(split)
}
} else {
if (key.includes(split) && !matchingKeys.includes(split)) matchingKeys.push(split)
}
})
})

// if there are any inverse matches return false
if (inverseMatchingKeys.length) return false
if (!fields.length) {
fields = data
}

return matchingKeys.length >= searchArray.length
})
} else return null
}, [dataWithKeywords, searchString, data])
// separate searchString into array by ,
// end up with array ['search', 'search2']
const searchArray = searchString?.split(',').reduce((acc, cur) => {
if (cur.trim() === '') return acc
else {
acc.push(cur.trim().toLowerCase())
return acc
}
}, [])

if (!filteredData) {
filteredData = data
}
let filteredData = useMemo(
() => filterByFieldsAndValues({ filters: searchArray, data, fields, matchesAll: true }),
[searchString, data],
)

const onSearchChange = (v) => (id ? setSearch(v) : setSearchLocal(v))

Expand Down
2 changes: 1 addition & 1 deletion src/pages/BrowserPage/BrowserPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Section } from '@ynput/ayon-react-components'
import Hierarchy from '/src/containers/hierarchy'
import TaskList from '/src/containers/taskList'

import Products from './Products'
import Products from './Products/Products'
import BrowserDetailsPanel from './BrowserDetailsPanel'

const detailsMinWidth = 533
Expand Down
File renamed without changes.
Loading

0 comments on commit dd8bfd2

Please sign in to comment.