Skip to content
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
3 changes: 2 additions & 1 deletion shared/src/components/AttributeEditor/AttributeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const SCOPE_OPTIONS = [
{ value: 'version', label: 'Version' },
{ value: 'representation', label: 'Representation' },
{ value: 'user', label: 'User' },
{ value: 'list', label: 'List' },
]

// Define types for constants
Expand All @@ -37,7 +38,7 @@ const GLOBAL_FIELDS: GlobalFieldEntry[] = [
{ value: 'example', scope: null },
// @ts-expect-error - project is not a scope?
{ value: 'default', scope: ['project'] },
{ value: 'inherit', scope: null },
{ value: 'inherit', scope: ['project', 'folder', 'task', 'product', 'version', 'representation', 'user'] },
]

interface TypeOptionDef {
Expand Down
31 changes: 25 additions & 6 deletions shared/src/components/ListAttributeForm/ListAttributeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EntityListModel,
useGetProjectQuery,
useUpdateEntityListMutation,
useGetAttributeListQuery,
} from '@shared/api'
import { toast } from 'react-toastify'

Expand Down Expand Up @@ -40,6 +41,19 @@ export const ListAttributeForm: FC<ListAttributeFormProps> = ({

const { data: project } = useGetProjectQuery({ projectName })

// Fetch list-scoped attributes
const { data: allAttributes = [] } = useGetAttributeListQuery()
const listScopedAttributes = useMemo(
() =>
allAttributes
.filter((attr) => attr.scope?.includes('list'))
.map((attr) => ({
name: attr.name,
data: attr.data,
})),
[allAttributes],
)

const fields: AttributeField[] = useMemo(
() => [
{ name: 'label', data: { type: 'string', title: 'Label' } },
Expand All @@ -53,18 +67,20 @@ export const ListAttributeForm: FC<ListAttributeFormProps> = ({
enableSearch: true,
},
},
...listScopedAttributes,
{ name: 'active', data: { type: 'boolean', title: 'Active' } },
...attributes,
],
[project?.tags, attributes],
[project?.tags, listScopedAttributes, attributes],
)

useEffect(() => {
if (list) {
const parsedAttrib = list.attrib || {}

// get the data for the attrib fields only
const attribFieldValues = attributes.reduce((acc, { name }) => {
// get the data for all attrib fields (list-scoped and passed attributes)
const allAttribFields = [...listScopedAttributes, ...attributes]
const attribFieldValues = allAttribFields.reduce((acc, { name }) => {
const fieldValue = (parsedAttrib as Record<string, any>)[name]
// Default to empty string for all attrib fields
acc[name] = fieldValue || ''
Expand All @@ -78,12 +94,15 @@ export const ListAttributeForm: FC<ListAttributeFormProps> = ({
...attribFieldValues,
})
}
}, [list, JSON.stringify(attributes)])
}, [list, JSON.stringify(attributes), JSON.stringify(listScopedAttributes)])

const [updateList] = useUpdateEntityListMutation()

const isAttribField = (fieldName: string): boolean => {
return attributes.some((attr) => attr.name === fieldName)
return (
attributes.some((attr) => attr.name === fieldName) ||
listScopedAttributes.some((attr) => attr.name === fieldName)
)
}

const handleChange = useCallback(
Expand Down Expand Up @@ -134,7 +153,7 @@ export const ListAttributeForm: FC<ListAttributeFormProps> = ({
toast.error('Failed to update list attribute: ', error.attrib.detail)
}
},
[list?.id, list?.attrib, projectName, updateList],
[list?.id, list?.attrib, projectName, updateList, listScopedAttributes, attributes],
)

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,96 @@ import { entityTypeOptions } from '../NewListDialog/NewListDialog'
import { createPortal } from 'react-dom'
import styled from 'styled-components'
import { useListsContext } from '@pages/ProjectListsPage/context'
import { AttributeData, AttributeEnumItem, EntityList, useGetAttributeListQuery } from '@shared/api'
import { useProjectDataContext } from '@shared/containers/ProjectTreeTable'
import { getAttributeIcon } from '@shared/util'

// Helper function to aggregate attribute values from lists
const getAttributeValuesFromLists = (
lists: EntityList[],
attributeName: string,
enums?: AttributeEnumItem[],
type?: AttributeData['type'],
): Option[] => {
const enumOptions: Option[] = []
const options: (Option & { count: number })[] = []

// add the enum values first
if (enums) {
enums.forEach((enumItem) => {
enumOptions.push({
id: enumItem.value.toString(),
type: type,
label: enumItem.label,
values: [],
icon: enumItem.icon,
color: enumItem.color,
})
})
}

// aggregate values from all lists
lists.forEach((list) => {
const value = list.attrib?.[attributeName]

// no value? skip
if (value === null || value === undefined) return

let text = ''

// convert value to text
switch (typeof value) {
case 'string':
case 'number':
case 'boolean':
text = value.toString()
break
case 'object':
if (Array.isArray(value)) {
text = value.join(', ')
} else {
text = JSON.stringify(value)
}
break
default:
break
}

// create id for text value
const id = text
.trim()
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '-')

// check if the option already exists in enums
const existingOption = enumOptions?.find((enumItem) => enumItem.id === id)
if (existingOption) return

// check if options already has the value, if so, increment the count
const existingValue = options.find((option) => option.id === id)
if (existingValue) {
existingValue.count++
return
} else {
// add option
options.push({
id,
type: type,
label: text,
values: [],
count: 1,
})
}
})

// sort options based on count
options.sort((a, b) => b.count - a.count)

// enum options first, then the rest
return [...enumOptions, ...options]
}

const Dialog = styled.div`
position: fixed;
inset: 0;
Expand All @@ -33,20 +120,15 @@ const ListsFiltersDialog: FC<ListsFiltersDialogProps> = ({}) => {

const filtersRef = useRef<SearchFilterRef>(null)

// Fetch list-scoped attributes
const { data: allAttributes = [] } = useGetAttributeListQuery()

useEffect(() => {
if (listsFiltersOpen && filtersRef.current) {
filtersRef.current.open()
}
}, [listsFiltersOpen, filtersRef])

// keeps track of the filters whilst adding/removing filters
const [filters, setFilters] = useState<Filter[]>(listsFilters)

// update filters when it changes
useEffect(() => {
setFilters(listsFilters)
}, [listsFilters, setFilters])

const options = useMemo<Option[]>(() => {
const opts: Option[] = [
{
Expand Down Expand Up @@ -94,8 +176,62 @@ const ListsFiltersDialog: FC<ListsFiltersDialogProps> = ({}) => {
})
}

// Add attribute options
const listScopedAttributes = allAttributes.filter((attr) => attr.scope?.includes('list'))

const attributeOptions: Option[] = listScopedAttributes.map((attr) => {
const hasEnum = !!attr.data.enum?.length
const option: Option = {
id: `attrib.${attr.name}`,
label: attr.data.title || attr.name,
type: attr.data.type || 'string',
icon: getAttributeIcon(attr.name, attr.data.type, hasEnum),
allowsCustomValues: true,
values: [],
}

// if the attribute type is boolean, add yes/no options
if (attr.data.type === 'boolean') {
option.singleSelect = true
option.values = [
{
id: 'true',
label: 'Yes',
icon: 'radio_button_checked',
},
{
id: 'false',
label: 'No',
icon: 'radio_button_unchecked',
},
]
} else {
// Get aggregated values from lists data
const aggregatedValues = getAttributeValuesFromLists(
listsData,
attr.name,
attr.data.enum,
attr.data.type,
)

option.values = aggregatedValues
}

return option
})

opts.push(...attributeOptions)

return opts
}, [listsData, projectInfo])
}, [allAttributes, listsData, projectInfo])

// keeps track of the filters whilst adding/removing filters
const [filters, setFilters] = useState<Filter[]>(listsFilters)

// update filters when it changes
useEffect(() => {
setFilters(listsFilters)
}, [listsFilters, setFilters])

// on keydown, close the dialog
useEffect(() => {
Expand All @@ -113,7 +249,7 @@ const ListsFiltersDialog: FC<ListsFiltersDialogProps> = ({}) => {
}
}, [setListsFiltersOpen, listsFiltersOpen])

if (listsFiltersOpen === false) return null
if (!listsFiltersOpen) return null

return createPortal(
<Dialog
Expand Down