Skip to content

Commit

Permalink
feat: add Researcher Classes filters and table [PT-187185134] [PT-187…
Browse files Browse the repository at this point in the history
  • Loading branch information
pjanik committed Mar 22, 2024
1 parent 81ea50c commit 9c20315
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 15 deletions.
16 changes: 8 additions & 8 deletions rails/app/assets/javascripts/react-components.js

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions rails/app/assets/javascripts/react-test-globals.js

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions rails/app/views/admin/projects/classes.haml
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
%div
%h1 Research Classes: #{@project.name}

#form-container

:javascript
PortalComponents.renderResearcherClassesForm({projectId: #{@project.id}}, "form-container")
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import React from 'react'
import Select from 'react-select'
import jQuery from 'jquery'
import ResearcherClassesTable from './table'

import 'react-day-picker/lib/style.css'
import css from './style.scss'

const title = str => (str.charAt(0).toUpperCase() + str.slice(1)).replace(/_/g, ' ')

const queryCache = {}

export default class ResearcherClassesForm extends React.Component {
constructor (props) {
super(props)
this.state = {
// the current values of the filters
teachers: [],
cohorts: [],
runnables: [],
// all possible values for each pulldown
filterables: {
teachers: [],
cohorts: [],
runnables: [],
classes: []
},
// waiting for results
waitingFor_teachers: false,
waitingFor_cohorts: false,
waitingFor_runnables: false,
waitingFor_classes: false,
totals: {},
// checkbox options
removeCCTeachers: false,
queryParams: {}
}
}

// eslint-disable-next-line
UNSAFE_componentWillMount () {
this.getTotals()
}

getTotals () {
jQuery.ajax({
url: '/api/v1/researcher_classes',
type: 'GET',
data: { totals: true, remove_cc_teachers: this.state.removeCCTeachers, project_id: this.props.projectId }
}).then(data => {
if (data.error) {
window.alert(data.error)
}
if (data.totals) {
this.setState({ totals: data.totals })
}
})
}

query (_params, _fieldName, searchString) {
if (_fieldName) {
this.setState({ [`waitingFor_${_fieldName}`]: true })
}
const params = jQuery.extend({}, _params) // clone
if (_fieldName) {
// we remove the value of each field from the filter query for that
// dropdown, as we want to know all possible values for that dropdown
// given only the other filters
delete params[_fieldName]
}
if (searchString) {
params[_fieldName] = searchString
}

const cacheKey = JSON.stringify(params)

const handleResponse = (fieldName => {
return data => {
let newState = { filterables: this.state.filterables }

queryCache[cacheKey] = data

let hits = data.hits && data.hits[fieldName] ? data.hits[fieldName] : []
if (searchString) {
// merge results and remove dups
let merged = (newState.filterables[fieldName] || []).concat(hits)
newState.filterables[fieldName] = merged.filter((str, i) => merged.indexOf(str) === i)
} else {
newState.filterables[fieldName] = hits
}

newState.filterables[fieldName].sort((a, b) => a.label.localeCompare(b.label))

newState[`waitingFor_${_fieldName}`] = false
this.setState(newState)
return data
}
})(_fieldName)

if ((queryCache[cacheKey] != null ? queryCache[cacheKey].then : undefined)) { // already made a Promise that is still pending
queryCache[cacheKey].then(handleResponse) // chain a new Then
} else if (queryCache[cacheKey]) { // have data that has already returned
handleResponse(queryCache[cacheKey]) // use it directly
} else {
queryCache[cacheKey] = jQuery.ajax({ // make req and add new Promise to cache
url: '/api/v1/researcher_classes',
type: 'GET',
data: params
}).then(handleResponse)
}
}

getQueryParams () {
const params = { remove_cc_teachers: this.state.removeCCTeachers, project_id: this.props.projectId }
for (var filter of ['teachers', 'cohorts', 'runnables']) {
if ((this.state[filter] != null ? this.state[filter].length : undefined) > 0) {
params[filter] = this.state[filter].map(v => v.value).sort().join(',')
}
}
return params
}

updateFilters () {
const params = this.getQueryParams()
this.query(params)
this.query(params, 'teachers')
this.query(params, 'cohorts')
this.query(params, 'runnables')
this.query(params, 'classes')
}

renderInput (name, titleOverride) {
if (!this.state.filterables[name]) { return }

const hits = this.state.filterables[name]

const isLoading = this.state[`waitingFor_${name}`]
const placeholder = !isLoading ? (hits.length === 0 ? 'Search...' : 'Select or search...') : 'Loading ...'

const options = hits.map(hit => {
return { value: hit.id, label: hit.label }
})

const handleSelectInputChange = value => {
if (value.length === 4) {
const params = this.getQueryParams()
this.query(params, name, value)
}
}

const handleSelectChange = value => {
this.setState({ [name]: value }, () => {
this.updateFilters()
})
}

const handleLoadAll = e => {
e.preventDefault()
this.query({ load_all: name, remove_cc_teachers: this.state.removeCCTeachers, project_id: this.props.projectId }, name)
}

const titleCounts = this.state.totals.hasOwnProperty(name) ? ` (${hits.length} of ${this.state.totals[name]})` : ''
let loadAllLink
if ((this.state.totals[name] > 0) && (hits.length !== this.state.totals[name])) {
loadAllLink = <a href='#' onClick={handleLoadAll} style={{ marginLeft: 10 }}>load all</a>
}

return (
<div style={{ marginTop: '6px' }}>
<span>{`${titleOverride || title(name)}${titleCounts}`}{loadAllLink}</span>
<Select
name={name}
options={options}
isMulti
placeholder={placeholder}
isLoading={isLoading}
value={this.state[name]}
onInputChange={handleSelectInputChange}
onChange={handleSelectChange}
/>
</div>
)
}

renderForm () {
const handleRemoveCCTeachers = e => {
this.setState({ removeCCTeachers: e.target.checked }, () => {
this.getTotals()
this.updateFilters()
})
}

return (
<form method='get'>
{this.renderInput('cohorts')}
{this.renderInput('teachers')}
<div style={{ marginTop: '6px' }}>
<input type='checkbox' checked={this.state.removeCCTeachers} onChange={handleRemoveCCTeachers} /> Remove Concord Consortium Teachers? *
</div>
{this.renderInput('runnables', 'Resources')}

<div style={{ marginTop: '24px' }}>
* Concord Consortium Teachers belong to schools named "Concord Consortium".
</div>
</form>
)
}

render () {
const classes = this.state.filterables.classes

return (
<div className={css.researcherClassesForm}>
{this.renderForm()}
{
classes.length > 0 &&
<ResearcherClassesTable classes={classes} />
}
</div>
)
}
}

ResearcherClassesForm.defaultProps = {
projectId: ''
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.researcherClassesForm {
input[type="submit"] {
margin: 10px 10px 0 0;
}

input[type="checkbox"] {
margin: 15px 10px 0 0;
}
}

.researcherClassesTable {
hr {
background-color: #444;
}

table {
width: 100%;
border: 1px solid #444;

th {
background-color: #cdcdcd;
text-align: left;
}
tr:nth-child(even) {
background-color: #efefef;
}
tr:nth-child(odd) {
background-color: #fff;
}
td {
text-align: left;
}
td + td, th + th {
border-left: 1px solid #444;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react'
import 'react-day-picker/lib/style.css'
import css from './style.scss'

export default class ResearcherClassesTable extends React.Component {
render () {
const { classes } = this.props
if (classes.length === 0) {
return null
}
return (
<div className={css.researcherClassesTable}>
<hr />
<div>Results</div>

<table>
<thead>
<tr>
<th>Cohort</th>
<th>Teacher</th>
<th>Class</th>
<th />
</tr>
</thead>
<tbody>
{
classes.map((c, i) => (
<tr key={i}>
<td>{c.cohort_names}</td>
<td>{c.teacher_names}</td>
<td>{c.name}</td>
<td><a href={c.class_url} target='_blank'>View Class</a></td>
</tr>
))
}
</tbody>
</table>
</div>
)
}
}

ResearcherClassesTable.defaultProps = {
classes: []
}
6 changes: 6 additions & 0 deletions react-components/src/library/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import UnitTestExample from './components/unit-test-example'
import RunWithCollaborators from './components/run-with-collaborators'
import LearnerReportForm from './components/learner-report-form'
import UserReportForm from './components/user-report-form'
import ResearcherClassesForm from './components/researcher-classes-form'
import SiteNotices from './components/site-notices'
import SiteNoticesNewForm from './components/site-notices/new'
import SiteNoticesEditForm from './components/site-notices/edit'
Expand Down Expand Up @@ -109,6 +110,11 @@ window.PortalComponents = {
render(React.createElement(UserReportForm, options), id)
},

ResearcherClassesForm: ResearcherClassesForm,
renderResearcherClassesForm: function (options, id) {
render(React.createElement(ResearcherClassesForm, options), id)
},

Navigation: Navigation,
renderNavigation: function (options, id) {
render(React.createElement(Navigation, options), id)
Expand Down

0 comments on commit 9c20315

Please sign in to comment.