Skip to content

Commit 8046a83

Browse files
committed
picker fixes
1 parent fa8f4e2 commit 8046a83

File tree

2 files changed

+141
-62
lines changed

2 files changed

+141
-62
lines changed
Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
import { PathHelper } from '@sensenet/client-utils'
2-
import React, { useEffect, useState } from 'react'
2+
import React, { memo, useEffect, useMemo, useState } from 'react'
33
import { IconOptions } from './Icon'
44

5+
// Global cache for icons
6+
const iconCache = new Map<string, string>()
7+
58
const IconFromPath = ({ path, options }: { path: string; options: IconOptions }) => {
6-
const [icon, setIcon] = useState<string | null>(null)
9+
const [icon, setIcon] = useState<string | null>(iconCache.get(path) || null)
710

811
useEffect(() => {
912
const controller = new AbortController()
1013
const { signal } = controller
1114

1215
const fetchIcon = async () => {
16+
// Check cache first
17+
if (iconCache.has(path)) {
18+
setIcon(iconCache.get(path)!)
19+
return
20+
}
21+
1322
const imageUrl = PathHelper.joinPaths(options.repo.configuration.repositoryUrl, path)
1423

1524
if (path.endsWith('.svg')) {
@@ -21,6 +30,7 @@ const IconFromPath = ({ path, options }: { path: string; options: IconOptions })
2130
.replace('width=', 'width="24px" oldwidth=')
2231
.replace('height=', 'height="24px" oldheight=')
2332
if (!signal.aborted) {
33+
iconCache.set(path, resizedSvg) // Store in cache
2434
setIcon(resizedSvg)
2535
}
2636
} catch (err) {
@@ -30,25 +40,42 @@ const IconFromPath = ({ path, options }: { path: string; options: IconOptions })
3040
}
3141
} else {
3242
if (!signal.aborted) {
43+
iconCache.set(path, imageUrl) // Store in cache
3344
setIcon(imageUrl)
3445
}
3546
}
3647
}
3748

38-
fetchIcon()
49+
if (!icon) {
50+
fetchIcon()
51+
}
3952

4053
return () => {
4154
controller.abort()
4255
}
43-
}, [options.repo, path])
56+
}, [path, options.repo, icon])
4457

45-
if (!icon) return null
58+
// Memoize the rendered output to prevent unnecessary DOM updates
59+
const renderedIcon = useMemo(() => {
60+
if (!icon) return null
4661

47-
return path.endsWith('.svg') ? (
48-
<span dangerouslySetInnerHTML={{ __html: icon }} style={options.style} aria-hidden="true" />
49-
) : (
50-
<img src={icon} alt="icon" style={options.style} />
51-
)
62+
return path.endsWith('.svg') ? (
63+
<span dangerouslySetInnerHTML={{ __html: icon }} style={options.style} aria-hidden="true" />
64+
) : (
65+
<img src={icon} alt="icon" style={options.style} />
66+
)
67+
}, [icon, path, options.style])
68+
69+
return renderedIcon
5270
}
5371

72+
// Memoize the component to prevent re-renders if props are unchanged
73+
export const MemoizedIconFromPath = memo(IconFromPath, (prevProps, nextProps) => {
74+
return (
75+
prevProps.path === nextProps.path &&
76+
prevProps.options.repo === nextProps.options.repo &&
77+
prevProps.options.style === nextProps.options.style
78+
)
79+
})
80+
5481
export { IconFromPath }

packages/sn-pickers-react/src/components/picker-advanced.tsx

Lines changed: 104 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { GenericContent, User } from '@sensenet/default-content-types'
99
import { Query, QueryExpression, QueryOperators } from '@sensenet/query'
1010
import { ColDef } from 'ag-grid-community'
1111
import { AgGridReact } from 'ag-grid-react'
12-
import React, { useCallback, useEffect, useRef, useState } from 'react'
12+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
1313
import { GenericContentWithIsParent } from '../types'
1414

