Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a page to view all vocabulary in a course version #38760

Merged
merged 6 commits into from Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/Gruntfile.js
Expand Up @@ -565,7 +565,8 @@ describe('entry tests', () => {
'teacher_dashboard/parent_letter':
'./src/sites/studio/pages/teacher_dashboard/parent_letter.js',
'teacher_feedbacks/index':
'./src/sites/studio/pages/teacher_feedbacks/index.js'
'./src/sites/studio/pages/teacher_feedbacks/index.js',
'vocabularies/edit': './src/sites/studio/pages/vocabularies/edit.js'
};

var internalEntries = {
Expand Down
215 changes: 215 additions & 0 deletions apps/src/lib/levelbuilder/AllVocabulariesEditor.jsx
@@ -0,0 +1,215 @@
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import * as Table from 'reactabular-table';
import {lessonEditorTableStyles} from './lesson-editor/TableConstants';
import AddVocabularyDialog from './lesson-editor/AddVocabularyDialog';
import {connect} from 'react-redux';
import {
addVocabulary,
updateVocabulary,
removeVocabulary
} from '@cdo/apps/lib/levelbuilder/lesson-editor/vocabulariesEditorRedux';
import {vocabularyShape} from '@cdo/apps/lib/levelbuilder/shapes';
import Dialog from '@cdo/apps/templates/Dialog';

const styles = {
actionsColumn: {
display: 'flex',
justifyContent: 'space-evenly',
backgroundColor: 'white'
},
remove: {
fontSize: 14,
cursor: 'pointer',
textAlign: 'center',
width: '50%',
lineHeight: '30px'
},
edit: {
fontSize: 14,
cursor: 'pointer',
textAlign: 'center',
width: '50%',
lineHeight: '30px'
},
addButton: {
fontSize: 18,
marginTop: 'auto',
marginBottom: 'auto'
},
header: {
display: 'flex',
justifyContent: 'space-between'
}
};

class AllVocabulariesEditor extends Component {
static propTypes = {
// Provided by redux
vocabularies: PropTypes.arrayOf(vocabularyShape).isRequired,
addVocabulary: PropTypes.func.isRequired,
updateVocabulary: PropTypes.func.isRequired,
removeVocabulary: PropTypes.func.isRequired,

courseVersionId: PropTypes.number.isRequired,
courseName: PropTypes.string.isRequired
};

constructor(props) {
super(props);

this.state = {
vocabularyForEdit: null,
vocabularyForDeletion: null,
addVocabularyDialogOpen: false
};
}

getColumns() {
return [
{
property: 'actions',
header: {
label: 'Actions'
},
cell: {
formatters: [this.actionsCellFormatter],
props: {
style: {
...lessonEditorTableStyles.actionsCell
}
}
}
},
{
property: 'word',
header: {
label: 'Word'
},
cell: {
props: {
style: {
...lessonEditorTableStyles.cell
}
}
}
},
{
property: 'definition',
header: {
label: 'Definition'
},
cell: {
props: {
style: {
...lessonEditorTableStyles.cell
}
}
}
}
];
}

handleEdit = vocabularyForEdit => {
this.setState({vocabularyForEdit, addVocabularyDialogOpen: true});
};

actionsCellFormatter = (actions, {rowData}) => {
return (
<div style={styles.actionsColumn}>
<div onMouseDown={() => this.handleEdit(rowData)} style={styles.edit}>
<i className="fa fa-edit" />
</div>
<div
onMouseDown={() => this.setState({vocabularyForDeletion: rowData})}
style={styles.remove}
className={'unit-test-destroy-vocabulary'}
>
<i className="fa fa-trash" />
</div>
</div>
);
};

afterVocabularySave = vocabulary => {
if (this.state.vocabularyForEdit) {
this.props.updateVocabulary(vocabulary);
} else {
this.props.addVocabulary(vocabulary);
}
};

handleDeleteVocabularyDialogClose = () => {
this.setState({
vocabularyForDeletion: null
});
};

handleDeleteVocabularyConfirm = () => {
this.props.removeVocabulary(this.state.vocabularyForDeletion.key);
this.handleDeleteVocabularyDialogClose();
};

handleAddVocabularyClick = e => {
e.preventDefault();
this.setState({addVocabularyDialogOpen: true});
};

render() {
const columns = this.getColumns();
return (
<div>
<div style={styles.header}>
<h1>{`Vocabulary for ${this.props.courseName}`}</h1>
<a
onClick={this.handleAddVocabularyClick}
style={styles.addButton}
type="button"
>
<i className="fa fa-plus" style={{marginRight: 7}} />
Create New Vocabulary
</a>
</div>
{this.state.addVocabularyDialogOpen && (
<AddVocabularyDialog
editingVocabulary={this.state.vocabularyForEdit}
afterSave={this.afterVocabularySave}
handleClose={() =>
this.setState({
vocabularyForEdit: null,
addVocabularyDialogOpen: false
})
}
courseVersionId={this.props.courseVersionId}
/>
)}
{this.state.vocabularyForDeletion && (
<Dialog
body={`Are you sure you want to permanently delete vocabulary "${
this.state.vocabularyForDeletion.word
}"?`}
cancelText="Cancel"
confirmText="Delete"
confirmType="danger"
isOpen={true}
handleClose={this.handleDeleteVocabularyDialogClose}
onCancel={this.handleDeleteVocabularyDialogClose}
onConfirm={this.handleDeleteVocabularyConfirm}
/>
)}

<Table.Provider columns={columns} style={{width: '100%'}}>
<Table.Header />
<Table.Body rows={this.props.vocabularies} rowKey="key" />
</Table.Provider>
</div>
);
}
}

export const UnconnectedAllVocabulariesEditor = AllVocabulariesEditor;

export default connect(
state => ({vocabularies: state.vocabularies}),
{addVocabulary, updateVocabulary, removeVocabulary}
)(AllVocabulariesEditor);
32 changes: 32 additions & 0 deletions apps/src/sites/studio/pages/vocabularies/edit.js
@@ -0,0 +1,32 @@
import getScriptData from '@cdo/apps/util/getScriptData';
import React from 'react';
import ReactDOM from 'react-dom';
import AllVocabulariesEditor from '@cdo/apps/lib/levelbuilder/AllVocabulariesEditor';
import vocabulariesEditor, {
initVocabularies
} from '@cdo/apps/lib/levelbuilder/lesson-editor/vocabulariesEditorRedux';
import {Provider} from 'react-redux';
import {getStore, registerReducers} from '@cdo/apps/redux';

$(document).ready(function() {
const vocabularies = getScriptData('vocabularies');
const courseVersionId = getScriptData('courseVersionId');
const courseName = getScriptData('courseName');

registerReducers({
vocabularies: vocabulariesEditor
});
const store = getStore();
store.dispatch(initVocabularies(vocabularies || []));

ReactDOM.render(
<Provider store={store}>
<AllVocabulariesEditor
vocabularies={vocabularies}
courseVersionId={courseVersionId}
courseName={courseName}
/>
</Provider>,
document.getElementById('vocabularies-table')
);
});
53 changes: 53 additions & 0 deletions apps/test/unit/lib/levelbuilder/AllVocabulariesEditorTest.js
@@ -0,0 +1,53 @@
import React from 'react';
import {mount} from 'enzyme';
import sinon from 'sinon';
import {expect} from '../../../util/reconfiguredChai';
import {UnconnectedAllVocabulariesEditor as AllVocabulariesEditor} from '@cdo/apps/lib/levelbuilder/AllVocabulariesEditor';

describe('AllVocabulariesEditor', () => {
let defaultProps, addVocabulary, updateVocabulary, removeVocabulary;
beforeEach(() => {
addVocabulary = sinon.spy();
updateVocabulary = sinon.spy();
removeVocabulary = sinon.spy();
defaultProps = {
vocabularies: [
{id: 1, key: '1', word: 'word1', definition: 'def1'},
{id: 2, key: '2', word: 'word2', definition: 'def2'}
],
addVocabulary,
updateVocabulary,
removeVocabulary,
courseVersionId: 2021,
courseName: 'test-course-2021'
};
});

it('renders default props', () => {
const wrapper = mount(<AllVocabulariesEditor {...defaultProps} />);
expect(wrapper.find('tr').length).to.equal(3);
});

it('can remove a vocabulary', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add a test for checking you can add vocabulary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was struggling on what to actually test for that path as the logic is mostly handled through another component. I'm adding a test that clicking the button actually brings up that component but not sure what else to add.

const wrapper = mount(<AllVocabulariesEditor {...defaultProps} />);
const numVocabularies = wrapper.find('tr').length;
expect(numVocabularies).at.least(2);
// Find one of the "remove" buttons and click it
const removeVocabularyButton = wrapper
.find('.unit-test-destroy-vocabulary')
.first();
removeVocabularyButton.simulate('mouseDown');
const removeDialog = wrapper.find('Dialog');
const deleteButton = removeDialog.find('button').at(1);
deleteButton.simulate('click');
expect(removeVocabulary).to.have.been.calledOnce;
});

it('can add a vocabulary', () => {
const wrapper = mount(<AllVocabulariesEditor {...defaultProps} />);
const addVocabularyButton = wrapper.find('a');
expect(addVocabularyButton.contains('Create New Vocabulary')).to.be.true;
addVocabularyButton.simulate('click');
expect(wrapper.find('AddVocabularyDialog').length).to.equal(1);
});
});
14 changes: 14 additions & 0 deletions dashboard/app/controllers/vocabularies_controller.rb
Expand Up @@ -36,11 +36,25 @@ def update
end
end

# GET /courses/:course_name/vocab/edit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense to me to have this in this controller

def edit
@course_version = find_matching_course_version
@vocabularies = @course_version.vocabularies.order(:word)
end

private

def vocabulary_params
vp = params.transform_keys(&:underscore)
vp = vp.permit(:id, :key, :word, :definition, :course_version_id)
vp
end

def find_matching_course_version
matching_unit_group = UnitGroup.find_by_name(params[:course_name])
return matching_unit_group.course_version if matching_unit_group
matching_standalone_course = Script.find_by_name(params[:course_name])
return matching_standalone_course.course_version if matching_standalone_course&.is_course
return nil
end
end
4 changes: 4 additions & 0 deletions dashboard/app/views/vocabularies/edit.html.haml
@@ -0,0 +1,4 @@
%script{src: webpack_asset_path('js/vocabularies/edit.js'),
data: {vocabularies: @vocabularies.to_json, courseVersionId: @course_version.id, courseName: params[:course_name].to_json}}

#vocabularies-table
1 change: 1 addition & 0 deletions dashboard/config/routes.rb
Expand Up @@ -320,6 +320,7 @@
get '/resourcesearch', to: 'resources#search', defaults: {format: 'json'}

resources :vocabularies, only: [:create, :update]
get '/courses/:course_name/vocab/edit', to: 'vocabularies#edit'
get '/vocabularysearch', to: 'vocabularies#search', defaults: {format: 'json'}

get '/beta', to: redirect('/')
Expand Down
24 changes: 24 additions & 0 deletions dashboard/test/controllers/vocabularies_controller_test.rb
Expand Up @@ -43,4 +43,28 @@ class VocabulariesControllerTest < ActionController::TestCase
vocabulary.reload
assert_equal 'definition', vocabulary.definition
end

test "can load vocab edit page of unit group course version" do
sign_in @levelbuilder
course_version = create :course_version
unit_group = create :unit_group, name: 'fake-course-2021', course_version: course_version
vocabulary = create :vocabulary, key: 'variable', word: 'variable', definition: 'definition', course_version: course_version

get :edit, params: {course_name: unit_group.name}
assert_response :success
assert_equal assigns(:course_version), course_version
assert_equal assigns(:vocabularies), [vocabulary]
end

test "can load vocab edit page of standalone script course version" do
sign_in @levelbuilder
course_version = create :course_version
script = create :script, name: 'fake-standalone-script-2021', is_course: true, course_version: course_version
vocabulary = create :vocabulary, key: 'variable', word: 'variable', definition: 'definition', course_version: course_version

get :edit, params: {course_name: script.name}
assert_response :success
assert_equal assigns(:course_version), course_version
assert_equal assigns(:vocabularies), [vocabulary]
end
end