Skip to content

Commit

Permalink
React Admin - Search/Filters/Ordering for Post/Competition/Semester/S…
Browse files Browse the repository at this point in the history
…eries/Problems (#510)

* add Dark mode toggle button

* unrelated: redundant style

* basic Post/Problem search+filter+sort

* `yarn dedupe` to fix MUI styles

* global medium buttons, `yarn add @mui/utils@^5`, `yarn dedupe`

* search+filter+sort for Competition, Semester, Series, Problems

* filter CompetitionFilterSection based on `competition_type: 0`

* open FilterSidebar by default
  • Loading branch information
rtrembecky authored Dec 14, 2024
1 parent dca4ace commit 4ba507f
Show file tree
Hide file tree
Showing 19 changed files with 364 additions and 1,789 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@emotion/styled": "^11.13.5",
"@mui/icons-material": "^5.16.7",
"@mui/material": "^5.16.7",
"@mui/utils": "^5",
"@tanstack/react-query": "5.61.0",
"@tanstack/react-query-devtools": "5.61.0",
"@testing-library/jest-dom": "^5.17.0",
Expand Down
20 changes: 18 additions & 2 deletions src/components/Admin/Admin.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {deepmerge} from '@mui/utils'
import {FC} from 'react'
import {Admin as ReactAdmin, Resource} from 'react-admin'
import {Admin as ReactAdmin, defaultDarkTheme, defaultLightTheme, RaThemeOptions, Resource} from 'react-admin'

import {AdminLayout} from './AdminLayout'
import {dataProvider} from './dataProvider'
Expand Down Expand Up @@ -48,15 +49,30 @@ import {SchoolList} from './resources/personal/schools/SchoolList'
import {SchoolShow} from './resources/personal/schools/SchoolShow'
import {useAuthProvider} from './useAuthProvider'

const themeOverrides: RaThemeOptions = {
components: {
RaButton: {
defaultProps: {
size: 'medium',
},
},
},
}

const lightTheme = deepmerge(defaultLightTheme, themeOverrides)
const darkTheme = deepmerge(defaultDarkTheme, themeOverrides)

export const Admin: FC = () => {
const authProvider = useAuthProvider()

return (
<ReactAdmin
authProvider={authProvider}
dataProvider={dataProvider}
layout={AdminLayout}
i18nProvider={myI18nProvider}
layout={AdminLayout}
theme={lightTheme}
darkTheme={darkTheme}
>
<Resource name="cms/post" list={PostList} edit={PostEdit} show={PostShow} create={PostCreate} />
<Resource
Expand Down
4 changes: 3 additions & 1 deletion src/components/Admin/AdminLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {Home, Logout} from '@mui/icons-material/'
import {Button, Stack, Typography} from '@mui/material'
import {useRouter} from 'next/router'
import {FC, PropsWithChildren} from 'react'
import {AppBar, Layout, useLogout, useTranslate} from 'react-admin'
import {AppBar, Layout, ToggleThemeButton, useLogout, useTranslate} from 'react-admin'

const AppMenuBar = () => {
const router = useRouter()
Expand All @@ -21,6 +21,8 @@ const AppMenuBar = () => {
</Stack>
</Button>

<ToggleThemeButton />

<Button color="inherit" onClick={() => logout()}>
<Stack gap={1} direction="row">
<Logout />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {FC} from 'react'
import {AutocompleteInput, FilterListSection, FilterLiveForm, ReferenceInput} from 'react-admin'

export const CompetitionFilterSection: FC = () => {
return (
<FilterListSection label="Competition" icon={null}>
<FilterLiveForm>
<ReferenceInput source="competition" reference="competition/competition" filter={{competition_type: 0}}>
<AutocompleteInput helperText={false} />
</ReferenceInput>
</FilterLiveForm>
</FilterListSection>
)
}
31 changes: 31 additions & 0 deletions src/components/Admin/custom/list-filtering/FilterSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {FilterList as FilterListIcon} from '@mui/icons-material'
import {Button, Card, CardContent, Stack} from '@mui/material'
import {FC, PropsWithChildren, useState} from 'react'
import {FilterLiveSearch} from 'react-admin'

export const FilterSidebar: FC<PropsWithChildren> = ({children}) => {
const [filterOpen, setFilterOpen] = useState(true)
const toggleFilter = () => setFilterOpen((prev) => !prev)

return (
<Stack
sx={{
order: -1,
position: 'relative',
overflow: 'visible',
}}
>
<Button onClick={toggleFilter} sx={{mt: 0.5, position: 'absolute', width: 'max-content', gap: 1}}>
<FilterListIcon />
Filters
</Button>

<Card sx={{mt: 8, mr: filterOpen ? 2 : 0, width: filterOpen ? 200 : 0, transition: 'width 0.2s'}}>
<CardContent>
<FilterLiveSearch />
{children}
</CardContent>
</Card>
</Stack>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {FC} from 'react'
import {AutocompleteInput, FilterListSection, FilterLiveForm, ReferenceInput, useListContext} from 'react-admin'

export const SemesterFilterSection: FC = () => {
const {filterValues} = useListContext()

return (
<FilterListSection label="Semester" icon={null}>
<FilterLiveForm>
<ReferenceInput
source="semester"
reference="competition/semester"
filter={{competition: filterValues.competition}}
>
<AutocompleteInput helperText={false} />
</ReferenceInput>
</FilterLiveForm>
</FilterListSection>
)
}
20 changes: 20 additions & 0 deletions src/components/Admin/custom/list-filtering/SeriesFilterSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {FC} from 'react'
import {AutocompleteInput, FilterListSection, FilterLiveForm, ReferenceInput, useListContext} from 'react-admin'

export const SeriesFilterSection: FC = () => {
const {filterValues} = useListContext()

return (
<FilterListSection label="Séria" icon={null}>
<FilterLiveForm>
<ReferenceInput
source="series"
reference="competition/series"
filter={{competition: filterValues.competition, semester: filterValues.semester}}
>
<AutocompleteInput helperText={false} />
</ReferenceInput>
</FilterLiveForm>
</FilterListSection>
)
}
81 changes: 18 additions & 63 deletions src/components/Admin/dataProvider.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,42 @@
import axios from 'axios'
import {stringify} from 'querystring'
import {DataProvider, RaRecord} from 'react-admin'
// TODO: BE chysta search, filter, pagination a sort. ked to bude ready,
// postupne odkomentujeme tento kod a zmazeme client-side handling nizsie
// import {FilterPayload, PaginationPayload, SortPayload} from 'react-admin'

import {DataProvider, FilterPayload, /* PaginationPayload, */ RaRecord, SortPayload} from 'react-admin'

Check warning on line 3 in src/components/Admin/dataProvider.ts

View workflow job for this annotation

GitHub Actions / branch-test

'RaRecord' is defined but never used

const getFilterQuery = ({q, ...otherSearchParams}: FilterPayload) => ({
...otherSearchParams,
search: q,
})
const getOrderingQuery = ({order, field}: SortPayload) => ({ordering: `${order === 'ASC' ? '' : '-'}${field}`})
// TODO: pagination podla BE
// const getPaginationQuery = ({page, perPage}: PaginationPayload) => ({
// offset: page,
// limit: perPage,
// })
// const getFilterQuery = ({q, ...otherSearchParams}: FilterPayload) => ({
// ...otherSearchParams,
// search: q,
// })
// const getOrderingQuery = ({field, order}: SortPayload) => ({
// ordering: `${order === 'ASC' ? '' : '-'}${field}`,
// })

const dynamicSort = (key: string, order: string) => {
const orderValue = order === 'ASC' ? 1 : -1
return (a: RaRecord, b: RaRecord) => {
if (a[key] > b[key]) return orderValue
if (a[key] < b[key]) return -orderValue
return 0
}
}

const apiUrl = '/api'

// skopirovane a dost upravene z https://github.com/bmihelac/ra-data-django-rest-framework/blob/master/src/index.ts
export const dataProvider: DataProvider = {
getList: async (resource, params) => {
getList: async (resource, {filter, sort, pagination}) => {
const query = {
// TODO: ked BE bude mat pagination, filter alebo sort
// ...getFilterQuery(params.filter),
// ...getPaginationQuery(params.pagination),
// ...getOrderingQuery(params.sort),
...(filter ? getFilterQuery(filter) : {}),
...(sort ? getOrderingQuery(sort) : {}),
// TODO: pagination podla BE
// ...getPaginationQuery(pagination),
}
const stringifiedQuery = stringify(query)
const {data} = await axios.get<any[]>(`${apiUrl}/${resource}${stringifiedQuery ? `/?${stringifiedQuery}` : ''}`)

Check warning on line 28 in src/components/Admin/dataProvider.ts

View workflow job for this annotation

GitHub Actions / branch-test

Unexpected any. Specify a different type

// client-side filter
let filteredData = data
if (params.filter) {
const {q: search, ...rest} = params.filter
if (search) {
// vyhladava to filter string vo vsetkych fieldoch kazdeho recordu
// - bohuzial tie fieldy su casto len IDcka inych modelov, tak nic moc :D
filteredData = data.filter((record: RaRecord) => {
const matches = Object.values(record).some((value) => {
return value && JSON.stringify(value).toLowerCase().includes(search.toLowerCase())
})
return matches
})
}

if (rest) {
filteredData = filteredData.filter((record: RaRecord) => {
return Object.entries(rest).every(([key, value]) => {
if (!value) return true
return record[key] === value
})
})
}
}

// client-side sort
if (params.sort) {
const {field, order} = params.sort

filteredData.sort(dynamicSort(field, order))
}

// client-side pagination
let pagedData = filteredData
if (params.pagination) {
const {page, perPage} = params.pagination
pagedData = filteredData.slice((page - 1) * perPage, page * perPage)
let pagedData = data
if (pagination) {
const {page, perPage} = pagination
pagedData = data.slice((page - 1) * perPage, page * perPage)
}

return {
data: pagedData,
total: filteredData.length,
total: data.length,
}
},
getOne: async (resource, params) => {
Expand Down
30 changes: 23 additions & 7 deletions src/components/Admin/resources/cms/post/PostList.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
import {FC} from 'react'
import {Datagrid, FunctionField, List, RaRecord, TextField} from 'react-admin'
import {Datagrid, FilterList, FilterListItem, FunctionField, List, RaRecord, TextField} from 'react-admin'

import {DateTimeField} from '@/components/Admin/custom/DateTimeField'
import {FilterSidebar} from '@/components/Admin/custom/list-filtering/FilterSidebar'
import {SitesArrayField} from '@/components/Admin/custom/SitesArrayField'
import {TruncatedTextField} from '@/components/Admin/custom/TruncatedTextField'
import {seminarIds, seminarIdToName} from '@/utils/useSeminarInfo'

export const PostList: FC = () => (
<List>
<List aside={<PostListFilters />}>
<Datagrid>
<TextField source="caption" />
<TruncatedTextField source="short_text" maxTextWidth={50} />
<TruncatedTextField source="details" maxTextWidth={50} />
<TextField source="caption" sortable={false} />
<TruncatedTextField source="short_text" maxTextWidth={50} sortable={false} />
<TruncatedTextField source="details" maxTextWidth={50} sortable={false} />
<DateTimeField source="added_at" />
<DateTimeField source="visible_after" />
<DateTimeField source="visible_until" />
<SitesArrayField source="sites" />
<FunctionField<RaRecord> source="links" render={(record) => record && <span>{record['links'].length}</span>} />
<SitesArrayField source="sites" sortable={false} />
<FunctionField<RaRecord>
source="links"
render={(record) => record && <span>{record['links'].length}</span>}
sortable={false}
/>
</Datagrid>
</List>
)

const PostListFilters: FC = () => (
<FilterSidebar>
<FilterList label="Seminár" icon={null}>
{seminarIds.map((seminarId) => (
<FilterListItem key={seminarId} label={seminarIdToName[seminarId]} value={{sites: seminarId}} />
))}
</FilterList>
</FilterSidebar>
)
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import {FC} from 'react'
import {Datagrid, FunctionField, List, NumberField, RaRecord, TextField} from 'react-admin'

import {FilterSidebar} from '@/components/Admin/custom/list-filtering/FilterSidebar'
import {SitesArrayField} from '@/components/Admin/custom/SitesArrayField'
import {TruncatedTextField} from '@/components/Admin/custom/TruncatedTextField'

export const CompetitionList: FC = () => (
<List>
<List aside={<CompetitionListFilters />}>
<Datagrid>
<TextField source="name" />
<TextField source="slug" />
<TextField source="slug" sortable={false} />
<TextField source="start_year" />
<TruncatedTextField source="description" maxTextWidth={30} />
<TruncatedTextField source="rules" maxTextWidth={30} />
<TextField source="competition_type.name" label="content.labels.competition_type" />
<SitesArrayField source="sites" />
<TextField source="who_can_participate" />
<NumberField source="min_years_until_graduation" />
<TruncatedTextField source="description" maxTextWidth={30} sortable={false} />
<TruncatedTextField source="rules" maxTextWidth={30} sortable={false} />
<TextField source="competition_type.name" label="content.labels.competition_type" sortable={false} />
<SitesArrayField source="sites" sortable={false} />
<TextField source="who_can_participate" sortable={false} />
<NumberField source="min_years_until_graduation" sortable={false} />
<FunctionField<RaRecord>
source="history_events"
label="content.labels.history_events_count"
render={(record) => record && <span>{record['history_events'].length}</span>}
sortable={false}
/>
</Datagrid>
</List>
)

const CompetitionListFilters: FC = () => <FilterSidebar />
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export const EventRegistrationList: FC = () => (
</Datagrid>
</List>
)

// TODO: filtre a ordering podla https://github.com/ZdruzenieSTROM/webstrom-backend/pull/460/files#diff-148e08b739e60a78edfc1e546340f501840b75f1646afa58ee524ff82cfc061eR905-R908
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export const EventList: FC = () => (
</Datagrid>
</List>
)

// TODO: filtre a ordering podla https://github.com/ZdruzenieSTROM/webstrom-backend/pull/460/files#diff-148e08b739e60a78edfc1e546340f501840b75f1646afa58ee524ff82cfc061eR832-R838
Loading

0 comments on commit 4ba507f

Please sign in to comment.