Skip to content

Commit

Permalink
first version of filters and sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
athens-server committed Jan 28, 2025
1 parent 4a3fcee commit ba97acd
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 7 deletions.
4 changes: 2 additions & 2 deletions packages/ui/locales/en/views.json
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,9 @@
"active": "Active users",
"inactive": "Pending users"
},
"usersHeader": "Users",
"searchPlaceholder": "Search",
"newUserButton": "New user",
"usersHeader": "Users"
"newUserButton": "New user"
},
"labelData": {
"create": "Create labels"
Expand Down
8 changes: 6 additions & 2 deletions packages/ui/locales/es/views.json
Original file line number Diff line number Diff line change
Expand Up @@ -389,9 +389,13 @@
"delete": "Delete webhook"
},
"userManagement": {
"tabs": {
"active": "Active users",
"inactive": "Pending users"
},
"usersHeader": "Users",
"searchPlaceholder": "Search",
"newUserButton": "New user",
"usersHeader": "Users"
"newUserButton": "New user"
},
"labelData": {
"create": "Create labels"
Expand Down
8 changes: 6 additions & 2 deletions packages/ui/locales/fr/views.json
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,13 @@
"delete": "Delete webhook"
},
"userManagement": {
"tabs": {
"active": "Active users",
"inactive": "Pending users"
},
"usersHeader": "Users",
"searchPlaceholder": "Search",
"newUserButton": "New user",
"usersHeader": "Users"
"newUserButton": "New user"
},
"labelData": {
"create": "Create labels"
Expand Down
85 changes: 85 additions & 0 deletions packages/ui/src/views/user-management/constants/filter-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { type FilterCondition, type FilterOption, type SortDirection, type SortOption } from '@components/filters'
import { TFunction } from 'i18next'

export const getBasicConditions = (t: TFunction): FilterCondition[] => [
{ label: t('component:filter.is', 'is'), value: 'is' },
{ label: t('component:filter.isNot', 'is not'), value: 'is_not' },
{ label: t('component:filter.isEmpty', 'is empty'), value: 'is_empty' },
{ label: t('component:filter.isNotEmpty', 'is not empty'), value: 'is_not_empty' }
]

export const getRangeConditions = (t: TFunction): FilterCondition[] => [
{ label: t('component:filter.is', 'is'), value: 'is' },
{ label: t('component:filter.isBefore', 'is before'), value: 'is_before' },
{ label: t('component:filter.isAfter', 'is after'), value: 'is_after' },
{ label: t('component:filter.isBetween', 'is between'), value: 'is_between' },
{ label: t('component:filter.isEmpty', 'is empty'), value: 'is_empty' },
{ label: t('component:filter.isNotEmpty', 'is not empty'), value: 'is_not_empty' }
]

export const getTextConditions = (t: TFunction): FilterCondition[] => [
{ label: t('component:filter.is', 'is'), value: 'is' },
{ label: t('component:filter.isNot', 'is not'), value: 'is_not' },
{ label: t('component:filter.contains', 'contains'), value: 'contains' },
{ label: t('component:filter.doesNotContain', 'does not contain'), value: 'does_not_contain' },
{ label: t('component:filter.startsWith', 'starts with'), value: 'starts_with' },
{ label: t('component:filter.endsWith', 'ends with'), value: 'ends_with' },
{ label: t('component:filter.isEmpty', 'is empty'), value: 'is_empty' },
{ label: t('component:filter.isNotEmpty', 'is not empty'), value: 'is_not_empty' }
]

export const getNumberConditions = (t: TFunction): FilterCondition[] => [
{ label: '=', value: 'equals' },
{ label: '≠', value: 'not_equals' },
{ label: '>', value: 'greater' },
{ label: '<', value: 'less' },
{ label: '≥', value: 'greater_equals' },
{ label: '≤', value: 'less_equals' },
{ label: t('component:filter.isEmpty', 'is empty'), value: 'is_empty' },
{ label: t('component:filter.isNotEmpty', 'is not empty'), value: 'is_not_empty' }
]

export const getFilterOptions = (t: TFunction): FilterOption[] => [
{
label: t('component:filter.type', 'Type'),
value: 'type',
type: 'checkbox',
conditions: getBasicConditions(t),
options: [
{ label: t('component:filter.public', 'Public'), value: 'public' },
{ label: t('component:filter.private', 'Private'), value: 'private' },
{ label: t('component:filter.fork', 'Fork'), value: 'fork' }
]
},
{
label: t('component:filter.createdTime', 'Created Time'),
value: 'created_time',
type: 'calendar',
conditions: getRangeConditions(t)
},
{
label: t('component:filter.name', 'Name'),
value: 'name',
type: 'text',
conditions: getTextConditions(t)
},
{
label: t('component:filter.stars', 'Stars'),
value: 'stars',
type: 'number',
conditions: getNumberConditions(t)
}
]

export const getSortOptions = (t: TFunction): SortOption[] => [
{ label: t('component:sort.lastUpdated', 'Last updated'), value: 'updated' },
{ label: t('component:sort.stars', 'Stars'), value: 'stars' },
{ label: t('component:sort.forks', 'Forks'), value: 'forks' },
{ label: t('component:sort.pullRequests', 'Pull Requests'), value: 'pulls' },
{ label: t('component:sort.title', 'Title'), value: 'title' }
]

export const getSortDirections = (t: TFunction): SortDirection[] => [
{ label: t('component:filter.ascending', 'Ascending'), value: 'asc' },
{ label: t('component:filter.descending', 'Descending'), value: 'desc' }
]
3 changes: 3 additions & 0 deletions packages/ui/src/views/user-management/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { useFilters } from './use-filters'

export { useFilters }
204 changes: 204 additions & 0 deletions packages/ui/src/views/user-management/hooks/use-filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { useCallback, useState } from 'react'

import { FilterAction, FilterHandlers, FilterSearchQueries, FilterValue, SortValue } from '@/components/filters/types'

export const useFilters = (): FilterHandlers => {
// Initialize state with initialState if provided
const [activeFilters, setActiveFilters] = useState<FilterValue[]>([])
const [activeSorts, setActiveSorts] = useState<SortValue[]>([])
const [searchQueries, setSearchQueries] = useState<FilterSearchQueries>({
filters: {},
menu: {}
})
const [filterToOpen, setFilterToOpen] = useState<FilterAction | null>(null)

// Load saved views on mount

// FILTERS
/**
* Handles adding a new filter to the active filters list
* @param newFilter - Filter object containing:
* - type: string - The type of filter (e.g., 'type', 'language')
* - condition: string - The condition for filtering (will be overridden with default 'is')
* - selectedValues: string[] - Array of selected values (will be initialized as empty)
*
* Only adds the filter if one with the same type doesn't already exist
*/
const handleFilterChange = (
newFilter: Omit<FilterValue, 'condition' | 'selectedValues'>,
defaultCondition = 'is'
) => {
setActiveFilters(prevFilters => {
// Indicate which filter should be opened
setFilterToOpen({ type: newFilter.type, kind: 'filter' })

if (!prevFilters.find(f => f.type === newFilter.type)) {
return [
...prevFilters,
{
...newFilter,
condition: defaultCondition,
selectedValues: []
}
]
}
return prevFilters
})
}

/**
* Updates the selected values for a specific filter type
* @param type - The type of filter to update (e.g., 'type', 'language')
* @param selectedValues - New array of selected values for the filter
*
* Maps through all filters and updates only the matching one
* while preserving other filters unchanged
*/
const handleUpdateFilter = (type: string, selectedValues: string[]) => {
setActiveFilters(activeFilters.map(filter => (filter.type === type ? { ...filter, selectedValues } : filter)))
}

/**
* Updates the condition for a specific filter type
* @param type - The type of filter to update (e.g., 'type', 'language')
* @param condition - New condition value (e.g., 'is', 'is_not', 'is_empty')
*
* Special handling for 'is_empty' condition:
* - When 'is_empty' is selected, clears all selected values
* - For other conditions, preserves existing selected values
*/
const handleUpdateCondition = (type: string, condition: string) => {
setActiveFilters(prevFilters =>
prevFilters.map(filter => {
if (filter.type === type) {
return {
...filter,
condition,
selectedValues: condition === 'is_empty' ? [] : filter.selectedValues
}
}
return filter
})
)
}

/**
* Removes a filter from the active filters list
* @param type - The type of filter to remove (e.g., 'type', 'language')
*
* Filters out the specified filter type while keeping all others
*/
const handleRemoveFilter = (type: string) => {
setActiveFilters(prevFilters => prevFilters.filter(filter => filter.type !== type))
}

const handleResetFilters = useCallback(() => {
setActiveFilters([])
}, [])

// SORTS
/**
* Handles adding a new sort to the active sorts list
* @param newSort - Sort object containing:
* - type: string - The type of sort (e.g., 'updated', 'stars')
* - direction: string - The direction of sort (will be initialized as 'asc')
*
* Only adds the sort if one with the same type doesn't already exist
*/
const handleSortChange = (newSort: SortValue) => {
// Indicate which filter should be opened
setFilterToOpen({ type: newSort.type, kind: 'sort' })

if (!activeSorts.find(sort => sort.type === newSort.type)) {
setActiveSorts([...activeSorts, newSort])
}
}

/**
* Updates a specific sort in the active sorts list
* @param index - The index of the sort to update
* @param updatedSort - The new sort object to replace the existing one
*
* Maps through all sorts and updates only the matching one
* while preserving other sorts unchanged
*/
const handleUpdateSort = (index: number, updatedSort: SortValue) => {
setActiveSorts(activeSorts.map((sort, i) => (i === index ? updatedSort : sort)))
}

/**
* Removes a sort from the active sorts list
* @param index - The index of the sort to remove
*
* Filters out the specified sort index while keeping all others
*/
const handleRemoveSort = (index: number) => {
setActiveSorts(activeSorts.filter((_, i) => i !== index))
}

const handleReorderSorts = (newSorts: SortValue[]) => {
setActiveSorts(newSorts)
}

const handleResetSorts = () => {
setActiveSorts([])
}

const handleResetAll = useCallback(() => {
setActiveFilters([])
setActiveSorts([])
setSearchQueries({ filters: {}, menu: {} })
}, [])

// SEARCH
const handleSearchChange = (type: string, query: string, searchType: keyof FilterSearchQueries) => {
setSearchQueries(prev => ({
...prev,
[searchType]: {
...prev[searchType],
[type]: query
}
}))
}

const clearSearchQuery = (type: string, searchType: keyof FilterSearchQueries) => {
setSearchQueries(prev => ({
...prev,
[searchType]: {
...prev[searchType],
[type]: ''
}
}))
}

// Clear the filter to open
const clearFilterToOpen = () => {
setFilterToOpen(null)
}

return {
// Filters
activeFilters,
activeSorts,
setActiveFilters,
setActiveSorts,
searchQueries,
filterToOpen,
handleFilterChange,
handleUpdateFilter,
handleUpdateCondition,
handleRemoveFilter,
handleResetFilters,
clearFilterToOpen,
// Sorts
handleSortChange,
handleUpdateSort,
handleRemoveSort,
handleReorderSorts,
handleResetSorts,
handleResetAll,
// Search
handleSearchChange,
clearSearchQuery
}
}
18 changes: 17 additions & 1 deletion packages/ui/src/views/user-management/user-management-page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useMemo } from 'react'

