diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/Toolbar.md b/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/Toolbar.md index d209d228..4419dd4c 100644 --- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/Toolbar.md +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/Toolbar.md @@ -12,7 +12,7 @@ source: react # If you use typescript, the name of the interface to display props for # These are found through the sourceProps function provided in patternfly-docs.source.js sortValue: 2 -propComponents: ['DataViewToolbar', 'DataViewFilters', 'DataViewTextFilter', 'DataViewCheckboxFilter'] +propComponents: ['DataViewToolbar', 'DataViewFilters', 'DataViewTextFilter', 'DataViewCheckboxFilter', 'DataViewTreeFilter'] sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/Toolbar.md --- import { useMemo, useState, useEffect } from 'react'; @@ -26,6 +26,7 @@ import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataView import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters'; import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter'; import { DataViewCheckboxFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewCheckboxFilter'; +import { DataViewTreeFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTreeFilter'; The **data view toolbar** component renders a default opinionated data view toolbar above or below the data section. @@ -143,6 +144,12 @@ This example demonstrates the setup and usage of filters within the data view. I ``` +### Tree filter example +This example demonstrates the usage of a tree filter with hierarchical data. The tree filter allows users to select items from a nested structure, making it ideal for categorized or grouped filtering options. + +```js file="./TreeFilterExample.tsx" + +``` ## All/selected data switch All/selected data switch allows users to toggle between displaying the entire table (All) and only the rows they have selected (Selected). If the user deselects the last selected row, the filter automatically switches back to All, displaying all table rows again. Until at least one row is selected, a tooltip with guidance is displayed, and the Selected button does not perform any action. The Selected button is intentionally not disabled for accessibility reasons but instead has `aria-disabled` set. diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/TreeFilterExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/TreeFilterExample.tsx new file mode 100644 index 00000000..0ac0f3db --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Toolbar/TreeFilterExample.tsx @@ -0,0 +1,266 @@ +import React, { useMemo } from 'react'; +import { Pagination } from '@patternfly/react-core'; +import { BrowserRouter, useSearchParams } from 'react-router-dom'; +import { TreeViewDataItem } from '@patternfly/react-core'; +import { useDataViewFilters, useDataViewPagination } from '@patternfly/react-data-view/dist/dynamic/Hooks'; +import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView'; +import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; +import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar'; +import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters'; +import { DataViewTreeFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTreeFilter'; + +const perPageOptions = [ + { title: '5', value: 5 }, + { title: '10', value: 10 } +]; + +interface Repository { + name: string; + workspace: string; + tags: string[]; + os: string; + lastSeen: string; +} + +interface RepositoryFilters { + name: string; + workspace: string[]; + os: string[]; + tags: string[]; +} + +const repositories: Repository[] = [ + { name: 'Server-001', workspace: 'Development Workspace', tags: ['web', 'frontend'], os: 'Ubuntu 22.04', lastSeen: '2 hours ago' }, + { name: 'Server-002', workspace: 'Development Workspace', tags: ['api', 'backend'], os: 'RHEL 9', lastSeen: '5 hours ago' }, + { name: 'Server-003', workspace: 'Development Workspace', tags: ['database'], os: 'Windows Server 2022', lastSeen: '1 day ago' }, + { name: 'Server-004', workspace: 'Production Workspace', tags: ['web', 'frontend'], os: 'Ubuntu 22.04', lastSeen: '30 minutes ago' }, + { name: 'Server-005', workspace: 'Production Workspace', tags: ['api', 'backend'], os: 'Debian 12', lastSeen: '1 hour ago' }, + { name: 'Server-006', workspace: 'Production Workspace', tags: ['monitoring'], os: 'macOS Ventura', lastSeen: '3 hours ago' }, + { name: 'Server-007', workspace: 'Production Workspace', tags: ['cache'], os: 'macOS Sonoma', lastSeen: '2 days ago' }, + { name: 'Server-008', workspace: 'Testing Workspace', tags: ['test', 'frontend'], os: 'CentOS 8', lastSeen: '6 hours ago' }, + { name: 'Server-009', workspace: 'Testing Workspace', tags: ['test', 'backend'], os: 'Fedora 38', lastSeen: '4 hours ago' } +]; + +const treeOptions: TreeViewDataItem[] = [ + { + name: 'Production Workspace', + id: 'workspace-prod', + checkProps: { 'aria-label': 'prod-workspace-check', checked: false } + }, + { + name: 'Testing Workspace', + id: 'workspace-test', + checkProps: { 'aria-label': 'test-workspace-check', checked: false } + } +]; + +const osOptions: TreeViewDataItem[] = [ + { + name: 'Linux', + id: 'os-linux', + checkProps: { 'aria-label': 'linux-check', checked: false }, + children: [ + { + name: 'Ubuntu 22.04', + id: 'os-ubuntu', + checkProps: { checked: false } + }, + { + name: 'RHEL 9', + id: 'os-rhel', + checkProps: { checked: false } + }, + { + name: 'Debian 12', + id: 'os-debian', + checkProps: { checked: false } + }, + { + name: 'CentOS 8', + id: 'os-centos', + checkProps: { checked: false } + }, + { + name: 'Fedora 38', + id: 'os-fedora', + checkProps: { checked: false } + } + ], + defaultExpanded: true + }, + { + name: 'Windows', + id: 'os-windows', + checkProps: { 'aria-label': 'windows-check', checked: false }, + children: [ + { + name: 'Windows Server 2022', + id: 'os-windows-2022', + checkProps: { checked: false } + } + ] + }, + { + name: 'macOS', + id: 'os-macos', + checkProps: { 'aria-label': 'macos-check', checked: false }, + children: [ + { + name: 'macOS Ventura', + id: 'os-macos-ventura', + checkProps: { checked: false } + }, + { + name: 'macOS Sonoma', + id: 'os-macos-sonoma', + checkProps: { checked: false } + } + ] + } +]; + +const tagOptions: TreeViewDataItem[] = [ + { + name: 'Environment', + id: 'tags-env', + checkProps: { 'aria-label': 'env-check', checked: false }, + children: [ + { + name: 'web', + id: 'tag-web', + checkProps: { checked: false } + }, + { + name: 'api', + id: 'tag-api', + checkProps: { checked: false } + }, + { + name: 'database', + id: 'tag-database', + checkProps: { checked: false } + } + ], + defaultExpanded: true + }, + { + name: 'Layer', + id: 'tags-layer', + checkProps: { 'aria-label': 'layer-check', checked: false }, + children: [ + { + name: 'frontend', + id: 'tag-frontend', + checkProps: { checked: false } + }, + { + name: 'backend', + id: 'tag-backend', + checkProps: { checked: false } + } + ] + }, + { + name: 'Other', + id: 'tags-other', + checkProps: { 'aria-label': 'other-check', checked: false }, + children: [ + { + name: 'monitoring', + id: 'tag-monitoring', + checkProps: { checked: false } + }, + { + name: 'cache', + id: 'tag-cache', + checkProps: { checked: false } + }, + { + name: 'test', + id: 'tag-test', + checkProps: { checked: false } + } + ] + } +]; + +const columns = ['Name', 'Workspace', 'Tags', 'OS', 'Last seen']; + +const ouiaId = 'TreeFilterExample'; + +const MyTable: React.FunctionComponent = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const { filters, onSetFilters, clearAllFilters } = useDataViewFilters({ + initialFilters: { name: '', workspace: [], os: [], tags: [] }, + searchParams, + setSearchParams, + }); + const pagination = useDataViewPagination({ perPage: 5 }); + const { page, perPage } = pagination; + + const filteredData = useMemo( + () => + repositories.filter( + (item) => + (!filters.name || item.name?.toLocaleLowerCase().includes(filters.name?.toLocaleLowerCase())) && + (!filters.workspace || filters.workspace.length === 0 || filters.workspace.includes(item.workspace)) && + (!filters.os || filters.os.length === 0 || filters.os.includes(item.os)) && + (!filters.tags || filters.tags.length === 0 || filters.tags.some(tag => item.tags.includes(tag))) + ), + [filters] + ); + + const pageRows = useMemo( + () => + filteredData + .slice((page - 1) * perPage, (page - 1) * perPage + perPage) + .map((item) => [item.name, item.workspace, item.tags.join(', '), item.os, item.lastSeen]), + [page, perPage, filteredData] + ); + + return ( + + } + filters={ + onSetFilters(values)} values={filters}> + { + console.log('Selected OS items:', selectedItems); + }} + /> + { + console.log('Selected tag items:', selectedItems); + }} + /> + + } + /> + + + } + /> + + ); +}; + +export const TreeFilterExample: React.FunctionComponent = () => ( + + + +); diff --git a/packages/module/patternfly-docs/generated/index.js b/packages/module/patternfly-docs/generated/index.js index be42c242..7d67102f 100644 --- a/packages/module/patternfly-docs/generated/index.js +++ b/packages/module/patternfly-docs/generated/index.js @@ -2,8 +2,8 @@ module.exports = { '/extensions/data-view/toolbar/react': { id: "Toolbar", title: "Data view toolbar", - toc: [[{"text":"Toolbar example"}],{"text":"Toolbar actions"},[{"text":"Actions example"}],{"text":"Pagination"},[{"text":"Pagination state"},{"text":"Pagination example"}],{"text":"Selection"},[{"text":"Selection state"},{"text":"Selection example"}],{"text":"Filters"},[{"text":"Filters state"},{"text":"Filtering example"}],{"text":"All/selected data switch"},[{"text":"All/selected example"}]], - examples: ["Toolbar example","Actions example","Pagination example","Selection example","Filtering example","All/selected example"], + toc: [[{"text":"Toolbar example"}],{"text":"Toolbar actions"},[{"text":"Actions example"}],{"text":"Pagination"},[{"text":"Pagination state"},{"text":"Pagination example"}],{"text":"Selection"},[{"text":"Selection state"},{"text":"Selection example"}],{"text":"Filters"},[{"text":"Filters state"},{"text":"Filtering example"},{"text":"Tree filter example"}],{"text":"All/selected data switch"},[{"text":"All/selected example"}]], + examples: ["Toolbar example","Actions example","Pagination example","Selection example","Filtering example","Tree filter example","All/selected example"], section: "extensions", subsection: "Data view", source: "react", diff --git a/packages/module/src/DataViewFilters/DataViewFilters.tsx b/packages/module/src/DataViewFilters/DataViewFilters.tsx index dc0dc218..d750452e 100644 --- a/packages/module/src/DataViewFilters/DataViewFilters.tsx +++ b/packages/module/src/DataViewFilters/DataViewFilters.tsx @@ -62,7 +62,7 @@ export const DataViewFilters = ({ useEffect(() => { filterItems.length > 0 && setActiveAttributeMenu(filterItems[0].title); - }, [ filterItems ]); + }, [ filterItems.length ]); const handleClickOutside = (event: MouseEvent) => isAttributeMenuOpen && diff --git a/packages/module/src/DataViewTreeFilter/DataViewTreeFilter.test.tsx b/packages/module/src/DataViewTreeFilter/DataViewTreeFilter.test.tsx new file mode 100644 index 00000000..cf5eeb6e --- /dev/null +++ b/packages/module/src/DataViewTreeFilter/DataViewTreeFilter.test.tsx @@ -0,0 +1,50 @@ +import { render } from '@testing-library/react'; +import DataViewTreeFilter, { DataViewTreeFilterProps } from './DataViewTreeFilter'; +import DataViewToolbar from '../DataViewToolbar'; +import { TreeViewDataItem } from '@patternfly/react-core'; + +describe('DataViewTreeFilter component', () => { + const treeItems: TreeViewDataItem[] = [ + { + name: 'Development Workspace', + id: 'workspace-dev', + checkProps: { 'aria-label': 'dev-workspace-check', checked: false } + }, + { + name: 'Production Workspace', + id: 'workspace-prod', + checkProps: { 'aria-label': 'prod-workspace-check', checked: false } + }, + { + name: 'Operating Systems', + id: 'os-parent', + checkProps: { 'aria-label': 'os-check', checked: false }, + children: [ + { + name: 'Linux', + id: 'os-linux', + checkProps: { checked: false } + }, + { + name: 'Windows', + id: 'os-windows', + checkProps: { checked: false } + } + ] + } + ]; + + const defaultProps: DataViewTreeFilterProps = { + filterId: 'test-tree-filter', + title: 'Test Tree Filter', + value: ['Linux'], + items: treeItems + }; + + it('should render correctly', () => { + const { container } = render( + } /> + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/module/src/DataViewTreeFilter/DataViewTreeFilter.tsx b/packages/module/src/DataViewTreeFilter/DataViewTreeFilter.tsx new file mode 100644 index 00000000..27400907 --- /dev/null +++ b/packages/module/src/DataViewTreeFilter/DataViewTreeFilter.tsx @@ -0,0 +1,348 @@ +import { Dropdown, MenuToggle, MenuToggleElement, ToolbarFilter, ToolbarFilterProps, TreeView, TreeViewDataItem } from '@patternfly/react-core' +import React, { FC, useState, useRef, useEffect } from 'react' +import { createUseStyles } from 'react-jss' + +/** This style is needed so the tree filter dropdown looks like the basic filter dropdow */ +const useStyles = createUseStyles({ + dataViewTreeFilterTreeView: { + '& .pf-v6-c-tree-view__node::after': { + borderRadius: 0, + borderRightStyle: 'none', + borderLeftStyle: 'none' + }, + '& .pf-v6-c-tree-view__content': { + borderRadius: 0 + } + } +}) + +export interface DataViewTreeFilterProps { + /** Unique key for the filter attribute */ + filterId: string; + /** Array of current filter values */ + value?: string[]; + /** Filter title displayed in the toolbar */ + title: string; + /** Callback for when the selection changes */ + onChange?: (event?: React.MouseEvent, values?: string[]) => void; + /** Controls visibility of the filter in the toolbar */ + showToolbarItem?: ToolbarFilterProps['showToolbarItem']; + /** Custom OUIA ID */ + ouiaId?: string; + /** Hierarchical data items for the tree structure */ + items?: TreeViewDataItem[]; + /** When true, expands all tree nodes by default */ + defaultExpanded?: boolean; + /** Callback for when tree items are selected/deselected, provides all currently selected nodes */ + onSelect?: (selectedItems: TreeViewDataItem[]) => void; + /** Array of pre-selected item id's to be checked on initial render */ + defaultSelected?: string[]; +} + +export const DataViewTreeFilter: FC = ({ + filterId, + title, + value = [], + onChange, + showToolbarItem, + ouiaId = 'DataViewTreeFilter', + items, + defaultExpanded = false, + onSelect, + defaultSelected = [] +}: DataViewTreeFilterProps) => { + const classes = useStyles(); + const [isOpen, setIsOpen] = useState(false); + const [treeData, setTreeData] = useState(items || []); + const menuRef = useRef(null); + const isInitialMount = useRef(true); + const hasCalledInitialOnChange = useRef(false); + + // Helper function to expand all nodes in the tree + const expandAllNodes = (items: TreeViewDataItem[]): TreeViewDataItem[] => { + return items.map(item => ({ + ...item, + defaultExpanded: true, + children: item.children ? expandAllNodes(item.children) : undefined + })); + }; + + // Helper function to set pre-selected items + const setPreSelectedItems = (items: TreeViewDataItem[], selectedIds: string[]): TreeViewDataItem[] => { + return items.map(item => { + const isSelected = selectedIds.includes(String(item.id)); + const hasSelectedChildren = item.children?.some(child => selectedIds.includes(String(child.id))) ?? false; + + return { + ...item, + checkProps: item.checkProps ? { + ...item.checkProps, + checked: isSelected || hasSelectedChildren + } : undefined, + children: item.children ? setPreSelectedItems(item.children, selectedIds) : undefined + }; + }); + }; + + // Generic helper to collect items from tree based on predicate + const collectTreeItems = ( + items: TreeViewDataItem[], + predicate: (item: TreeViewDataItem) => boolean, + leafOnly = false + ): TreeViewDataItem[] => { + const collected: TreeViewDataItem[] = []; + + const collect = (item: TreeViewDataItem) => { + const isLeaf = !item.children || item.children.length === 0; + + if (predicate(item) && (!leafOnly || isLeaf)) { + collected.push(item); + } + + item.children?.forEach(child => collect(child)); + }; + + items.forEach(item => collect(item)); + return collected; + }; + + // Helper function to get all checked items (not just leaf nodes) + const getAllCheckedItems = (items: TreeViewDataItem[]): TreeViewDataItem[] => { + return collectTreeItems(items, item => item.checkProps?.checked === true, false); + }; + + // Initialize tree data with defaultExpanded and defaultSelected (only on first mount) + useEffect(() => { + if (!items) return; + + let initializedData = [...items]; + + // Apply default expansion + if (defaultExpanded) { + initializedData = expandAllNodes(initializedData); + } + + // Apply pre-selected items only on initial mount + if (isInitialMount.current && defaultSelected.length > 0) { + initializedData = setPreSelectedItems(initializedData, defaultSelected); + } + + setTreeData(initializedData); + + if (isInitialMount.current) { + isInitialMount.current = false; + } + }, [items, defaultExpanded]); + + // Call onChange and onSelect after tree data is initialized with default selections + useEffect(() => { + if (!hasCalledInitialOnChange.current && defaultSelected.length > 0 && treeData.length > 0) { + const selectedValues = getAllCheckedLeafItems(treeData); + + // Only call if there are actually selected values + if (selectedValues.length > 0) { + // Defer the callbacks to avoid updating parent during render + queueMicrotask(() => { + if (onChange) { + onChange(undefined, selectedValues); + } + + if (onSelect) { + const selectedItems = getAllCheckedItems(treeData); + onSelect(selectedItems); + } + }); + + hasCalledInitialOnChange.current = true; + } + } + }, [treeData]); + + // Sync tree checkboxes when value prop changes (when clearAllFilters is called) + useEffect(() => { + if (value.length === 0 && treeData.length > 0) { + const currentCheckedItems = getAllCheckedLeafItems(treeData); + + // Only update if there are checked items that need to be unchecked + if (currentCheckedItems.length > 0) { + const uncheckRecursive = (items: TreeViewDataItem[]): TreeViewDataItem[] => { + return items.map(item => ({ + ...item, + checkProps: item.checkProps ? { ...item.checkProps, checked: false } : undefined, + children: item.children ? uncheckRecursive(item.children) : undefined + })); + }; + + setTreeData(uncheckRecursive(treeData)); + } + } + }, [value]); + + // Check if all children are checked (recursive) + const areAllChildrenChecked = (item: TreeViewDataItem): boolean => { + if (!item.children?.length) { + return item.checkProps?.checked === true; + } + return item.children.every(child => areAllChildrenChecked(child)); + }; + + // Check if some children are checked (recursive) + const areSomeChildrenChecked = (item: TreeViewDataItem): boolean => { + if (!item.children?.length) { + return item.checkProps?.checked === true; + } + return item.children.some(child => areSomeChildrenChecked(child)); + }; + + // Find tree item by name + const findItemByName = (items: TreeViewDataItem[], name: string): TreeViewDataItem | null => { + for (const item of items) { + if (item.name === name) { + return item; + } + if (item.children) { + const found = findItemByName(item.children, name); + if (found) return found; + } + } + return null; + }; + + // Find parent item by child ID + const findParentById = (items: TreeViewDataItem[], childId: string): TreeViewDataItem | null => { + for (const item of items) { + if (item.children?.some(child => child.id === childId)) { + return item; + } + if (item.children) { + const found = findParentById(item.children, childId); + if (found) return found; + } + } + return null; + }; + + // Get all checked leaf items (returns array of names) + const getAllCheckedLeafItems = (items: TreeViewDataItem[]): string[] => { + return collectTreeItems( + items, + item => item.checkProps?.checked === true, + true + ).map(item => String(item.name)); + }; + + // Update parent checkbox states based on children (recursive) + const onCheckParentHandle = (childId: string): void => { + const parent = findParentById(treeData, childId); + if (!parent) return; + + if (parent.checkProps) { + const allChildrenChecked = areAllChildrenChecked(parent); + const someChildrenChecked = areSomeChildrenChecked(parent); + + parent.checkProps.checked = allChildrenChecked ? true : someChildrenChecked ? null : false; + } + + if (parent.id) { + onCheckParentHandle(parent.id); + } + }; + + // Check/uncheck item and all its children (recursive) + const onCheckHandle = (treeViewItem: TreeViewDataItem, checked: boolean): void => { + if (treeViewItem.checkProps) { + treeViewItem.checkProps.checked = checked; + } + + treeViewItem.children?.forEach(child => onCheckHandle(child, checked)); + }; + + // Handle checkbox change event + const onCheck = (event: React.ChangeEvent, treeViewItem: TreeViewDataItem) => { + const checked = (event.target as HTMLInputElement).checked; + + onCheckHandle(treeViewItem, checked); + + if (treeViewItem.id) { + onCheckParentHandle(treeViewItem.id); + } + + setTreeData(prev => [...prev]); + + const selectedValues = getAllCheckedLeafItems(treeData); + onChange?.(event as any, selectedValues); + + if (onSelect) { + const selectedItems = getAllCheckedItems(treeData); + onSelect(selectedItems); + } + }; + + // Clear a specific filter by name (when label chip is removed) + const onFilterSelectorClear = (itemName: string) => { + const treeViewItem = findItemByName(treeData, itemName); + if (!treeViewItem) return; + + onCheckHandle(treeViewItem, false); + if (treeViewItem.id) { + onCheckParentHandle(treeViewItem.id); + } + }; + + // Uncheck all items in the tree + const uncheckAllItems = () => { + const uncheckRecursive = (items: TreeViewDataItem[]): TreeViewDataItem[] => { + return items.map(item => ({ + ...item, + checkProps: item.checkProps ? { ...item.checkProps, checked: false } : undefined, + children: item.children ? uncheckRecursive(item.children) : undefined + })); + }; + + const updatedTreeData = uncheckRecursive(treeData); + setTreeData(updatedTreeData); + onChange?.(undefined, []); + }; + + const dropdown = ( + setIsOpen(isOpen)} + toggle={(toggleRef: React.Ref) => ( + setIsOpen(!isOpen)} isExpanded={isOpen}> + {title} + + )} + ouiaId={ouiaId} + shouldFocusToggleOnSelect + > + + + ); + + return ( + ({ key: item, node: item }))} + deleteLabel={(_, label) => { + const labelKey = typeof label === 'string' ? label : label.key; + onChange?.(undefined, value.filter(item => item !== labelKey)); + onFilterSelectorClear(labelKey); + }} + deleteLabelGroup={uncheckAllItems} + categoryName={title} + showToolbarItem={showToolbarItem}> + {dropdown} + + ) +} + +export default DataViewTreeFilter; \ No newline at end of file diff --git a/packages/module/src/DataViewTreeFilter/__snapshots__/DataViewTreeFilter.test.tsx.snap b/packages/module/src/DataViewTreeFilter/__snapshots__/DataViewTreeFilter.test.tsx.snap new file mode 100644 index 00000000..0eae4a4a --- /dev/null +++ b/packages/module/src/DataViewTreeFilter/__snapshots__/DataViewTreeFilter.test.tsx.snap @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataViewTreeFilter component should render correctly 1`] = ` +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
    +
  • + + + + Linux + + + + + + +
  • +
+
+
+
+
+
+
+ +
+
+
+
+
+`; diff --git a/packages/module/src/DataViewTreeFilter/index.ts b/packages/module/src/DataViewTreeFilter/index.ts new file mode 100644 index 00000000..7faa9ae6 --- /dev/null +++ b/packages/module/src/DataViewTreeFilter/index.ts @@ -0,0 +1,2 @@ +export { default } from './DataViewTreeFilter'; +export * from './DataViewTreeFilter'; \ No newline at end of file diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index aca7f761..d09fbc5c 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -4,6 +4,9 @@ export { default as InternalContext } from './InternalContext'; export * from './InternalContext'; export * from './Hooks'; +export { default as DataViewTreeFilter } from './DataViewTreeFilter'; +export * from './DataViewTreeFilter'; + export { default as DataViewToolbar } from './DataViewToolbar'; export * from './DataViewToolbar';