Skip to content

Commit

Permalink
ucfopen#2000 Module library page now hydrates to enable live filtrati…
Browse files Browse the repository at this point in the history
…on. Module/collection filter functions were moved into their own file to be shared easily between components. Added a function to the collection backend model to enable looking up all public collections, whenever that becomes necessary.
  • Loading branch information
FrenjaminBanklin committed Jan 12, 2023
1 parent 63e28c3 commit 0bbf5a6
Show file tree
Hide file tree
Showing 13 changed files with 327 additions and 69 deletions.
3 changes: 2 additions & 1 deletion packages/app/obojobo-repository/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ module.exports = {
dashboard: 'shared/components/pages/page-dashboard-client.jsx',
stats: 'shared/components/pages/page-stats-client.jsx',
homepage: 'shared/components/pages/page-homepage.jsx',
'page-module': 'shared/components/pages/page-module-client.jsx'
'page-module': 'shared/components/pages/page-module-client.jsx',
'page-library': 'shared/components/pages/page-library-client.jsx'
}
}
}
19 changes: 19 additions & 0 deletions packages/app/obojobo-repository/server/models/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@ class Collection {
this.createdAt = created_at
}

static fetchAllPublic() {
return db
.manyOrNone(
`
SELECT
id,
title,
user_id,
created_at
FROM repository_collections
WHERE visibility_type = 'public'
AND deleted = FALSE
`
)
.then(result => {
return result.map(row => new Collection(row))
})
}