import { Button, ListActions, PaginationComponent, SearchBox, Spacer, Text } from '@/components'
import { Button, Filters, FiltersBar, ListActions, PaginationComponent, SearchBox, Spacer, Text } from '@/components'
import { SandboxLayout } from '@/views'
import { getFilterOptions, getSortDirections, getSortOptions } from '@views/repo/constants/filter-options'
import { useFilters } from '@views/repo/hooks'

import { UserManagementTabs } from './components/tabs'
import { UsersList } from './components/users-list'
Expand Down Expand Up @@ -30,6 +32,12 @@ export const UserManagementPage: React.FC<IUserManagementPageProps> = ({
} = useAdminListUsersStore()
const { t } = useTranslationStore()

const FILTER_OPTIONS = getFilterOptions(t)
const SORT_OPTIONS = getSortOptions(t)
const SORT_DIRECTIONS = getSortDirections(t)

const filterHandlers = useFilters()

const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(event.target.value)
}
Expand Down Expand Up @@ -68,11 +76,19 @@ export const UserManagementPage: React.FC<IUserManagementPageProps> = ({
/>
</ListActions.Left>
<ListActions.Right>
<Filters filterOptions={FILTER_OPTIONS} sortOptions={SORT_OPTIONS} filterHandlers={filterHandlers} t={t} />
<Button variant="default" onClick={() => handleDialogOpen(null, DialogLabels.CREATE_USER)}>
{t('views:userManagement.newUserButton', 'New user')}
</Button>
</ListActions.Right>
</ListActions.Root>
<FiltersBar
filterOptions={FILTER_OPTIONS}
sortOptions={SORT_OPTIONS}
sortDirections={SORT_DIRECTIONS}
filterHandlers={filterHandlers}
t={t}
/>
<Spacer size={5} />
{renderUserListContent()}
<Spacer size={8} />
Expand Down

0 comments on commit ba97acd

Please sign in to comment.