1515
// Icons
@@ -204,25 +204,47 @@ const TreeNode = ({
204204
const [childNodes, setChildNodes] = useState<GenericContent[]>([])
205205
const [isLoaded, setIsLoaded] = useState<boolean>(false)
206206

207-
const handleNodeClick = async () => {
207+
const getChildren = useCallback(async () => {
208208
const abortController = new AbortController()
209209
const childrenResult = await repository.loadCollection<GenericContent>({
210210
path: node.Path,
211211
requestInit: { signal: abortController.signal },
212212
})
213+
return sortResults(childrenResult.d.results)
214+
}, [node.Path, repository])
213215

214-
onSetCurrentNode(node)
215-
setChildNodes(sortResults(childrenResult.d.results))
216-
setIsLoaded(true)
216+
const setStates = async () => {
217+
if (!isLoaded) {
218+
const children = await getChildren()
219+
setChildNodes(children)
220+
setIsLoaded(true)
221+
}
222+
}
217223

224+
const setExpandedItems = useCallback(() => {
218225
setExpanded((prevExpanded) =>
219226
prevExpanded.includes(node.Id.toString()) ? prevExpanded : [...prevExpanded, node.Id.toString()],
220227
)
228+
}, [node.Id, setExpanded])
229+
230+
const onLabelClick = async () => {
231+
onSetCurrentNode(node)
232+
setStates()
233+
setExpandedItems()
234+
}
235+
236+
const onIconClick = async () => {
237+
setStates()
238+
setExpandedItems()
221239
}
222240

223241
useEffect(() => {
224-
if (expanded.includes(node.Id.toString()) && !isLoaded) {
225-
handleNodeClick()
242+
if (!isLoaded) {
243+
if (currentPath === node.Path) {
244+
onLabelClick()
245+
} else if (expanded.includes(node.Id.toString())) {
246+
onIconClick()
247+
}
226248
}
227249
// eslint-disable-next-line react-hooks/exhaustive-deps
228250
}, [expanded])
@@ -233,14 +255,16 @@ const TreeNode = ({
233255
key={node.Id}
234256
nodeId={node.Id.toString()}
235257
label={
236-
<div style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }} onClick={handleNodeClick}>
258+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
237259
<div className={classes.treeIcon}>{renderIcon(node)}</div>
238260
<div className={classes.treeLabel}>{node.Name}</div>
239261
</div>
240262
}
263+
onIconClick={onIconClick}
264+
onLabelClick={onLabelClick}
241265
collapseIcon={<MinusSquare />}
242266
expandIcon={<PlusSquare />}
243-
endIcon={<PlusSquare />}>
267+
endIcon={isLoaded && childNodes.length === 0 ? null : <PlusSquare />}>
244268
{childNodes.map((childNode) => (
245269
<TreeNode
246270
key={childNode.Id}
@@ -317,49 +341,77 @@ export const PickerAdvanced: React.FC<PickerAdvancedProps> = ({
317341
const searchFieldRef = useRef<HTMLInputElement | null>(null)
318342

319343
//Grid Columns
320-
const addCol: ColDef = {
321-
headerName: '',
322-
field: '',
323-
width: 66,
324-
cellRenderer: (props: { data: GenericContent }) => {
325-
if (selectionRoots && selectionRoots.length > 0) {
326-
const isInSelectionRoots = selectionRoots.some(
327-
(availablePath) => props.data.Path === availablePath || props.data.Path.startsWith(`${availablePath}/`),
328-
)
329-
if (!isInSelectionRoots) return <></>
330-
}
331-
const isDisabled =
332-
selectedItems.some((item) => item.Id === props.data.Id) || (!allowMultiple && selectedItems.length > 0)
333-
if (isDisabled) return <></>
334-
return (
335-
<Button className={classes.actionButton} onClick={() => handleAdd(props.data)}>
336-
&#10009;
337-
</Button>
338-
)
339-
},
340-
}
341-
const removeCol: ColDef = {
342-
headerName: '',
343-
field: '',
344-
width: 66,
345-
cellRenderer: (props: { data: GenericContent }) => {
346-
return (
347-
<Button className={classes.actionButton} onClick={() => handleRemove(props.data)}>
348-
&#10006;
349-
</Button>
350-
)
351-
},
352-
}
353-
const iconCol: ColDef = {
354-
headerName: '',
355-
field: 'Icon',
356-
width: 24,
357-
minWidth: 24,
358-
cellRenderer: (props: { data: GenericContent }) => renderIcon(props.data),
359-
cellStyle: { padding: 0 },
360-
}
361-
const availableCols = [iconCol, ...baseColumns, ...(canPick ? [addCol] : [])]
362-
const selectedCols = [iconCol, ...baseColumns, ...(canPick ? [removeCol] : [])]
344+
const availableCols = useMemo(
345+
() => [
346+
{
347+
headerName: '',
348+
field: 'Icon',
349+
width: 24,
350+
minWidth: 24,
351+
cellRenderer: (props: { data: GenericContent }) => renderIcon(props.data),
352+
cellStyle: { padding: 0 },
353+
},
354+
...baseColumns,
355+
...(canPick
356+
? [
357+
{
358+
headerName: 'Add',
359+
field: '',
360+
width: 66,
361+
cellRenderer: (props: { data: GenericContent }) => {
362+
if (selectionRoots && selectionRoots.length > 0) {
363+
const isInSelectionRoots = selectionRoots.some(
364+
(availablePath) =>
365+
props.data.Path === availablePath || props.data.Path.startsWith(`${availablePath}/`),
366+
)
367+
if (!isInSelectionRoots) return <></>
368+
}
369+
const isDisabled =
370+
selectedItems.some((item) => item.Id === props.data.Id) ||
371+
(!allowMultiple && selectedItems.length > 0)
372+
if (isDisabled) return <></>
373+
return (
374+
<Button className={classes.actionButton} onClick={() => handleAdd(props.data)}>
375+
&#10009;
376+
</Button>
377+
)
378+
},
379+
},
380+
]
381+
: []),
382+
],
383+
[allowMultiple, canPick, classes.actionButton, renderIcon, selectedItems, selectionRoots],
384+
)
385+
const selectedCols = useMemo(
386+
() => [
387+
{
388+
headerName: '',
389+
field: 'Icon',
390+
width: 24,
391+
minWidth: 24,
392+
cellRenderer: (props: { data: GenericContent }) => renderIcon(props.data),
393+
cellStyle: { padding: 0 },
394+
},
395+
...baseColumns,
396+
...(canPick
397+
? [
398+
{
399+
headerName: 'Remove',
400+
field: '',
401+
width: 69,
402+
cellRenderer: (props: { data: GenericContent }) => {
403+
return (
404+
<Button className={classes.actionButton} onClick={() => handleRemove(props.data)}>
405+
&#10006;
406+
</Button>
407+
)
408+
},
409+
},
410+
]
411+
: []),
412+
],
413+
[canPick, classes.actionButton, renderIcon],
414+
)
363415

364416
//Button Actions
365417
const handleAdd = (item: GenericContent) => {

0 commit comments

Comments
 (0)