static fetchById(id) {
return db
.one(
Expand Down
21 changes: 21 additions & 0 deletions packages/app/obojobo-repository/server/models/collection.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ describe('Collection Model', () => {
expect(c.createdAt).toBe(mockRawCollection.created_at)
})

test('fetchAllPublic retrieves all public collections from the database', () => {
expect.hasAssertions()

const mockDbResponse = [
{ ...mockRawCollection },
{ ...mockRawCollection, id: 'mockCollectionId2', title: 'mockCollectionTitle2' }
]
db.manyOrNone.mockResolvedValueOnce(mockDbResponse)

return CollectionModel.fetchAllPublic().then(collections => {
expect(collections.length).toBe(2)
for (let c = 0; c < collections.length; c++) {
expect(collections[c]).toBeInstanceOf(CollectionModel)
expect(collections[c].id).toBe(mockDbResponse[c].id)
expect(collections[c].title).toBe(mockDbResponse[c].title)
expect(collections[c].userId).toBe(mockDbResponse[c].user_id)
expect(collections[c].createdAt).toBe(mockDbResponse[c].created_at)
}
})
})

test('fetchById retrieves a Collection from the database', () => {
expect.hasAssertions()

Expand Down
8 changes: 6 additions & 2 deletions packages/app/obojobo-repository/server/routes/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ router
.route('/library')
.get(getCurrentUser)
.get((req, res) => {
// when allowing for multiple public collections, replace this
// with a call to 'Collection.fetchAllPublic' followed by
// Promise.all() using collections.map(c => (c.loadRelatedDrafts()))
return Collection.fetchById(publicLibCollectionId)
.then(collection => {
return collection.loadRelatedDrafts()
Expand All @@ -122,9 +125,10 @@ router
pageCount: 1,
currentUser: req.currentUser,
// must use webpackAssetPath for all webpack assets to work in dev and production!
appCSSUrl: webpackAssetPath('repository.css')
appCSSUrl: webpackAssetPath('repository.css'),
appJsUrl: webpackAssetPath('page-library.js')
}
res.render('pages/page-library.jsx', props)
res.render('pages/page-library-server.jsx', props)
})
.catch(res.unexpected)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ describe('repository library route', () => {

expect(response.header['content-type']).toContain('text/html')
expect(response.statusCode).toBe(200)
expectPageTitleToBe(response, 'Module Library')
expectPageTitleToBe(response, 'Obojobo Module Library')
})
})

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { hydrateElWithoutStore } from '../../react-utils'
import PageLibrary from './page-library.jsx'

hydrateElWithoutStore(PageLibrary, '#react-hydrate-root')
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const React = require('react')
const DefaultLayout = require('../layouts/default')
const { convertPropsToString } = require('../../react-utils')

const PageLibraryServer = props => {
return (
<DefaultLayout
title={`Obojobo Module Library`}
appScriptUrl={props.appJsUrl}
appCSSUrl={props.appCSSUrl}
>
<span id="react-hydrate-root" data-react-props={convertPropsToString(props)} />
</DefaultLayout>
)
}

module.exports = PageLibraryServer
Original file line number Diff line number Diff line change
@@ -1,54 +1,76 @@
require('./page-library.scss')

const React = require('react')
import LayoutDefault from '../layouts/default'
import RepositoryNav from '../repository-nav'
import RepositoryBanner from '../repository-banner'
import Module from '../module'

const Search = require('../search')

const { filterModules } = require('../../util/filter-functions')

const title = 'Module Library'

const PageLibrary = props => (
<LayoutDefault
title={title}
className="repository--library"
appCSSUrl={props.appCSSUrl /* provided by resp.render() */}
>
<RepositoryNav
userId={props.currentUser.id}
userPerms={props.currentUser.perms}
avatarUrl={props.currentUser.avatarUrl}
displayName={`${props.currentUser.firstName} ${props.currentUser.lastName}`}
noticeCount={0}
/>
<RepositoryBanner className="default-bg" title={title} />

<div className="repository--section-wrapper">
<section className="repository--main-content">
<p>Find modules for your course.</p>
{props.collections.map(collection => (
<span key={collection.id}>
<div className="repository--main-content--title">
<span>{collection.title}</span>
</div>
<div className="repository--item-list--collection">
<div className="repository--item-list--collection--item-wrapper">
<div className="repository--item-list--row">
<div className="repository--item-list--collection--item--multi-wrapper">
{collection.drafts.map(draft => (
<Module key={draft.draftId} {...draft}></Module>
))}
</div>
const PageLibrary = props => {
const [filterString, setFilterString] = React.useState('')

const filteredDisplay = props.collections.map(collection => {
const visibleModulesInCollection = filterModules(collection.drafts, filterString, false)
const modulesInCollectionRender = visibleModulesInCollection.map(draft => (
<Module key={draft.draftId} {...draft}></Module>
))

if (visibleModulesInCollection.length) {
return (
<span key={collection.id}>
<div className="repository--main-content--title">
<span>{collection.title}</span>
</div>
<div className="repository--item-list--collection">
<div className="repository--item-list--collection--item-wrapper">
<div className="repository--item-list--row">
<div className="repository--item-list--collection--item--multi-wrapper">
{modulesInCollectionRender}
</div>
</div>
</div>
</span>
))}
</section>
</div>
</LayoutDefault>
)
</div>
</span>
)
}
return null
})

return (
<LayoutDefault
title={title}
className="repository--library"
appCSSUrl={props.appCSSUrl /* provided by resp.render() */}
>
<RepositoryNav
userId={props.currentUser.id}
userPerms={props.currentUser.perms}
avatarUrl={props.currentUser.avatarUrl}
displayName={`${props.currentUser.firstName} ${props.currentUser.lastName}`}
noticeCount={0}
/>
<RepositoryBanner className="default-bg" title={title} />

<div className="repository--section-wrapper">
<section className="repository--main-content">
<p>Find modules for your course.</p>
<Search value={filterString} placeholder="Filter Modules..." onChange={setFilterString} />
{filteredDisplay}
</section>
</div>
</LayoutDefault>
)
}

PageLibrary.defaultProps = {
collections: []
}

module.exports = PageLibrary
// module.exports = PageLibrary
export default PageLibrary
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// reduce a larger font-size set on an ancestor in the library page
.repository--nav--links--search {
font-size: 0.9em;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PageLibrary from './page-library'
import { shallow } from 'enzyme'

const Module = require('../module')
const Search = require('../search')

describe('PageLibrary', () => {
const mockCurrentUser = {
Expand All @@ -20,7 +21,7 @@ describe('PageLibrary', () => {
expect(mainContentChild.find('span').length).toBe(0)
})

test('renders collections correctly based on props', () => {
test('renders correctly when collections are provided but contain no drafts', () => {
const mockCollections = [
{
id: 'mockCollectionId',
Expand All @@ -38,6 +39,28 @@ describe('PageLibrary', () => {
<PageLibrary currentUser={mockCurrentUser} collections={mockCollections} />
)

const mainContentSpans = component.find('.repository--main-content > span')
expect(mainContentSpans.length).toBe(0)
})

test('renders correctly when collections are provided and contain drafts', () => {
const mockCollections = [
{
id: 'mockCollectionId',
title: 'mockCollectionTitle',
drafts: [{ draftId: 'mockDraftId', title: 'mockDraft' }]
},
{
id: 'mockCollectionId2',
title: 'mockCollectionTitle2',
drafts: [{ draftId: 'mockDraftId2', title: 'mockDraft2' }]
}
]

const component = shallow(
<PageLibrary currentUser={mockCurrentUser} collections={mockCollections} />
)

const mainContentSpans = component.find('.repository--main-content > span')
expect(mainContentSpans.length).toBe(2)
expect(mainContentSpans.at(0).key()).toBe('mockCollectionId')
Expand All @@ -50,9 +73,9 @@ describe('PageLibrary', () => {
id: 'mockCollectionId',
title: 'mockCollectionTitle',
drafts: [
{ draftId: 'mockDraftId' },
{ draftId: 'mockDraftId2' },
{ draftId: 'mockDraftId3' }
{ draftId: 'mockDraftId', title: 'mockDraft' },
{ draftId: 'mockDraftId2', title: 'mockDraft2' },
{ draftId: 'mockDraftId3', title: 'mockDraft3' }
]
}
]
Expand All @@ -71,4 +94,46 @@ describe('PageLibrary', () => {
expect(moduleComponents.at(1).key()).toBe('mockDraftId2')
expect(moduleComponents.at(2).key()).toBe('mockDraftId3')
})

test('renders collections and drafts correctly based on applied filters', () => {
const mockCollections = [
{
id: 'mockCollectionId',
title: 'mockCollectionTitle',
drafts: [{ draftId: 'mockDraftId', title: 'mockDraft1' }]
},
{
id: 'mockCollectionId2',
title: 'mockCollectionTitle2',
drafts: [{ draftId: 'mockDraftId2', title: 'mockDraft2' }]
}
]

const component = shallow(
<PageLibrary currentUser={mockCurrentUser} collections={mockCollections} />
)

// everything should be visible by default
let mainContentSpans = component.find('.repository--main-content > span')
expect(mainContentSpans.length).toBe(2)
expect(mainContentSpans.at(0).key()).toBe('mockCollectionId')
expect(mainContentSpans.at(1).key()).toBe('mockCollectionId2')

let firstCollectionModules = mainContentSpans.at(0).find(Module)
expect(firstCollectionModules.at(0).key()).toBe('mockDraftId')

const secondCollectionModules = mainContentSpans.at(1).find(Module)
expect(secondCollectionModules.at(0).key()).toBe('mockDraftId2')

// change the filter string to '2'
const searchComponent = component.find(Search)
searchComponent.props().onChange('2')
// since none of the modules in the first collection have '2' in the title, it should not render
mainContentSpans = component.find('.repository--main-content > span')
expect(mainContentSpans.length).toBe(1)
expect(mainContentSpans.at(0).key()).toBe('mockCollectionId2')

firstCollectionModules = mainContentSpans.at(0).find(Module)
expect(firstCollectionModules.at(0).key()).toBe('mockDraftId2')
})
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
const { handle } = require('redux-pack')

const whitespaceRegex = /\s+/g

const {
SHOW_MODULE_PERMISSIONS,
CLOSE_MODAL,
Expand Down Expand Up @@ -43,6 +41,8 @@ const {
BULK_RESTORE_MODULES
} = require('../actions/dashboard-actions')

const { filterModules, filterCollections } = require('../util/filter-functions')

const searchPeopleResultsState = (isFetching = false, hasFetched = false, items = []) => ({
items,
hasFetched,
Expand All @@ -65,27 +65,6 @@ const closedDialogState = () => ({
}
})

function filterModules(modules, searchString) {
searchString = ('' + searchString).replace(whitespaceRegex, '').toLowerCase()

return modules.filter(m =>
((m.title || '') + m.draftId)
.replace(whitespaceRegex, '')
.toLowerCase()
.includes(searchString)
)
}
function filterCollections(collections, searchString) {
searchString = ('' + searchString).replace(whitespaceRegex, '').toLowerCase()

return collections.filter(c =>
((c.title || '') + c.id)
.replace(whitespaceRegex, '')
.toLowerCase()
.includes(searchString)
)
}

function DashboardReducer(state, action) {
switch (action.type) {
case CREATE_NEW_COLLECTION:
Expand Down
Loading

0 comments on commit 0bbf5a6

Please sign in to comment.