diff --git a/apps/www/app/blog/[slug]/page.tsx b/apps/www/app/blog/[slug]/page.tsx index cb11a22a1f..7f142ae402 100644 --- a/apps/www/app/blog/[slug]/page.tsx +++ b/apps/www/app/blog/[slug]/page.tsx @@ -198,7 +198,11 @@ export default async function BlogPostPage({ params }: { params: Promise const content = parsedContent.content const tocDepth = (parsedContent.data as any)?.toc_depth ?? 3 const mdxSource = await mdxSerialize(content, { tocDepth }) - const blogPost = { ...parsedContent.data } + const { generateReadingTime } = await import('lib/helpers') + const blogPost = { + ...parsedContent.data, + readingTime: generateReadingTime(content), + } const allStaticPosts = getSortedPosts({ directory: '_blog' }) const allPosts = [...allStaticPosts].sort((a, b) => { diff --git a/apps/www/components/Blog/BlogPostRenderer.tsx b/apps/www/components/Blog/BlogPostRenderer.tsx index 3aa8cf0a0e..95cb9021e8 100644 --- a/apps/www/components/Blog/BlogPostRenderer.tsx +++ b/apps/www/components/Blog/BlogPostRenderer.tsx @@ -163,14 +163,6 @@ const BlogPostRenderer = ({ ? `/images/blog/${blogMetaData.thumb}` : '' - const generateReadingTime = (text: string | undefined): string => { - if (!text) return '0 min read' - const wordsPerMinute = 200 - const numberOfWords = text.split(/\s/g).length - const minutes = Math.ceil(numberOfWords / wordsPerMinute) - return `${minutes} min read` - } - return ( <> {isDraftMode && } @@ -203,10 +195,7 @@ const BlogPostRenderer = ({

{dayjs(blogMetaData.date).format('DD MMM YYYY')}

-

- {(blogMetaData as any).readingTime || - generateReadingTime(blogMetaData.source)} -

+

{(blogMetaData as any).readingTime}

{authors.length > 0 && (
diff --git a/packages/ui-patterns/src/FilterBar/FilterBar.test.tsx b/packages/ui-patterns/src/FilterBar/FilterBar.test.tsx index 7aa3e2d7f4..407d92b60e 100644 --- a/packages/ui-patterns/src/FilterBar/FilterBar.test.tsx +++ b/packages/ui-patterns/src/FilterBar/FilterBar.test.tsx @@ -74,7 +74,7 @@ describe('FilterBar', () => { const handleFilterChange = vi.fn((filters) => { currentFilters = filters }) - + const { rerender } = render( { await waitFor(() => { expect(handleFilterChange).toHaveBeenCalled() }) - + // Re-render with updated filters rerender( { const handleFilterChange = vi.fn((filters) => { currentFilters = filters }) - + const { rerender } = render( { await waitFor(() => { expect(handleFilterChange).toHaveBeenCalled() }) - + rerender( { /> ) - const valueInput = await waitFor( - () => screen.getByLabelText('Value for Status'), - { timeout: 3000 } - ) + const valueInput = await waitFor(() => screen.getByLabelText('Value for Status'), { + timeout: 3000, + }) valueInput.focus() // Popover should show value options @@ -169,7 +168,7 @@ describe('FilterBar', () => { await waitFor(() => { expect(handleFilterChange).toHaveBeenCalledTimes(2) // Once for property, once for value }) - + rerender( { const handleFilterChange = vi.fn((filters) => { currentFilters = filters }) - + const { rerender } = render( { await waitFor(() => { expect(handleFilterChange).toHaveBeenCalled() }) - + rerender( { ) // Wait for FilterCondition to be created - const valueInput = await waitFor( - () => screen.getByLabelText('Value for Tag'), - { timeout: 3000 } - ) + const valueInput = await waitFor(() => screen.getByLabelText('Value for Tag'), { + timeout: 3000, + }) // Focus the value input to show the popover await user.click(valueInput) @@ -261,7 +259,7 @@ describe('FilterBar', () => { await waitFor(() => { expect(handleFilterChange).toHaveBeenCalledTimes(2) // Once for property, once for value }) - + rerender( { expect(result.current.loadingOptions).toEqual({}) }) }) -}) \ No newline at end of file +}) diff --git a/packages/ui-patterns/src/FilterBar/useAIFilter.ts b/packages/ui-patterns/src/FilterBar/useAIFilter.ts index f591877782..6e8cda324b 100644 --- a/packages/ui-patterns/src/FilterBar/useAIFilter.ts +++ b/packages/ui-patterns/src/FilterBar/useAIFilter.ts @@ -96,9 +96,7 @@ function processConditions(conditions: any[], filterProperties: FilterProperty[] conditions: processConditions(condition.conditions, filterProperties), } } else { - const matchedProperty = filterProperties.find( - (prop) => prop.name === condition.propertyName - ) + const matchedProperty = filterProperties.find((prop) => prop.name === condition.propertyName) if (!matchedProperty) { throw new Error(`Invalid property: ${condition.propertyName}`) } @@ -109,4 +107,4 @@ function processConditions(conditions: any[], filterProperties: FilterProperty[] } } }) -} \ No newline at end of file +} diff --git a/packages/ui-patterns/src/FilterBar/useCommandMenu.ts b/packages/ui-patterns/src/FilterBar/useCommandMenu.ts index c8832c049b..676fc97af8 100644 --- a/packages/ui-patterns/src/FilterBar/useCommandMenu.ts +++ b/packages/ui-patterns/src/FilterBar/useCommandMenu.ts @@ -43,7 +43,7 @@ export function useCommandMenu({ if (activeInput?.type === 'group') { items.push(...getPropertyItems(filterProperties, inputValue)) - + if (supportsOperators) { items.push({ value: 'group', @@ -59,7 +59,16 @@ export function useCommandMenu({ }) } } else if (activeInput?.type === 'value') { - items.push(...getValueItems(activeInput, activeFilters, filterProperties, propertyOptionsCache, loadingOptions, inputValue)) + items.push( + ...getValueItems( + activeInput, + activeFilters, + filterProperties, + propertyOptionsCache, + loadingOptions, + inputValue + ) + ) } return items @@ -86,13 +95,17 @@ function getOperatorItems( const property = filterProperties.find((p) => p.name === condition?.propertyName) const operatorValue = condition?.operator?.toUpperCase() || '' const availableOperators = property?.operators || ['='] - + return availableOperators .filter((op) => op.toUpperCase().includes(operatorValue)) .map((op) => ({ value: op, label: op })) } -function getInputValue(activeInput: ActiveInput, freeformText: string, activeFilters: FilterGroup): string { +function getInputValue( + activeInput: ActiveInput, + freeformText: string, + activeFilters: FilterGroup +): string { return activeInput?.type === 'group' ? freeformText : activeInput?.type === 'value' @@ -147,7 +160,7 @@ function getValueItems( function getArrayOptionItems(options: any[], inputValue: string): CommandItem[] { const items: CommandItem[] = [] - + for (const option of options) { if (typeof option === 'string') { if (option.toLowerCase().includes(inputValue.toLowerCase())) { @@ -174,7 +187,7 @@ function getArrayOptionItems(options: any[], inputValue: string): CommandItem[] } } } - + return items } @@ -191,4 +204,4 @@ function getCachedOptionItems(options: any[]): CommandItem[] { label: option.label, } }) -} \ No newline at end of file +} diff --git a/packages/ui-patterns/src/FilterBar/useKeyboardNavigation.ts b/packages/ui-patterns/src/FilterBar/useKeyboardNavigation.ts index 5464a5fe20..f4940d6bf1 100644 --- a/packages/ui-patterns/src/FilterBar/useKeyboardNavigation.ts +++ b/packages/ui-patterns/src/FilterBar/useKeyboardNavigation.ts @@ -90,135 +90,159 @@ export function useKeyboardNavigation({ [activeInput, activeFilters, removeFilterByPath, removeGroupByPath, setActiveInput] ) - const findPreviousCondition = useCallback((currentPath: number[]): number[] | null => { - const [groupPath, conditionIndex] = [currentPath.slice(0, -1), currentPath[currentPath.length - 1]] - - // Try previous condition in same group - if (conditionIndex > 0) { - const prevPath = [...groupPath, conditionIndex - 1] + const findPreviousCondition = useCallback( + (currentPath: number[]): number[] | null => { + const [groupPath, conditionIndex] = [ + currentPath.slice(0, -1), + currentPath[currentPath.length - 1], + ] + + // Try previous condition in same group + if (conditionIndex > 0) { + const prevPath = [...groupPath, conditionIndex - 1] + const group = findGroupByPath(activeFilters, groupPath) + const prevCondition = group?.conditions[conditionIndex - 1] + // If previous is a condition (not a group), return its path + if (prevCondition && !('logicalOperator' in prevCondition)) { + return prevPath + } + // If previous is a group, find its last condition recursively + if (prevCondition && 'logicalOperator' in prevCondition) { + return findLastConditionInGroup(prevPath) + } + } + + // No previous condition in this group, go up to parent + if (groupPath.length > 0) { + return findPreviousCondition(groupPath) + } + + return null + }, + [activeFilters] + ) + + const findNextCondition = useCallback( + (currentPath: number[]): number[] | null => { + const [groupPath, conditionIndex] = [ + currentPath.slice(0, -1), + currentPath[currentPath.length - 1], + ] const group = findGroupByPath(activeFilters, groupPath) - const prevCondition = group?.conditions[conditionIndex - 1] - // If previous is a condition (not a group), return its path - if (prevCondition && !('logicalOperator' in prevCondition)) { - return prevPath + + // Try next condition in same group + if (group && conditionIndex < group.conditions.length - 1) { + const nextPath = [...groupPath, conditionIndex + 1] + const nextCondition = group.conditions[conditionIndex + 1] + // If next is a condition, return its path + if (!('logicalOperator' in nextCondition)) { + return nextPath + } + // If next is a group, find its first condition recursively + return findFirstConditionInGroup(nextPath) } - // If previous is a group, find its last condition recursively - if (prevCondition && 'logicalOperator' in prevCondition) { - return findLastConditionInGroup(prevPath) + + // No next condition in this group, go up to parent and find next + if (groupPath.length > 0) { + return findNextCondition(groupPath) } - } - - // No previous condition in this group, go up to parent - if (groupPath.length > 0) { - return findPreviousCondition(groupPath) - } - - return null - }, [activeFilters]) - const findNextCondition = useCallback((currentPath: number[]): number[] | null => { - const [groupPath, conditionIndex] = [currentPath.slice(0, -1), currentPath[currentPath.length - 1]] - const group = findGroupByPath(activeFilters, groupPath) - - // Try next condition in same group - if (group && conditionIndex < group.conditions.length - 1) { - const nextPath = [...groupPath, conditionIndex + 1] - const nextCondition = group.conditions[conditionIndex + 1] - // If next is a condition, return its path - if (!('logicalOperator' in nextCondition)) { - return nextPath + return null + }, + [activeFilters] + ) + + const findFirstConditionInGroup = useCallback( + (groupPath: number[]): number[] | null => { + const group = findGroupByPath(activeFilters, groupPath) + if (!group || group.conditions.length === 0) return null + + const firstCondition = group.conditions[0] + if (!('logicalOperator' in firstCondition)) { + return [...groupPath, 0] } - // If next is a group, find its first condition recursively - return findFirstConditionInGroup(nextPath) - } - - // No next condition in this group, go up to parent and find next - if (groupPath.length > 0) { - return findNextCondition(groupPath) - } - - return null - }, [activeFilters]) + // First item is a group, recurse + return findFirstConditionInGroup([...groupPath, 0]) + }, + [activeFilters] + ) - const findFirstConditionInGroup = useCallback((groupPath: number[]): number[] | null => { - const group = findGroupByPath(activeFilters, groupPath) - if (!group || group.conditions.length === 0) return null - - const firstCondition = group.conditions[0] - if (!('logicalOperator' in firstCondition)) { - return [...groupPath, 0] - } - // First item is a group, recurse - return findFirstConditionInGroup([...groupPath, 0]) - }, [activeFilters]) + const findLastConditionInGroup = useCallback( + (groupPath: number[]): number[] | null => { + const group = findGroupByPath(activeFilters, groupPath) + if (!group || group.conditions.length === 0) return null - const findLastConditionInGroup = useCallback((groupPath: number[]): number[] | null => { - const group = findGroupByPath(activeFilters, groupPath) - if (!group || group.conditions.length === 0) return null - - const lastCondition = group.conditions[group.conditions.length - 1] - const lastIndex = group.conditions.length - 1 - if (!('logicalOperator' in lastCondition)) { - return [...groupPath, lastIndex] - } - // Last item is a group, recurse - return findLastConditionInGroup([...groupPath, lastIndex]) - }, [activeFilters]) + const lastCondition = group.conditions[group.conditions.length - 1] + const lastIndex = group.conditions.length - 1 + if (!('logicalOperator' in lastCondition)) { + return [...groupPath, lastIndex] + } + // Last item is a group, recurse + return findLastConditionInGroup([...groupPath, lastIndex]) + }, + [activeFilters] + ) - const findPreviousConditionFromGroup = useCallback((groupPath: number[]): number[] | null => { - // If this group has conditions, find the last one - const group = findGroupByPath(activeFilters, groupPath) - if (group && group.conditions.length > 0) { - return findLastConditionInGroup(groupPath) - } - - // No conditions in this group, find previous sibling or parent - if (groupPath.length > 0) { - const parentPath = groupPath.slice(0, -1) - const groupIndex = groupPath[groupPath.length - 1] - if (groupIndex > 0) { - // Find last condition in previous sibling - const prevSiblingPath = [...parentPath, groupIndex - 1] - const parentGroup = findGroupByPath(activeFilters, parentPath) - const prevSibling = parentGroup?.conditions[groupIndex - 1] - if (prevSibling) { - if ('logicalOperator' in prevSibling) { - return findLastConditionInGroup(prevSiblingPath) - } else { - return prevSiblingPath + const findPreviousConditionFromGroup = useCallback( + (groupPath: number[]): number[] | null => { + // If this group has conditions, find the last one + const group = findGroupByPath(activeFilters, groupPath) + if (group && group.conditions.length > 0) { + return findLastConditionInGroup(groupPath) + } + + // No conditions in this group, find previous sibling or parent + if (groupPath.length > 0) { + const parentPath = groupPath.slice(0, -1) + const groupIndex = groupPath[groupPath.length - 1] + if (groupIndex > 0) { + // Find last condition in previous sibling + const prevSiblingPath = [...parentPath, groupIndex - 1] + const parentGroup = findGroupByPath(activeFilters, parentPath) + const prevSibling = parentGroup?.conditions[groupIndex - 1] + if (prevSibling) { + if ('logicalOperator' in prevSibling) { + return findLastConditionInGroup(prevSiblingPath) + } else { + return prevSiblingPath + } } } + // Look at parent group + return findPreviousConditionFromGroup(parentPath) } - // Look at parent group - return findPreviousConditionFromGroup(parentPath) - } - - return null - }, [activeFilters, findLastConditionInGroup]) - const findNextConditionFromGroup = useCallback((groupPath: number[]): number[] | null => { - // Find next sibling or dive into nested groups - if (groupPath.length > 0) { - const parentPath = groupPath.slice(0, -1) - const groupIndex = groupPath[groupPath.length - 1] - const parentGroup = findGroupByPath(activeFilters, parentPath) - - if (parentGroup && groupIndex < parentGroup.conditions.length - 1) { - // Find first condition in next sibling - const nextSiblingPath = [...parentPath, groupIndex + 1] - const nextSibling = parentGroup.conditions[groupIndex + 1] - if ('logicalOperator' in nextSibling) { - return findFirstConditionInGroup(nextSiblingPath) - } else { - return nextSiblingPath + return null + }, + [activeFilters, findLastConditionInGroup] + ) + + const findNextConditionFromGroup = useCallback( + (groupPath: number[]): number[] | null => { + // Find next sibling or dive into nested groups + if (groupPath.length > 0) { + const parentPath = groupPath.slice(0, -1) + const groupIndex = groupPath[groupPath.length - 1] + const parentGroup = findGroupByPath(activeFilters, parentPath) + + if (parentGroup && groupIndex < parentGroup.conditions.length - 1) { + // Find first condition in next sibling + const nextSiblingPath = [...parentPath, groupIndex + 1] + const nextSibling = parentGroup.conditions[groupIndex + 1] + if ('logicalOperator' in nextSibling) { + return findFirstConditionInGroup(nextSiblingPath) + } else { + return nextSiblingPath + } } + // Look at parent group + return findNextConditionFromGroup(parentPath) } - // Look at parent group - return findNextConditionFromGroup(parentPath) - } - - return null - }, [activeFilters, findFirstConditionInGroup]) + + return null + }, + [activeFilters, findFirstConditionInGroup] + ) const handleArrowLeft = useCallback( (e: KeyboardEvent) => { @@ -252,7 +276,7 @@ export function useKeyboardNavigation({ const groupPath = activeInput.path.slice(0, -1) const conditionIndex = activeInput.path[activeInput.path.length - 1] const group = findGroupByPath(activeFilters, groupPath) - + if (group && conditionIndex < group.conditions.length - 1) { // There's a next condition, navigate to it const nextCondition = group.conditions[conditionIndex + 1] @@ -279,10 +303,17 @@ export function useKeyboardNavigation({ } } }, - [activeInput, activeFilters, findGroupByPath, findFirstConditionInGroup, findNextConditionFromGroup, setActiveInput] + [ + activeInput, + activeFilters, + findGroupByPath, + findFirstConditionInGroup, + findNextConditionFromGroup, + setActiveInput, + ] ) return { handleKeyDown, } -} \ No newline at end of file +} diff --git a/packages/ui-patterns/src/FilterBar/utils.test.ts b/packages/ui-patterns/src/FilterBar/utils.test.ts index cd77207570..27cdd8ebb8 100644 --- a/packages/ui-patterns/src/FilterBar/utils.test.ts +++ b/packages/ui-patterns/src/FilterBar/utils.test.ts @@ -201,7 +201,9 @@ describe('FilterBar Utils', () => { const filterOption = { value: 'test', label: 'Test' } expect(isFilterOptionObject(filterOption)).toBe(true) expect(isFilterOptionObject('string')).toBe(false) - expect(isFilterOptionObject({ component: () => React.createElement('div', {}, 'test') })).toBe(false) + expect( + isFilterOptionObject({ component: () => React.createElement('div', {}, 'test') }) + ).toBe(false) }) it('identifies async functions', () => { @@ -224,4 +226,4 @@ describe('FilterBar Utils', () => { expect(isSyncOptionsFunction(array)).toBe(false) }) }) -}) \ No newline at end of file +}) diff --git a/packages/ui-patterns/src/FilterBar/utils.ts b/packages/ui-patterns/src/FilterBar/utils.ts index 1e44454cb8..6276c18aa5 100644 --- a/packages/ui-patterns/src/FilterBar/utils.ts +++ b/packages/ui-patterns/src/FilterBar/utils.ts @@ -1,12 +1,12 @@ -import { - FilterGroup, - FilterCondition, +import { + FilterGroup, + FilterCondition, FilterProperty, CustomOptionObject, FilterOptionObject, AsyncOptionsFunction, SyncOptionsFunction, - isGroup + isGroup, } from './types' export function findGroupByPath(group: FilterGroup, path: number[]): FilterGroup | null { @@ -60,12 +60,16 @@ export function isAsyncOptionsFunction( if (typeof options !== 'function') return false // More reliable async function detection const fnString = options.toString() - return options.constructor.name === 'AsyncFunction' || - fnString.startsWith('async ') || - fnString.includes('async function') + return ( + options.constructor.name === 'AsyncFunction' || + fnString.startsWith('async ') || + fnString.includes('async function') + ) } -export function isSyncOptionsFunction(options: FilterProperty['options']): options is SyncOptionsFunction { +export function isSyncOptionsFunction( + options: FilterProperty['options'] +): options is SyncOptionsFunction { if (!options || Array.isArray(options) || isCustomOptionObject(options)) return false return typeof options === 'function' } @@ -113,8 +117,8 @@ export function removeFromGroup(group: FilterGroup, path: number[]): FilterGroup } export function addFilterToGroup( - group: FilterGroup, - path: number[], + group: FilterGroup, + path: number[], property: FilterProperty ): FilterGroup { if (path.length === 0) { @@ -207,10 +211,7 @@ export function updateNestedOperator( } } -export function updateNestedLogicalOperator( - group: FilterGroup, - path: number[] -): FilterGroup { +export function updateNestedLogicalOperator(group: FilterGroup, path: number[]): FilterGroup { if (path.length === 0) { return { ...group, @@ -244,9 +245,7 @@ export function updateGroupAtPath( return { ...group, conditions: group.conditions.map((condition, index) => - index === current - ? updateGroupAtPath(condition as FilterGroup, rest, newGroup) - : condition + index === current ? updateGroupAtPath(condition as FilterGroup, rest, newGroup) : condition ), } -} \ No newline at end of file +}