mirror of https://github.com/grafana/grafana
Explore: Nested Content Outline (#80688)
* indentation levels * Highlight parent section when child is selected * Fix context, add rotation to toggle button * Merge single child logic; fix styling * Fix collapsed logic, make elipsis work, reorganize for better readability; * Add connector * Tooltip placement * Update tests so they test components the same way users would interact with them * Clean up indendation levels * Support collapsing logic for multiple section; highlight all items in a section of an active child - parent is active only when section is collapsed and child inside of it is active * Simplify making ellipsis work * Show tooltip if the text overflows in expanded mode * The whole button container should have same background when section is expanded in mini view * Fix a bug where root items were not being sorted by document position * Update query order when query rows are changed through dragging and dropping * Fix the issue where chaning the title of a query row would remove the querypull/85393/head
parent
73e426b081
commit
649c456eab
@ -1,64 +1,163 @@ |
|||||||
import { uniqueId } from 'lodash'; |
import { uniqueId } from 'lodash'; |
||||||
import React, { useState, useContext, createContext, ReactNode, useCallback } from 'react'; |
import React, { useState, useContext, createContext, ReactNode, useCallback, useRef, useEffect } from 'react'; |
||||||
|
|
||||||
import { ContentOutlineItemBaseProps } from './ContentOutlineItem'; |
import { ContentOutlineItemBaseProps } from './ContentOutlineItem'; |
||||||
|
|
||||||
export interface ContentOutlineItemContextProps extends ContentOutlineItemBaseProps { |
export interface ContentOutlineItemContextProps extends ContentOutlineItemBaseProps { |
||||||
id: string; |
id: string; |
||||||
ref: HTMLElement | null; |
ref: HTMLElement | null; |
||||||
|
children?: ContentOutlineItemContextProps[]; |
||||||
} |
} |
||||||
|
|
||||||
type RegisterFunction = ({ title, icon, ref }: Omit<ContentOutlineItemContextProps, 'id'>) => string; |
type RegisterFunction = (outlineItem: Omit<ContentOutlineItemContextProps, 'id'>) => string; |
||||||
|
|
||||||
export interface ContentOutlineContextProps { |
export interface ContentOutlineContextProps { |
||||||
outlineItems: ContentOutlineItemContextProps[]; |
outlineItems: ContentOutlineItemContextProps[]; |
||||||
register: RegisterFunction; |
register: RegisterFunction; |
||||||
unregister: (id: string) => void; |
unregister: (id: string) => void; |
||||||
|
updateOutlineItems: (newItems: ContentOutlineItemContextProps[]) => void; |
||||||
|
} |
||||||
|
|
||||||
|
interface ContentOutlineContextProviderProps { |
||||||
|
children: ReactNode; |
||||||
|
/** |
||||||
|
* used to resort children of an outline item when the dependencies change |
||||||
|
* e.g. when the order of query rows changes on drag and drop |
||||||
|
*/ |
||||||
|
refreshDependencies?: unknown[]; |
||||||
|
} |
||||||
|
|
||||||
|
interface ParentlessItems { |
||||||
|
[panelId: string]: ContentOutlineItemContextProps[]; |
||||||
} |
} |
||||||
|
|
||||||
const ContentOutlineContext = createContext<ContentOutlineContextProps | undefined>(undefined); |
const ContentOutlineContext = createContext<ContentOutlineContextProps | undefined>(undefined); |
||||||
|
|
||||||
export const ContentOutlineContextProvider = ({ children }: { children: ReactNode }) => { |
export function ContentOutlineContextProvider({ children, refreshDependencies }: ContentOutlineContextProviderProps) { |
||||||
const [outlineItems, setOutlineItems] = useState<ContentOutlineItemContextProps[]>([]); |
const [outlineItems, setOutlineItems] = useState<ContentOutlineItemContextProps[]>([]); |
||||||
|
const parentlessItemsRef = useRef<ParentlessItems>({}); |
||||||
|
|
||||||
const register: RegisterFunction = useCallback(({ title, icon, ref }) => { |
const register: RegisterFunction = useCallback((outlineItem) => { |
||||||
const id = uniqueId(`${title}-${icon}_`); |
const id = uniqueId(`${outlineItem.panelId}-${outlineItem.title}-${outlineItem.icon}_`); |
||||||
|
|
||||||
setOutlineItems((prevItems) => { |
setOutlineItems((prevItems) => { |
||||||
const updatedItems = [...prevItems, { id, title, icon, ref }]; |
if (outlineItem.level === 'root') { |
||||||
|
const mergeSingleChild = checkMergeSingleChild(parentlessItemsRef, outlineItem); |
||||||
return updatedItems.sort((a, b) => { |
const updatedItems = [ |
||||||
if (a.ref && b.ref) { |
...prevItems, |
||||||
const diff = a.ref.compareDocumentPosition(b.ref); |
{ |
||||||
if (diff === Node.DOCUMENT_POSITION_PRECEDING) { |
...outlineItem, |
||||||
return 1; |
id, |
||||||
} else if (diff === Node.DOCUMENT_POSITION_FOLLOWING) { |
children: parentlessItemsRef.current[outlineItem.panelId] || [], |
||||||
return -1; |
mergeSingleChild, |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
return updatedItems.sort(sortElementsByDocumentPosition); |
||||||
|
} |
||||||
|
|
||||||
|
if (outlineItem.level === 'child') { |
||||||
|
const parentIndex = prevItems.findIndex( |
||||||
|
(item) => item.panelId === outlineItem.panelId && item.level === 'root' |
||||||
|
); |
||||||
|
if (parentIndex === -1) { |
||||||
|
const parentlessItemSibling = Object.keys(parentlessItemsRef.current).find( |
||||||
|
(key) => key === outlineItem.panelId |
||||||
|
); |
||||||
|
|
||||||
|
if (parentlessItemSibling) { |
||||||
|
parentlessItemsRef.current[outlineItem.panelId].push({ |
||||||
|
...outlineItem, |
||||||
|
id, |
||||||
|
}); |
||||||
|
} else { |
||||||
|
parentlessItemsRef.current[outlineItem.panelId] = [ |
||||||
|
{ |
||||||
|
...outlineItem, |
||||||
|
id, |
||||||
|
}, |
||||||
|
]; |
||||||
} |
} |
||||||
|
return [...prevItems]; |
||||||
} |
} |
||||||
return 0; |
|
||||||
}); |
const newItems = [...prevItems]; |
||||||
|
const parent = { ...newItems[parentIndex] }; |
||||||
|
const childrenUpdated = [...(parent.children || []), { ...outlineItem, id }]; |
||||||
|
childrenUpdated.sort(sortElementsByDocumentPosition); |
||||||
|
const mergeSingleChild = checkMergeSingleChild(parentlessItemsRef, parent); |
||||||
|
|
||||||
|
newItems[parentIndex] = { |
||||||
|
...parent, |
||||||
|
children: childrenUpdated, |
||||||
|
mergeSingleChild, |
||||||
|
}; |
||||||
|
|
||||||
|
return newItems; |
||||||
|
} |
||||||
|
|
||||||
|
return [...prevItems]; |
||||||
}); |
}); |
||||||
|
|
||||||
return id; |
return id; |
||||||
}, []); |
}, []); |
||||||
|
|
||||||
const unregister = useCallback((id: string) => { |
const unregister = useCallback((id: string) => { |
||||||
setOutlineItems((prevItems) => prevItems.filter((item) => item.id !== id)); |
setOutlineItems((prevItems) => |
||||||
|
prevItems |
||||||
|
.filter((item) => item.id !== id) |
||||||
|
.map((item) => { |
||||||
|
if (item.children) { |
||||||
|
item.children = item.children.filter((child) => child.id !== id); |
||||||
|
} |
||||||
|
return item; |
||||||
|
}) |
||||||
|
); |
||||||
|
}, []); |
||||||
|
|
||||||
|
const updateOutlineItems = useCallback((newItems: ContentOutlineItemContextProps[]) => { |
||||||
|
setOutlineItems(newItems); |
||||||
}, []); |
}, []); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setOutlineItems((prevItems) => { |
||||||
|
const newItems = [...prevItems]; |
||||||
|
for (const item of newItems) { |
||||||
|
item.children?.sort(sortElementsByDocumentPosition); |
||||||
|
} |
||||||
|
return newItems; |
||||||
|
}); |
||||||
|
}, [refreshDependencies]); |
||||||
|
|
||||||
return ( |
return ( |
||||||
<ContentOutlineContext.Provider value={{ outlineItems, register, unregister }}> |
<ContentOutlineContext.Provider value={{ outlineItems, register, unregister, updateOutlineItems }}> |
||||||
{children} |
{children} |
||||||
</ContentOutlineContext.Provider> |
</ContentOutlineContext.Provider> |
||||||
); |
); |
||||||
}; |
} |
||||||
|
|
||||||
export function useContentOutlineContext() { |
|
||||||
const ctx = useContext(ContentOutlineContext); |
|
||||||
|
|
||||||
if (!ctx) { |
export function sortElementsByDocumentPosition(a: ContentOutlineItemContextProps, b: ContentOutlineItemContextProps) { |
||||||
throw new Error('useContentOutlineContext must be used within a ContentOutlineContextProvider'); |
if (a.ref && b.ref) { |
||||||
|
const diff = a.ref.compareDocumentPosition(b.ref); |
||||||
|
if (diff === Node.DOCUMENT_POSITION_PRECEDING) { |
||||||
|
return 1; |
||||||
|
} else if (diff === Node.DOCUMENT_POSITION_FOLLOWING) { |
||||||
|
return -1; |
||||||
|
} |
||||||
} |
} |
||||||
return ctx; |
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
function checkMergeSingleChild( |
||||||
|
parentlessItemsRef: React.MutableRefObject<ParentlessItems>, |
||||||
|
outlineItem: Omit<ContentOutlineItemContextProps, 'id'> |
||||||
|
) { |
||||||
|
const children = parentlessItemsRef.current[outlineItem.panelId] || []; |
||||||
|
const mergeSingleChild = children.length === 1 && outlineItem.mergeSingleChild; |
||||||
|
|
||||||
|
return mergeSingleChild; |
||||||
|
} |
||||||
|
|
||||||
|
export function useContentOutlineContext() { |
||||||
|
return useContext(ContentOutlineContext); |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue