new account course search page
also lay foundation for new account user search test plan: * in development mode, enable the "Course and User Search" feature flag * should be able to view the "Search" tab on the sidebar (may need to resave the account to clear the sidebar cache) (replaces the "Courses" and "Users" tabs) * searching for courses on the account page should work pretty good (the people tab is still forthcoming) closes #CNVS-24750 Change-Id: Id44d1b3c7c36e407339858d2c1657579d1128abc Reviewed-on: https://gerrit.instructure.com/65268 Tested-by: Jenkins Reviewed-by: Jeremy Stanley <jeremy@instructure.com> QA-Review: Clare Strong <clare@instructure.com> Product-Review: Cosme Salazar <cosme@instructure.com>
This commit is contained in:
parent
9d2ec79ec0
commit
9bc2dc05bb
|
@ -0,0 +1,15 @@
|
|||
require [
|
||||
'jquery'
|
||||
'react'
|
||||
"jsx/account_course_user_search/index"
|
||||
], ($, React, App) ->
|
||||
React.render(
|
||||
React.createElement(
|
||||
App,
|
||||
{
|
||||
accountId: ENV.ACCOUNT_ID.toString(),
|
||||
permissions: ENV.PERMISSIONS
|
||||
}
|
||||
),
|
||||
$("#content")[0]
|
||||
)
|
|
@ -167,6 +167,7 @@ class AccountsController < ApplicationController
|
|||
return unless authorized_action(@account, @current_user, :read)
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
return course_user_search if @account.feature_enabled?(:course_user_search)
|
||||
if value_to_boolean(params[:theme_applied])
|
||||
flash[:notice] = t("Your custom theme has been successfully applied.")
|
||||
end
|
||||
|
@ -182,6 +183,22 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def course_user_search
|
||||
can_read_course_list = @account.grants_right?(@current_user, session, :read_course_list)
|
||||
can_read_roster = @account.grants_right?(@current_user, session, :read_roster)
|
||||
|
||||
unless can_read_course_list || can_read_roster
|
||||
return render_unauthorized_action
|
||||
end
|
||||
|
||||
@permissions = {
|
||||
theme_editor: use_new_styles? && can_do(@account, @current_user, :manage_account_settings) && @account.branding_allowed?,
|
||||
can_read_course_list: can_read_course_list,
|
||||
can_read_roster: can_read_roster
|
||||
}
|
||||
render template: "accounts/course_user_search"
|
||||
end
|
||||
|
||||
# @API Get the sub-accounts of an account
|
||||
#
|
||||
# List accounts that are sub-accounts of the given account.
|
||||
|
@ -232,6 +249,10 @@ class AccountsController < ApplicationController
|
|||
# include only courses with no enrollments. If not present, do not filter
|
||||
# on course enrollment status.
|
||||
#
|
||||
# @argument enrollment_type[] [String, "teacher"|"student"|"ta"|"observer"|"designer"]
|
||||
# If set, only return courses that have at least one user enrolled in
|
||||
# in the course with one of the specified enrollment types.
|
||||
#
|
||||
# @argument published [Boolean]
|
||||
# If true, include only published courses. If false, exclude published
|
||||
# courses. If not present, do not filter on published status.
|
||||
|
@ -263,7 +284,7 @@ class AccountsController < ApplicationController
|
|||
# @argument search_term [String]
|
||||
# The partial course name, code, or full ID to match and return in the results list. Must be at least 3 characters.
|
||||
#
|
||||
# @argument include[] [String, "syllabus_body"|"term"|"course_progress"|"storage_quota_used_mb"]
|
||||
# @argument include[] [String, "syllabus_body"|"term"|"course_progress"|"storage_quota_used_mb"|"total_students"|"teachers"]
|
||||
# - All explanations can be seen in the {api:CoursesController#index Course API index documentation}
|
||||
# - "sections", "needs_grading_count" and "total_scores" are not valid options at the account level
|
||||
#
|
||||
|
@ -286,6 +307,10 @@ class AccountsController < ApplicationController
|
|||
@courses = @courses.without_enrollments
|
||||
end
|
||||
|
||||
if params[:enrollment_type].is_a?(Array)
|
||||
@courses = @courses.with_enrollment_types(params[:enrollment_type])
|
||||
end
|
||||
|
||||
if value_to_boolean(params[:completed])
|
||||
@courses = @courses.completed
|
||||
elsif !params[:completed].nil? && !value_to_boolean(params[:completed])
|
||||
|
@ -332,6 +357,7 @@ class AccountsController < ApplicationController
|
|||
@courses = Api.paginate(@courses, self, api_v1_account_courses_url)
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(@courses, [:account, :root_account]).run
|
||||
ActiveRecord::Associations::Preloader.new(@courses, [:teachers]).run if includes.include?("teachers")
|
||||
render :json => @courses.map { |c| course_json(c, @current_user, session, includes, nil) }
|
||||
end
|
||||
|
||||
|
|
|
@ -318,7 +318,7 @@ class CoursesController < ApplicationController
|
|||
# 'StudentEnrollment', 'TeacherEnrollment', 'TaEnrollment', 'ObserverEnrollment',
|
||||
# or 'DesignerEnrollment'.
|
||||
#
|
||||
# @argument include[] [String, "needs_grading_count"|"syllabus_body"|"total_scores"|"term"|"course_progress"|"sections"|"storage_quota_used_mb"|"total_students"|"favorites"]
|
||||
# @argument include[] [String, "needs_grading_count"|"syllabus_body"|"total_scores"|"term"|"course_progress"|"sections"|"storage_quota_used_mb"|"total_students"|"favorites"|"teachers"]
|
||||
# - "needs_grading_count": Optional information to include with each Course.
|
||||
# When needs_grading_count is given, and the current user has grading
|
||||
# rights, the total number of submissions needing grading for all
|
||||
|
@ -362,6 +362,9 @@ class CoursesController < ApplicationController
|
|||
# - "passback_status": Include the grade passback_status
|
||||
# - "favorites": Optional information to include with each Course.
|
||||
# Indicates if the user has marked the course as a favorite course.
|
||||
# - "teachers": Teacher information to include with each Course.
|
||||
# Returns an array of hashes containing the {{api:Users:UserDisplay UserDisplay} information
|
||||
# for each teacher in the course.
|
||||
#
|
||||
# @argument state[] [String, "unpublished"|"available"|"completed"|"deleted"]
|
||||
# If set, only return courses that are in the given state(s).
|
||||
|
@ -404,7 +407,6 @@ class CoursesController < ApplicationController
|
|||
|
||||
format.json {
|
||||
render json: courses_for_user(@current_user)
|
||||
|
||||
}
|
||||
end
|
||||
end
|
||||
|
@ -2413,6 +2415,10 @@ class CoursesController < ApplicationController
|
|||
Canvas::Builders::EnrollmentDateBuilder.preload(enrollments)
|
||||
enrollments_by_course = enrollments.group_by(&:course_id).values
|
||||
enrollments_by_course = Api.paginate(enrollments_by_course, self, api_v1_courses_url) if api_request?
|
||||
if includes.include?("teachers")
|
||||
courses = enrollments_by_course.map(&:first).map(&:course)
|
||||
ActiveRecord::Associations::Preloader.new(courses, :teachers).run
|
||||
end
|
||||
enrollments_by_course.each do |course_enrollments|
|
||||
course = course_enrollments.first.course
|
||||
hash << course_json(course, user, session, includes, course_enrollments)
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
define([
|
||||
"./createStore",
|
||||
"underscore"
|
||||
], function(createStore, _) {
|
||||
|
||||
var { string, shape, arrayOf } = React.PropTypes;
|
||||
|
||||
var AccountsTreeStore = createStore({
|
||||
getUrl() {
|
||||
return `/api/v1/accounts/${this.context.accountId}/sub_accounts`;
|
||||
},
|
||||
|
||||
loadTree() {
|
||||
var key = this.getKey();
|
||||
// fetch the account itself first, then get its subaccounts
|
||||
this._load(key, `/api/v1/accounts/${this.context.accountId}`, {}, {wrap: true}).then(() => {
|
||||
this.loadAll(null, true);
|
||||
});
|
||||
},
|
||||
|
||||
normalizeParams() {
|
||||
return { recursive: true };
|
||||
},
|
||||
|
||||
getTree() {
|
||||
var data = this.get();
|
||||
if (!data || !data.data) return [];
|
||||
var accounts = [];
|
||||
var idIndexMap = {};
|
||||
data.data.forEach(function(item, i) {
|
||||
var account = _.extend({}, item, {subAccounts: []});
|
||||
accounts.push(account);
|
||||
idIndexMap[account.id] = i;
|
||||
})
|
||||
accounts.forEach(function(account) {
|
||||
var parentIdx = idIndexMap[account.parent_account_id];
|
||||
if (typeof parentIdx === "undefined") return;
|
||||
accounts[parentIdx].subAccounts.push(account);
|
||||
});
|
||||
return [accounts[0]];
|
||||
}
|
||||
});
|
||||
|
||||
AccountsTreeStore.PropType = shape({
|
||||
id: string.isRequired,
|
||||
parent_account_id: string,
|
||||
name: string.isRequired,
|
||||
subAccounts: arrayOf(
|
||||
function() { AccountsTreeStore.PropType.apply(this, arguments) }
|
||||
).isRequired
|
||||
});
|
||||
|
||||
return AccountsTreeStore;
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
define([
|
||||
"react",
|
||||
"i18n!account_course_user_search",
|
||||
"underscore",
|
||||
"./CoursesListRow",
|
||||
], function(React, I18n, _, CoursesListRow) {
|
||||
|
||||
var { number, string, func, shape, arrayOf } = React.PropTypes;
|
||||
|
||||
var CoursesList = React.createClass({
|
||||
propTypes: {
|
||||
courses: arrayOf(shape(CoursesListRow.propTypes)).isRequired
|
||||
},
|
||||
|
||||
render() {
|
||||
var { courses } = this.props;
|
||||
|
||||
return (
|
||||
<div className="pad-box no-sides">
|
||||
<table className="ic-Table courses-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>
|
||||
{I18n.t("Course ID")}
|
||||
</th>
|
||||
<th>
|
||||
{I18n.t("SIS ID")}
|
||||
</th>
|
||||
<th>
|
||||
{I18n.t("Teacher")}
|
||||
</th>
|
||||
<th>
|
||||
{I18n.t("Enrollments")}
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{courses.map((course) => <CoursesListRow key={course.id} {...course} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return CoursesList;
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
define([
|
||||
"react",
|
||||
"i18n!account_course_user_search",
|
||||
"underscore",
|
||||
"./UserLink",
|
||||
], function(React, I18n, _, UserLink) {
|
||||
|
||||
var { number, string, func, shape, arrayOf } = React.PropTypes;
|
||||
|
||||
var CoursesListRow = React.createClass({
|
||||
propTypes: {
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
workflow_state: string.isRequired,
|
||||
total_students: number.isRequired,
|
||||
teachers: arrayOf(shape(UserLink.propTypes)).isRequired
|
||||
},
|
||||
|
||||
render() {
|
||||
var { id, name, workflow_state, sis_course_id, total_students, teachers } = this.props;
|
||||
var url = `/courses/${id}`;
|
||||
var isPublished = workflow_state !== "unpublished";
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td style={{width: 16}}>
|
||||
{isPublished && (<i className="icon-publish courses-list__published-icon" />)}
|
||||
</td>
|
||||
<td>
|
||||
<a href={url}>{name}</a>
|
||||
</td>
|
||||
<td>
|
||||
{sis_course_id}
|
||||
</td>
|
||||
<td>
|
||||
{teachers &&
|
||||
<div style={{margin: "-6px 0 -9px 0"}}>
|
||||
{teachers.map((teacher) => <UserLink key={teacher.id} {...teacher} />)}
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
{total_students}
|
||||
</td>
|
||||
<td>
|
||||
{/* TODO actions */}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return CoursesListRow;
|
||||
});
|
|
@ -0,0 +1,143 @@
|
|||
define([
|
||||
"react",
|
||||
"i18n!account_course_user_search",
|
||||
"underscore",
|
||||
"./CoursesStore",
|
||||
"./TermsStore",
|
||||
"./AccountsTreeStore",
|
||||
"./CoursesList",
|
||||
"./CoursesToolbar",
|
||||
], function(React, I18n, _, CoursesStore, TermsStore, AccountsTreeStore, CoursesList, CoursesToolbar) {
|
||||
|
||||
var MIN_SEARCH_LENGTH = 3;
|
||||
|
||||
var stores = [CoursesStore, TermsStore, AccountsTreeStore];
|
||||
|
||||
var CoursesPane = React.createClass({
|
||||
getInitialState() {
|
||||
var filters = {
|
||||
enrollment_term_id: "",
|
||||
search_term: "",
|
||||
with_students: false
|
||||
};
|
||||
|
||||
return {
|
||||
filters,
|
||||
draftFilters: filters,
|
||||
errors: {}
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
stores.forEach((s) => s.addChangeListener(this.refresh));
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchCourses();
|
||||
TermsStore.loadAll();
|
||||
AccountsTreeStore.loadTree();
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
stores.forEach((s) => s.removeChangeListener(this.refresh));
|
||||
},
|
||||
|
||||
fetchCourses() {
|
||||
CoursesStore.load(this.state.filters);
|
||||
},
|
||||
|
||||
fetchMoreCourses() {
|
||||
CoursesStore.loadMore(this.state.filters);
|
||||
},
|
||||
|
||||
onUpdateFilters(newFilters) {
|
||||
this.setState({
|
||||
errors: {},
|
||||
draftFilters: _.extend({}, this.state.draftFilters, newFilters)
|
||||
});
|
||||
},
|
||||
|
||||
onApplyFilters() {
|
||||
var filters = this.state.draftFilters;
|
||||
if (filters.search_term && filters.search_term.length < MIN_SEARCH_LENGTH) {
|
||||
this.setState({errors: {search_term: I18n.t("Search term must be at least %{num} characters", {num: MIN_SEARCH_LENGTH})}})
|
||||
} else {
|
||||
this.setState({filters, errors: {}}, this.fetchCourses);
|
||||
}
|
||||
},
|
||||
|
||||
refresh() {
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
||||
renderCourseMessage(courses) {
|
||||
if (!courses || courses.loading) {
|
||||
return (
|
||||
<div className="text-center pad-box">
|
||||
{I18n.t("Loading...")}
|
||||
</div>
|
||||
);
|
||||
} else if (courses.error) {
|
||||
return (
|
||||
<div className="text-center pad-box">
|
||||
<div className="alert alert-error">
|
||||
{I18n.t("There was an error with your query; please try a different search")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (!courses.data.length) {
|
||||
return (
|
||||
<div className="text-center pad-box">
|
||||
<div className="alert alert-info">
|
||||
{I18n.t("No courses found")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (courses.next) {
|
||||
return (
|
||||
<div className="text-center pad-box">
|
||||
<button
|
||||
className="Button--link load_more_courses"
|
||||
onClick={this.fetchMoreCourses}
|
||||
>
|
||||
<i className="icon-refresh" />
|
||||
{" "}
|
||||
{I18n.t("Load more...")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
var { filters, draftFilters, errors } = this.state;
|
||||
var courses = CoursesStore.get(filters);
|
||||
var terms = TermsStore.get();
|
||||
var accounts = AccountsTreeStore.getTree();
|
||||
var isLoading = !(courses && !courses.loading && terms && !terms.loading);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CoursesToolbar
|
||||
onUpdateFilters={this.onUpdateFilters}
|
||||
onApplyFilters={this.onApplyFilters}
|
||||
terms={terms && terms.data}
|
||||
accounts={accounts}
|
||||
isLoading={isLoading}
|
||||
{...draftFilters}
|
||||
errors={errors}
|
||||
/>
|
||||
|
||||
{courses && courses.data &&
|
||||
<CoursesList courses={courses.data} />
|
||||
}
|
||||
|
||||
{this.renderCourseMessage(courses)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return CoursesPane;
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
define([
|
||||
"./createStore",
|
||||
], function(createStore) {
|
||||
|
||||
var CoursesStore = createStore({
|
||||
getUrl() {
|
||||
return `/api/v1/accounts/${this.context.accountId}/courses`;
|
||||
},
|
||||
|
||||
normalizeParams(params) {
|
||||
var payload = {};
|
||||
if (params.enrollment_term_id) payload.enrollment_term_id = params.enrollment_term_id;
|
||||
if (params.search_term) payload.search_term = params.search_term;
|
||||
if (params.with_students) payload.enrollment_type = ["student"];
|
||||
payload.include = ["total_students", "teachers"];
|
||||
return payload;
|
||||
}
|
||||
});
|
||||
|
||||
return CoursesStore;
|
||||
});
|
|
@ -0,0 +1,117 @@
|
|||
define([
|
||||
"react",
|
||||
"i18n!account_course_user_search",
|
||||
"underscore",
|
||||
"./TermsStore",
|
||||
"./AccountsTreeStore",
|
||||
"./NewCourseModal",
|
||||
"./IcInput",
|
||||
"./IcSelect",
|
||||
"./IcCheckbox"
|
||||
], function(React, I18n, _, TermsStore, AccountsTreeStore, NewCourseModal, IcInput, IcSelect, IcCheckbox) {
|
||||
|
||||
var { string, bool, func, object, arrayOf, shape } = React.PropTypes;
|
||||
|
||||
var CoursesToolbar = React.createClass({
|
||||
propTypes: {
|
||||
onUpdateFilters: func.isRequired,
|
||||
onApplyFilters: func.isRequired,
|
||||
isLoading: bool,
|
||||
|
||||
with_students: bool.isRequired,
|
||||
search_term: string,
|
||||
enrollment_term_id: string,
|
||||
errors: object,
|
||||
|
||||
terms: arrayOf(TermsStore.PropType),
|
||||
accounts: arrayOf(AccountsTreeStore.PropType)
|
||||
},
|
||||
|
||||
applyFilters(e) {
|
||||
e.preventDefault();
|
||||
this.props.onApplyFilters();
|
||||
},
|
||||
|
||||
renderTerms() {
|
||||
var { terms } = this.props;
|
||||
|
||||
if (terms) {
|
||||
return [
|
||||
<option key="all" value="">
|
||||
{I18n.t("All Terms")}
|
||||
</option>
|
||||
].concat(terms.map((term) => {
|
||||
return (
|
||||
<option key={term.id} value={term.id}>
|
||||
{term.name}
|
||||
</option>
|
||||
);
|
||||
}));
|
||||
} else {
|
||||
return <option value="">{I18n.t("Loading...")}</option>;
|
||||
}
|
||||
},
|
||||
|
||||
addCourse() {
|
||||
this.refs.addCourse.openModal();
|
||||
},
|
||||
|
||||
render() {
|
||||
var { terms, accounts, onUpdateFilters, isLoading, with_students, search_term, enrollment_term_id, errors } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form
|
||||
className="ic-Form-group ic-Form-group--inline course_search_bar"
|
||||
style={{alignItems: "center", opacity: isLoading ? 0.5 : 1}}
|
||||
onSubmit={this.applyFilters}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<IcSelect
|
||||
value={enrollment_term_id}
|
||||
onChange={(e) => onUpdateFilters({enrollment_term_id: e.target.value})}
|
||||
>
|
||||
{this.renderTerms()}
|
||||
</IcSelect>
|
||||
|
||||
<IcInput
|
||||
value={search_term}
|
||||
placeholder={I18n.t("Search courses...")}
|
||||
onChange={(e) => onUpdateFilters({search_term: e.target.value})}
|
||||
error={errors.search_term}
|
||||
/>
|
||||
|
||||
<div className="ic-Form-control" style={{flexGrow: 0.3}}>
|
||||
<button className="btn">
|
||||
{I18n.t("Go")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<IcCheckbox
|
||||
controlClassName="flex-grow-2"
|
||||
checked={with_students}
|
||||
onChange={(e) => onUpdateFilters({with_students: e.target.checked})}
|
||||
label={I18n.t("Hide courses without enrollments")}
|
||||
/>
|
||||
|
||||
<div className="ic-Form-actions">
|
||||
<button className="btn" type="button" onClick={this.addCourse}>
|
||||
<i className="icon-plus" />
|
||||
{" "}
|
||||
{I18n.t("Course")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<NewCourseModal
|
||||
ref="addCourse"
|
||||
terms={terms}
|
||||
accounts={accounts}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return CoursesToolbar;
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
define([
|
||||
"react",
|
||||
"./IcInput",
|
||||
"bower/classnames/index"
|
||||
], function(React, IcInput, classnames) {
|
||||
|
||||
var { string } = React.PropTypes;
|
||||
|
||||
/**
|
||||
* A checkbox input wrapped with appropriate ic-Form-* elements and
|
||||
* classes, with support for a label and error message.
|
||||
*
|
||||
* All other props are passed through to the
|
||||
* <input />
|
||||
*/
|
||||
var IcCheckbox = React.createClass({
|
||||
propTypes: {
|
||||
error: string,
|
||||
label: string
|
||||
},
|
||||
|
||||
render() {
|
||||
var { controlClassName } = this.props;
|
||||
|
||||
return (
|
||||
<IcInput
|
||||
{...this.props}
|
||||
type="checkbox"
|
||||
appendLabel={true}
|
||||
noClassName={true}
|
||||
controlClassName={classnames("ic-Form-control--checkbox", controlClassName)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
return IcCheckbox;
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
define([
|
||||
"react",
|
||||
"underscore",
|
||||
"bower/classnames/index"
|
||||
], function(React, _, classnames) {
|
||||
|
||||
var { string, any, bool } = React.PropTypes;
|
||||
|
||||
var idCount = 0;
|
||||
|
||||
/**
|
||||
* An input wrapped with appropriate ic-Form-* elements and classes,
|
||||
* with support for a label, error message and extra classes on the
|
||||
* wrapping div.
|
||||
*
|
||||
* All other props are passed through to the <input />
|
||||
*/
|
||||
var IcInput = React.createClass({
|
||||
propTypes: {
|
||||
error: string,
|
||||
label: string,
|
||||
elementType: any,
|
||||
controlClassName: string,
|
||||
appendLabel: bool,
|
||||
noClassName: bool
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
elementType: "input"
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this.id = `ic_input_${idCount++}`;
|
||||
},
|
||||
|
||||
render() {
|
||||
var { error, label, elementType, appendLabel, controlClassName, noClassName } = this.props;
|
||||
var inputProps = _.extend({}, _.omit(this.props, ["error", "label", "elementType"]), {id: this.id});
|
||||
if (elementType === "input" && !this.props.type) {
|
||||
inputProps.type = "text";
|
||||
}
|
||||
if (!noClassName) {
|
||||
inputProps.className = classnames(inputProps.className, "ic-Input");
|
||||
}
|
||||
|
||||
var labelElement = label &&
|
||||
<label htmlFor={this.id} className="ic-Label">{label}</label>;
|
||||
|
||||
return (
|
||||
<div className={classnames("ic-Form-control", controlClassName, {"ic-Form-control--has-error": error})}>
|
||||
{!!label && !appendLabel && labelElement}
|
||||
{React.createElement(elementType, inputProps)}
|
||||
{!!label && appendLabel && labelElement}
|
||||
{!!error &&
|
||||
<div className="ic-Form-message ic-Form-message--error" style={{position: "absolute"}}>
|
||||
<div className="ic-Form-message__Layout">
|
||||
<i className="icon-warning" role="presentation"></i>
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
return IcInput;
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
define([
|
||||
"react",
|
||||
"./IcInput"
|
||||
], function(React, IcInput) {
|
||||
|
||||
var { string } = React.PropTypes;
|
||||
|
||||
/**
|
||||
* A select wrapped with appropriate ic-Form-* elements and classes,
|
||||
* with support for a label and error message.
|
||||
*
|
||||
* All other props (including children) are passed through to the
|
||||
* <select />
|
||||
*/
|
||||
var IcSelect = React.createClass({
|
||||
propTypes: {
|
||||
error: string,
|
||||
label: string
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<IcInput
|
||||
{...this.props}
|
||||
elementType="select"
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
return IcSelect;
|
||||
});
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
define([
|
||||
"jquery",
|
||||
"react",
|
||||
"underscore",
|
||||
"i18n!account_course_user_search",
|
||||
"jsx/shared/modal",
|
||||
"jsx/shared/modal-content",
|
||||
"jsx/shared/modal-buttons",
|
||||
"./CoursesStore",
|
||||
"./TermsStore",
|
||||
"./AccountsTreeStore",
|
||||
"./IcInput",
|
||||
"./IcSelect",
|
||||
"compiled/jquery.rails_flash_notifications"
|
||||
], function($, React, _, I18n, Modal, ModalContent, ModalButtons, CoursesStore, TermsStore, AccountsTreeStore, IcInput, IcSelect) {
|
||||
|
||||
var { arrayOf } = React.PropTypes;
|
||||
|
||||
var NewCourseModal = React.createClass({
|
||||
propTypes: {
|
||||
terms: arrayOf(TermsStore.PropType),
|
||||
accounts: arrayOf(AccountsTreeStore.PropType)
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
isOpen: false,
|
||||
data: {},
|
||||
errors: {}
|
||||
}
|
||||
},
|
||||
|
||||
openModal() {
|
||||
this.setState({isOpen: true});
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.setState({isOpen: false, data: {}, errors: {}});
|
||||
},
|
||||
|
||||
onChange(field, value) {
|
||||
var { data } = this.state;
|
||||
var newData = {};
|
||||
newData[field] = value;
|
||||
data = _.extend({}, data, newData);
|
||||
this.setState({ data, errors: {} });
|
||||
},
|
||||
|
||||
onSubmit() {
|
||||
var { data } = this.state;
|
||||
var errors = {}
|
||||
if (!data.name) errors.name = I18n.t("Course name is required");
|
||||
if (!data.course_code) errors.course_code = I18n.t("Reference code is required");
|
||||
if (Object.keys(errors).length) {
|
||||
this.setState({ errors });
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: error handling
|
||||
CoursesStore.create({course: data}).then(() => {
|
||||
this.closeModal();
|
||||
$.flashMessage(I18n.t("%{course_name} successfully added!", {course_name: data.name}));
|
||||
});
|
||||
},
|
||||
|
||||
renderAccountOptions(accounts, result, depth) {
|
||||
accounts = accounts || this.props.accounts;
|
||||
result = result || [];
|
||||
depth = depth || 0;
|
||||
|
||||
accounts.forEach((account) => {
|
||||
result.push(
|
||||
<option key={account.id} value={account.id}>
|
||||
{Array(2 * depth + 1).join("\u00a0") + account.name}
|
||||
</option>
|
||||
);
|
||||
this.renderAccountOptions(account.subAccounts, result, depth + 1);
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
||||
renderTermOptions() {
|
||||
var { terms } = this.props;
|
||||
return (terms || []).map((term) => {
|
||||
return (
|
||||
<option key={term.id} value={term.id}>
|
||||
{term.name}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
render() {
|
||||
var { data, isOpen, errors } = this.state;
|
||||
var onChange = (field) => {
|
||||
return (e) => this.onChange(field, e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="ReactModal__Content--canvas ReactModal__Content--mini-modal"
|
||||
ref="canvasModal"
|
||||
isOpen={isOpen}
|
||||
title={I18n.t("Add a New Course")}
|
||||
onRequestClose={this.closeModal}
|
||||
onSubmit={this.onSubmit}
|
||||
>
|
||||
<ModalContent>
|
||||
<IcInput
|
||||
label={I18n.t("Course Name")}
|
||||
value={data.name}
|
||||
error={errors.name}
|
||||
onChange={onChange("name")}
|
||||
/>
|
||||
|
||||
<IcInput
|
||||
label={I18n.t("Reference Code")}
|
||||
value={data.course_code}
|
||||
error={errors.course_code}
|
||||
onChange={onChange("course_code")}
|
||||
/>
|
||||
|
||||
<IcSelect
|
||||
label={I18n.t("Department")}
|
||||
value={data.account_id}
|
||||
onChange={onChange("account_id")}
|
||||
>
|
||||
{this.renderAccountOptions()}
|
||||
</IcSelect>
|
||||
|
||||
<IcSelect
|
||||
label={I18n.t("Enrollment Term")}
|
||||
value={data.enrollment_term_id}
|
||||
onChange={onChange("account_id")}
|
||||
>
|
||||
{this.renderTermOptions()}
|
||||
</IcSelect>
|
||||
</ModalContent>
|
||||
|
||||
<ModalButtons>
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
onClick={this.closeModal}
|
||||
>
|
||||
{I18n.t("Cancel")}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={this.onSubmit}
|
||||
>
|
||||
{I18n.t("Add Course")}
|
||||
</button>
|
||||
</ModalButtons>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return NewCourseModal;
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
define([
|
||||
"react",
|
||||
"./createStore",
|
||||
], function(React, createStore) {
|
||||
|
||||
var { string, shape } = React.PropTypes;
|
||||
|
||||
var TermsStore = createStore({
|
||||
getUrl() {
|
||||
return `/api/v1/accounts/${this.context.accountId}/terms`;
|
||||
},
|
||||
|
||||
jsonKey: "enrollment_terms"
|
||||
});
|
||||
|
||||
TermsStore.PropType = shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired
|
||||
});
|
||||
|
||||
return TermsStore;
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
define([
|
||||
"react",
|
||||
"i18n!account_course_user_search"
|
||||
], function(React, I18n) {
|
||||
|
||||
var UserLink = React.createClass({
|
||||
propTypes: {
|
||||
id: React.PropTypes.string.isRequired,
|
||||
display_name: React.PropTypes.string.isRequired,
|
||||
avatar_image_url: React.PropTypes.string
|
||||
},
|
||||
|
||||
render() {
|
||||
var { id, display_name, avatar_image_url } = this.props;
|
||||
var url = `/users/${id}`;
|
||||
return (
|
||||
<div>
|
||||
{!!avatar_image_url &&
|
||||
<span className="ic-avatar" style={{width: 30, height: 30, margin: "-1px 10px 1px 0"}}>
|
||||
<img src={avatar_image_url} />
|
||||
</span>
|
||||
}
|
||||
<a href={url} className="user_link">{display_name}</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return UserLink;
|
||||
});
|
|
@ -0,0 +1,176 @@
|
|||
define([
|
||||
"jquery",
|
||||
"jsx/shared/helpers/createStore",
|
||||
"compiled/fn/parseLinkHeader",
|
||||
"underscore",
|
||||
"jquery.ajaxJSON"
|
||||
], function($, createStore, parseLinkHeader, _) {
|
||||
|
||||
/**
|
||||
* Build a store that support basic ajax fetching (first, next, all),
|
||||
* and caches the results by params.
|
||||
*
|
||||
* You only need to implement getUrl, and can optionally implement
|
||||
* normalizeParams and jsonKey
|
||||
*/
|
||||
var factory = function(spec) {
|
||||
return _.extend(createStore(), {
|
||||
/**
|
||||
* Get a blank state in the store; useful when mounting the top-
|
||||
* level component that uses the store
|
||||
*
|
||||
* @param {any} context
|
||||
* User-defined data you can use later on in normalizeParams
|
||||
* and getUrl; will be available as `this.context`
|
||||
*/
|
||||
reset(context) {
|
||||
this.clearState();
|
||||
this.context = context;
|
||||
},
|
||||
|
||||
getKey(params) {
|
||||
return JSON.stringify(params || {});
|
||||
},
|
||||
|
||||
normalizeParams(params) {
|
||||
return params;
|
||||
},
|
||||
|
||||
getUrl() {
|
||||
throw "not implemented"
|
||||
},
|
||||
|
||||
/**
|
||||
* If the API response is an object instead of an array, use this
|
||||
* to specify the key containing the actual array of results
|
||||
*/
|
||||
jsonKey: null,
|
||||
|
||||
/**
|
||||
* Load the first page of data for the given params
|
||||
*/
|
||||
load(params) {
|
||||
var key = this.getKey(params);
|
||||
this.lastParams = params;
|
||||
var params = this.normalizeParams(params);
|
||||
var url = this.getUrl();
|
||||
var state = this.getState()[key] || {};
|
||||
|
||||
// just use what's in the cache
|
||||
if (state.data) return;
|
||||
|
||||
this._load(key, url, params);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a record; since we're lazy, just blow away all the store
|
||||
* data, but reload the last thing we fetched
|
||||
*/
|
||||
create(params) {
|
||||
var url = this.getUrl();
|
||||
return $.ajaxJSON(url, "POST", params).then(() => {
|
||||
this.clearState();
|
||||
if (this.lastParams)
|
||||
this.load(this.lastParams);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the next page of data for the given params
|
||||
*/
|
||||
loadMore(params) {
|
||||
var key = this.getKey(params);
|
||||
this.lastParams = params;
|
||||
var state = this.getState()[key] || {};
|
||||
|
||||
if (!state.next) return;
|
||||
|
||||
this._load(key, state.next, {}, {append: true});
|
||||
},
|
||||
|
||||
/**
|
||||
* Load data from the endpoint, following `next` links until
|
||||
* everything has been fetched. Don't be dumb and call this
|
||||
* on users or something :P
|
||||
*/
|
||||
loadAll(params, append) {
|
||||
var key = this.getKey(params);
|
||||
var params = this.normalizeParams(params);
|
||||
this.lastParams = params;
|
||||
var url = this.getUrl();
|
||||
this._loadAll(key, url, params, append);
|
||||
},
|
||||
|
||||
_loadAll(key, url, params, append) {
|
||||
var promise = this._load(key, url, params, {append});
|
||||
if (!promise) return;
|
||||
|
||||
promise.then(() => {
|
||||
var state = this.getState()[key] || {};
|
||||
if (state.next) {
|
||||
this._loadAll(key, state.next, {}, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_load(key, url, params, options) {
|
||||
options = options || {};
|
||||
this.mergeState(key, {loading: true});
|
||||
|
||||
return $.ajaxJSON(url, "GET", params).then((data, _, xhr) => {
|
||||
if (this.jsonKey) {
|
||||
data = data[this.jsonKey];
|
||||
}
|
||||
if (options.wrap) {
|
||||
data = [data];
|
||||
}
|
||||
if (options.append) {
|
||||
data = (this.getStateFor(key).data || []).concat(data);
|
||||
}
|
||||
|
||||
var { next } = parseLinkHeader(xhr);
|
||||
this.mergeState(key, { data, next, loading: false });
|
||||
}, (xhr) => {
|
||||
this.mergeState(key, { error: true, loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
getStateFor(key) {
|
||||
return this.getState()[key] || {};
|
||||
},
|
||||
|
||||
mergeState(key, newState) {
|
||||
var state = this.getState()[key] || {};
|
||||
var overallState = {};
|
||||
overallState[key] = _.extend({}, state, newState);
|
||||
this.setState(overallState);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return whatever results we have for the given params, as well as
|
||||
* useful meta data.
|
||||
*
|
||||
* @return {Object} obj
|
||||
*
|
||||
* @return {Object[]} obj.data
|
||||
* The actual data
|
||||
*
|
||||
* @return {Boolean} obj.error
|
||||
* Indication of whether there was an error
|
||||
*
|
||||
* @return {Boolean} obj.loading
|
||||
* Whether or not we are currently fetching data
|
||||
*
|
||||
* @return {String} obj.next
|
||||
* A URL where we can retrieve the next page of data (if
|
||||
* there is more)
|
||||
*/
|
||||
get(params) {
|
||||
var key = this.getKey(params);
|
||||
return this.getState()[key];
|
||||
}
|
||||
}, spec);
|
||||
}
|
||||
|
||||
return factory;
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
define([
|
||||
"react",
|
||||
"i18n!account_course_user_search",
|
||||
"bower/react-tabs/dist/react-tabs",
|
||||
"underscore",
|
||||
"./CoursesPane",
|
||||
"./CoursesStore",
|
||||
"./TermsStore",
|
||||
"./AccountsTreeStore"
|
||||
], function(React, I18n, ReactTabs, _, CoursesPane, CoursesStore, TermsStore, AccountsTreeStore) {
|
||||
|
||||
var { Tab, Tabs, TabList, TabPanel } = ReactTabs;
|
||||
var { string, bool, shape } = React.PropTypes;
|
||||
|
||||
var stores = [CoursesStore, TermsStore, AccountsTreeStore];
|
||||
|
||||
var App = React.createClass({
|
||||
propTypes: {
|
||||
accountId: string.isRequired,
|
||||
permissions: shape({
|
||||
theme_editor: bool.isRequired
|
||||
}).isRequired
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
selectedTab: 0
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
stores.forEach((s) => s.reset({accountId: this.props.accountId}));
|
||||
},
|
||||
|
||||
handleSelected(selectedTab) {
|
||||
this.setState({selectedTab});
|
||||
},
|
||||
|
||||
render() {
|
||||
var { permissions, accountId } = this.props;
|
||||
|
||||
var tabs = [];
|
||||
var panels = [];
|
||||
if (permissions.can_read_course_list) {
|
||||
tabs.push(<Tab>{I18n.t("Courses")}</Tab>);
|
||||
panels.push(
|
||||
<TabPanel>
|
||||
<CoursesPane />
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
if (permissions.can_read_roster) {
|
||||
tabs.push(<Tab>{I18n.t("People")}</Tab>);
|
||||
panels.push(
|
||||
<TabPanel>
|
||||
TODO People Search
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="pad-box-mini no-sides grid-row middle-xs margin-none">
|
||||
<div className="col-xs-8 padding-none">
|
||||
<h1>{I18n.t("Search")}</h1>
|
||||
</div>
|
||||
<div className="col-xs-4 padding-none align-right">
|
||||
<div>
|
||||
{/* TODO: figure out a way for plugins to inject stuff like
|
||||
<a href="" className="btn button-group">{I18n.t("Analytics")}</a>
|
||||
w/o defining them here
|
||||
*/}
|
||||
{
|
||||
permissions.theme_editor &&
|
||||
<a href={`/accounts/${accountId}/theme_editor`} className="btn button-group">{I18n.t("Theme Editor")}</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
onSelect={this.handleSelected}
|
||||
selectedIndex={this.state.selectedTab}
|
||||
>
|
||||
<TabList>
|
||||
{tabs}
|
||||
</TabList>
|
||||
{panels}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return App;
|
||||
});
|
|
@ -14,4 +14,6 @@ you can use one of them in your feature, please move it into this
|
|||
directory and update the require paths in the other feature.
|
||||
|
||||
- app/jsx/theme_editor/RangeInput.jsx
|
||||
|
||||
- app/jsx/account_course_user_search/IcCheckbox.jsx
|
||||
- app/jsx/account_course_user_search/IcInput.jsx
|
||||
- app/jsx/account_course_user_search/IcSelect.jsx
|
||||
|
|
|
@ -1268,11 +1268,13 @@ class Account < ActiveRecord::Base
|
|||
TAB_SIS_IMPORT = 11
|
||||
TAB_GRADING_STANDARDS = 12
|
||||
TAB_QUESTION_BANKS = 13
|
||||
TAB_ADMIN_TOOLS = 17
|
||||
TAB_SEARCH = 18
|
||||
|
||||
# site admin tabs
|
||||
TAB_PLUGINS = 14
|
||||
TAB_JOBS = 15
|
||||
TAB_DEVELOPER_KEYS = 16
|
||||
TAB_ADMIN_TOOLS = 17
|
||||
|
||||
def external_tool_tabs(opts)
|
||||
tools = ContextExternalTool.active.find_all_for(self, :account_navigation)
|
||||
|
@ -1302,8 +1304,12 @@ class Account < ActiveRecord::Base
|
|||
tabs << { :id => TAB_DEVELOPER_KEYS, :label => t("#account.tab_developer_keys", "Developer Keys"), :css_class => "developer_keys", :href => :developer_keys_path, :no_args => true } if root_account? && self.grants_right?(user, :manage_developer_keys)
|
||||
else
|
||||
tabs = []
|
||||
tabs << { :id => TAB_COURSES, :label => t('#account.tab_courses', "Courses"), :css_class => 'courses', :href => :account_path } if user && self.grants_right?(user, :read_course_list)
|
||||
tabs << { :id => TAB_USERS, :label => t('#account.tab_users', "Users"), :css_class => 'users', :href => :account_users_path } if user && self.grants_right?(user, :read_roster)
|
||||
if feature_enabled?(:course_user_search)
|
||||
tabs << { :id => TAB_SEARCH, :label => t("Search"), :css_class => 'search', :href => :account_path } if user && (grants_right?(user, :read_course_list) || grants_right?(user, :read_roster))
|
||||
else
|
||||
tabs << { :id => TAB_COURSES, :label => t('#account.tab_courses', "Courses"), :css_class => 'courses', :href => :account_path } if user && self.grants_right?(user, :read_course_list)
|
||||
tabs << { :id => TAB_USERS, :label => t('#account.tab_users', "Users"), :css_class => 'users', :href => :account_users_path } if user && self.grants_right?(user, :read_roster)
|
||||
end
|
||||
tabs << { :id => TAB_STATISTICS, :label => t('#account.tab_statistics', "Statistics"), :css_class => 'statistics', :href => :statistics_account_path } if user && self.grants_right?(user, :view_statistics)
|
||||
tabs << { :id => TAB_PERMISSIONS, :label => t('#account.tab_permissions', "Permissions"), :css_class => 'permissions', :href => :account_permissions_path } if user && self.grants_right?(user, :manage_role_overrides)
|
||||
if user && self.grants_right?(user, :manage_outcomes)
|
||||
|
|
|
@ -536,6 +536,10 @@ class Course < ActiveRecord::Base
|
|||
scope :with_enrollments, -> {
|
||||
where("EXISTS (?)", Enrollment.active.where("enrollments.course_id=courses.id"))
|
||||
}
|
||||
scope :with_enrollment_types, -> (types) {
|
||||
types = types.map { |type| "#{type.capitalize}Enrollment" }
|
||||
where("EXISTS (?)", Enrollment.active.where("enrollments.course_id=courses.id").where(type: types))
|
||||
}
|
||||
scope :without_enrollments, -> {
|
||||
where("NOT EXISTS (?)", Enrollment.active.where("enrollments.course_id=courses.id"))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
@import "base/environment";
|
||||
|
||||
.padding-none {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.margin-none {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.react-tabs ul[role=tablist] {
|
||||
padding-left: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-color: $ic-border-light;
|
||||
|
||||
li[role=tab] {
|
||||
padding: 10px 25px;
|
||||
font-size: 1.1em;
|
||||
color: $ic-link-color;
|
||||
|
||||
&[selected] {
|
||||
font-weight: bold;
|
||||
color: $ic-color-dark;
|
||||
border-color: $ic-border-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-right: 10px;
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.courses-list td {
|
||||
vertical-align: top;
|
||||
}
|
||||
.courses-list__published-icon {
|
||||
color: $ic-color-success;
|
||||
}
|
||||
|
||||
.ReactModalPortal .ReactModal__Content {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.flex-grow-2 {
|
||||
flex-grow: 2;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<%
|
||||
@active_tab = "search"
|
||||
content_for :page_title, @account.name
|
||||
add_crumb t("Search"), account_path(@account)
|
||||
js_bundle :account_course_user_search
|
||||
css_bundle :account_course_user_search
|
||||
js_env ACCOUNT_ID: @account.id,
|
||||
PERMISSIONS: @permissions
|
||||
%>
|
|
@ -30,6 +30,7 @@
|
|||
"moment": "~2.10.6",
|
||||
"reflux": "~0.2.7",
|
||||
"axios": "~0.5.4",
|
||||
"when": "~3.7.3"
|
||||
"when": "~3.7.3",
|
||||
"react-tabs": "https://github.com/rackt/react-tabs.git#1ad1031ec4a50784225a05611c4959d15745fc3d"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ module Api::V1::Course
|
|||
include Api::V1::EnrollmentTerm
|
||||
include Api::V1::SectionEnrollments
|
||||
include Api::V1::PostGradesStatus
|
||||
include Api::V1::User
|
||||
|
||||
def course_settings_json(course)
|
||||
settings = {}
|
||||
|
@ -84,6 +85,7 @@ module Api::V1::Course
|
|||
hash['total_students'] = course.students.count if includes.include?('total_students')
|
||||
hash['passback_status'] = post_grades_status_json(course) if includes.include?('passback_status')
|
||||
hash['is_favorite'] = course.favorite_for_user?(user) if includes.include?('favorites')
|
||||
hash['teachers'] = course.teachers.map { |teacher| user_display_json(teacher) } if includes.include?('teachers')
|
||||
add_helper_dependant_entries(hash, course, builder)
|
||||
apply_nickname(hash, course, user) if user
|
||||
end
|
||||
|
|
|
@ -70,7 +70,11 @@ module Canvas
|
|||
end
|
||||
|
||||
def map
|
||||
@map ||= Canvas::RequireJs::ClientAppExtension.map.to_json
|
||||
@map ||= Canvas::RequireJs::ClientAppExtension.map.merge({
|
||||
'*' => {
|
||||
React: "react" # for misbehaving UMD like react-tabs
|
||||
}
|
||||
}).to_json
|
||||
end
|
||||
|
||||
def bundles
|
||||
|
|
|
@ -404,6 +404,14 @@ END
|
|||
applies_to: 'Course',
|
||||
state: 'allowed',
|
||||
root_opt_in: true
|
||||
},
|
||||
'course_user_search' => {
|
||||
display_name: -> { I18n.t('Course and User Search') },
|
||||
description: -> { I18n.t('Updated UI for searching and displaying users and courses within an account.') },
|
||||
applies_to: 'Account',
|
||||
state: 'hidden',
|
||||
development: true,
|
||||
root_opt_in: true
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "react-tabs",
|
||||
"homepage": "https://github.com/rackt/react-tabs",
|
||||
"authors": [
|
||||
"Matt Zabriskie"
|
||||
],
|
||||
"description": "React tabs component",
|
||||
"main": "./dist/react-tabs.js",
|
||||
"keywords": [
|
||||
"react",
|
||||
"tabs"
|
||||
],
|
||||
"license": "MIT",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"build",
|
||||
"examples",
|
||||
"lib",
|
||||
"node_modules",
|
||||
"specs",
|
||||
"package.json"
|
||||
],
|
||||
"_release": "1ad1031ec4",
|
||||
"_resolution": {
|
||||
"type": "commit",
|
||||
"commit": "1ad1031ec4a50784225a05611c4959d15745fc3d"
|
||||
},
|
||||
"_source": "https://github.com/rackt/react-tabs.git",
|
||||
"_target": "1ad1031ec4a50784225a05611c4959d15745fc3d",
|
||||
"_originalSource": "https://github.com/rackt/react-tabs.git"
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
v0.4.1 - Wed, 09 Sep 2015 19:18:50 GMT
|
||||
--------------------------------------
|
||||
|
||||
-
|
||||
|
||||
|
||||
v0.4.0 - Tue, 18 Aug 2015 22:53:59 GMT
|
||||
--------------------------------------
|
||||
|
||||
-
|
||||
|
||||
|
||||
v0.3.0 - Tue, 11 Aug 2015 00:42:23 GMT
|
||||
--------------------------------------
|
||||
|
||||
- [0eb43e5](../../commit/0eb43e5) [added] Support for disabling tabs
|
||||
|
||||
|
||||
v0.2.1 - Fri, 26 Jun 2015 19:35:50 GMT
|
||||
--------------------------------------
|
||||
|
||||
- [5132966](../../commit/5132966) [added] Bower support closes #22
|
||||
- [3f43e89](../../commit/3f43e89) [fixed] Issue with React being included twice closes #23
|
||||
|
||||
|
||||
# Changelog
|
||||
|
||||
### 0.1.0 (Jul 18, 2014)
|
||||
|
||||
- Initial release
|
||||
|
||||
### 0.1.1 (Jul 19, 2014)
|
||||
|
||||
- Fixing warning: Invalid access to component property
|
||||
- Fixing style weirdness in Firefox
|
||||
|
||||
### 0.1.2 (Jul 23, 2014)
|
||||
|
||||
- Making Tab and TabPanel to be stateless
|
||||
- Throwing Error when Tab count and TabPanel count aren't equal
|
||||
|
||||
### 0.2.0 (Jun 07, 2015)
|
||||
|
||||
- Allowing children of Tab to select Tab ([#9](https://github.com/rackt/react-tabs/pull/9))
|
||||
- Only render the selected TabPanel
|
||||
- Upgrading to React 0.13
|
||||
- Removing JSX
|
||||
- Fixing issue with focus management ([#7](https://github.com/rackt/react-tabs/pull/7))
|
||||
- Fixing issue caused by no children being provided ([#6](https://github.com/rackt/react-tabs/issues/6))
|
||||
- Fixing issue that made dynamic Tabs difficult
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2015 by Matt Zabriskie
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -0,0 +1,108 @@
|
|||
# react-tabs [![Build Status](https://travis-ci.org/rackt/react-tabs.svg?branch=master)](https://travis-ci.org/rackt/react-tabs)
|
||||
|
||||
React tabs component
|
||||
|
||||
## Installing
|
||||
|
||||
```bash
|
||||
$ npm install react-tabs
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||
http://rackt.github.io/react-tabs/example/
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
/** @jsx React.DOM */
|
||||
var React = require('react');
|
||||
var ReactTabs = require('react-tabs');
|
||||
var Tab = ReactTabs.Tab;
|
||||
var Tabs = ReactTabs.Tabs;
|
||||
var TabList = ReactTabs.TabList;
|
||||
var TabPanel = ReactTabs.TabPanel;
|
||||
|
||||
var App = React.createClass({
|
||||
handleSelect: function (index, last) {
|
||||
console.log('Selected tab: ' + index + ', Last tab: ' + last);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return (
|
||||
{/*
|
||||
<Tabs/> is a composite component and acts as the main container.
|
||||
|
||||
`onSelect` is called whenever a tab is selected. The handler for
|
||||
this function will be passed the current index as well as the last index.
|
||||
|
||||
`selectedIndex` is the tab to select when first rendered. By default
|
||||
the first (index 0) tab will be selected.
|
||||
|
||||
`forceRenderTabPanel` By default this react-tabs will only render the selected
|
||||
tab's contents. Setting `forceRenderTabPanel` to `true` allows you to override the
|
||||
default behavior, which may be useful in some circumstances (such as animating between tabs).
|
||||
|
||||
*/}
|
||||
|
||||
<Tabs
|
||||
onSelect={this.handleSelected}
|
||||
selectedIndex={2}
|
||||
>
|
||||
|
||||
{/*
|
||||
<TabList/> is a composit component and is the container for the <Tab/>s.
|
||||
*/}
|
||||
|
||||
<TabList>
|
||||
|
||||
{/*
|
||||
<Tab/> is the actual tab component that users will interact with.
|
||||
|
||||
Selecting a tab can be done by either clicking with the mouse,
|
||||
or by using the keyboard tab to give focus then navigating with
|
||||
the arrow keys (right/down to select tab to the right of selected,
|
||||
left/up to select tab to the left of selected).
|
||||
|
||||
The content of the <Tab/> (this.props.children) will be shown as the label.
|
||||
*/}
|
||||
|
||||
<Tab>Foo</Tab>
|
||||
<Tab>Bar</Tab>
|
||||
<Tab>Baz</Tab>
|
||||
</TabList>
|
||||
|
||||
{/*
|
||||
<TabPanel/> is the content for the tab.
|
||||
|
||||
There should be an equal number of <Tab/> and <TabPanel/> components.
|
||||
<Tab/> and <TabPanel/> components are tied together by the order in
|
||||
which they appear. The first (index 0) <Tab/> will be associated with
|
||||
the <TabPanel/> of the same index. Running this example when
|
||||
`selectedIndex` is 0 the tab with the label "Foo" will be selected
|
||||
and the content shown will be "Hello from Foo".
|
||||
|
||||
As with <Tab/> the content of <TabPanel/> will be shown as the content.
|
||||
*/}
|
||||
|
||||
<TabPanel>
|
||||
<h2>Hello from Foo</h2>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<h2>Hello from Bar</h2>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<h2>Hello from Baz</h2>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
React.render(<App/>, document.getElementById('container'));
|
||||
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "react-tabs",
|
||||
"version": "0.4.1",
|
||||
"homepage": "https://github.com/rackt/react-tabs",
|
||||
"authors": [
|
||||
"Matt Zabriskie"
|
||||
],
|
||||
"description": "React tabs component",
|
||||
"main": "./dist/react-tabs.js",
|
||||
"keywords": [
|
||||
"react",
|
||||
"tabs"
|
||||
],
|
||||
"license": "MIT",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"build",
|
||||
"examples",
|
||||
"lib",
|
||||
"node_modules",
|
||||
"specs",
|
||||
"package.json"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,716 @@
|
|||
(function webpackUniversalModuleDefinition(root, factory) {
|
||||
if(typeof exports === 'object' && typeof module === 'object')
|
||||
module.exports = factory(require("React"));
|
||||
else if(typeof define === 'function' && define.amd)
|
||||
define(["React"], factory);
|
||||
else if(typeof exports === 'object')
|
||||
exports["ReactTabs"] = factory(require("React"));
|
||||
else
|
||||
root["ReactTabs"] = factory(root["React"]);
|
||||
})(this, function(__WEBPACK_EXTERNAL_MODULE_2__) {
|
||||
return /******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId])
|
||||
/******/ return installedModules[moduleId].exports;
|
||||
/******/
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = {
|
||||
/******/ exports: {},
|
||||
/******/ id: moduleId,
|
||||
/******/ loaded: false
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.loaded = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules;
|
||||
/******/
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules;
|
||||
/******/
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = "";
|
||||
/******/
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(0);
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ([
|
||||
/* 0 */
|
||||
/***/ function(module, exports, __webpack_require__) {
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
Tabs: __webpack_require__(1),
|
||||
TabList: __webpack_require__(7),
|
||||
Tab: __webpack_require__(6),
|
||||
TabPanel: __webpack_require__(9)
|
||||
};
|
||||
|
||||
/***/ },
|
||||
/* 1 */
|
||||
/***/ function(module, exports, __webpack_require__) {
|
||||
|
||||
/* eslint indent:0 */
|
||||
'use strict';
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
|
||||
|
||||
var _reactAddons = __webpack_require__(2);
|
||||
|
||||
var _reactAddons2 = _interopRequireDefault(_reactAddons);
|
||||
|
||||
var _jsStylesheet = __webpack_require__(3);
|
||||
|
||||
var _jsStylesheet2 = _interopRequireDefault(_jsStylesheet);
|
||||
|
||||
var _helpersUuid = __webpack_require__(4);
|
||||
|
||||
var _helpersUuid2 = _interopRequireDefault(_helpersUuid);
|
||||
|
||||
var _helpersChildrenPropType = __webpack_require__(5);
|
||||
|
||||
var _helpersChildrenPropType2 = _interopRequireDefault(_helpersChildrenPropType);
|
||||
|
||||
// Determine if a node from event.target is a Tab element
|
||||
function isTabNode(node) {
|
||||
return node.nodeName === 'LI' && node.getAttribute('role') === 'tab';
|
||||
}
|
||||
|
||||
// Determine if a tab node is disabled
|
||||
function isTabDisabled(node) {
|
||||
return node.getAttribute('aria-disabled') === 'true';
|
||||
}
|
||||
|
||||
module.exports = _reactAddons2['default'].createClass({
|
||||
displayName: 'Tabs',
|
||||
|
||||
propTypes: {
|
||||
selectedIndex: _reactAddons.PropTypes.number,
|
||||
onSelect: _reactAddons.PropTypes.func,
|
||||
focus: _reactAddons.PropTypes.bool,
|
||||
children: _helpersChildrenPropType2['default'],
|
||||
forceRenderTabPanel: _reactAddons.PropTypes.bool
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
forceRenderTabPanel: _reactAddons.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps: function getDefaultProps() {
|
||||
return {
|
||||
selectedIndex: -1,
|
||||
focus: false,
|
||||
forceRenderTabPanel: false
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function getInitialState() {
|
||||
return this.copyPropsToState(this.props);
|
||||
},
|
||||
|
||||
getChildContext: function getChildContext() {
|
||||
return {
|
||||
forceRenderTabPanel: this.props.forceRenderTabPanel
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function componentWillMount() {
|
||||
(0, _jsStylesheet2['default'])(__webpack_require__(8));
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function componentWillReceiveProps(newProps) {
|
||||
this.setState(this.copyPropsToState(newProps));
|
||||
},
|
||||
|
||||
handleClick: function handleClick(e) {
|
||||
var node = e.target;
|
||||
do {
|
||||
if (isTabNode(node)) {
|
||||
if (isTabDisabled(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var index = [].slice.call(node.parentNode.children).indexOf(node);
|
||||
this.setSelected(index);
|
||||
return;
|
||||
}
|
||||
} while ((node = node.parentNode) !== null);
|
||||
},
|
||||
|
||||
handleKeyDown: function handleKeyDown(e) {
|
||||
if (isTabNode(e.target)) {
|
||||
var index = this.state.selectedIndex;
|
||||
var preventDefault = false;
|
||||
|
||||
// Select next tab to the left
|
||||
if (e.keyCode === 37 || e.keyCode === 38) {
|
||||
index = this.getPrevTab(index);
|
||||
preventDefault = true;
|
||||
}
|
||||
// Select next tab to the right
|
||||
/* eslint brace-style:0 */
|
||||
else if (e.keyCode === 39 || e.keyCode === 40) {
|
||||
index = this.getNextTab(index);
|
||||
preventDefault = true;
|
||||
}
|
||||
|
||||
// This prevents scrollbars from moving around
|
||||
if (preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
this.setSelected(index, true);
|
||||
}
|
||||
},
|
||||
|
||||
setSelected: function setSelected(index, focus) {
|
||||
// Don't do anything if nothing has changed
|
||||
if (index === this.state.selectedIndex) return;
|
||||
// Check index boundary
|
||||
if (index < 0 || index >= this.getTabsCount()) return;
|
||||
|
||||
// Keep reference to last index for event handler
|
||||
var last = this.state.selectedIndex;
|
||||
|
||||
// Update selected index
|
||||
this.setState({ selectedIndex: index, focus: focus === true });
|
||||
|
||||
// Call change event handler
|
||||
if (typeof this.props.onSelect === 'function') {
|
||||
this.props.onSelect(index, last);
|
||||
}
|
||||
},
|
||||
|
||||
getNextTab: function getNextTab(index) {
|
||||
var count = this.getTabsCount();
|
||||
|
||||
// Look for non-disabled tab from index to the last tab on the right
|
||||
for (var i = index + 1; i < count; i++) {
|
||||
var tab = this.getTab(i);
|
||||
if (!isTabDisabled(tab.getDOMNode())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// If no tab found, continue searching from first on left to index
|
||||
for (var i = 0; i < index; i++) {
|
||||
var tab = this.getTab(i);
|
||||
if (!isTabDisabled(tab.getDOMNode())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// No tabs are disabled, return index
|
||||
return index;
|
||||
},
|
||||
|
||||
getPrevTab: function getPrevTab(index) {
|
||||
var i = index;
|
||||
|
||||
// Look for non-disabled tab from index to first tab on the left
|
||||
while (i--) {
|
||||
var tab = this.getTab(i);
|
||||
if (!isTabDisabled(tab.getDOMNode())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// If no tab found, continue searching from last tab on right to index
|
||||
i = this.getTabsCount();
|
||||
while (i-- > index) {
|
||||
var tab = this.getTab(i);
|
||||
if (!isTabDisabled(tab.getDOMNode())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// No tabs are disabled, return index
|
||||
return index;
|
||||
},
|
||||
|
||||
getTabsCount: function getTabsCount() {
|
||||
return this.props.children && this.props.children[0] ? _reactAddons2['default'].Children.count(this.props.children[0].props.children) : 0;
|
||||
},
|
||||
|
||||
getPanelsCount: function getPanelsCount() {
|
||||
return _reactAddons2['default'].Children.count(this.props.children.slice(1));
|
||||
},
|
||||
|
||||
getTabList: function getTabList() {
|
||||
return this.refs.tablist;
|
||||
},
|
||||
|
||||
getTab: function getTab(index) {
|
||||
return this.refs['tabs-' + index];
|
||||
},
|
||||
|
||||
getPanel: function getPanel(index) {
|
||||
return this.refs['panels-' + index];
|
||||
},
|
||||
|
||||
getChildren: function getChildren() {
|
||||
var index = 0;
|
||||
var count = 0;
|
||||
var children = this.props.children;
|
||||
var state = this.state;
|
||||
var tabIds = this.tabIds = this.tabIds || [];
|
||||
var panelIds = this.panelIds = this.panelIds || [];
|
||||
var diff = this.tabIds.length - this.getTabsCount();
|
||||
|
||||
// Add ids if new tabs have been added
|
||||
// Don't bother removing ids, just keep them in case they are added again
|
||||
// This is more efficient, and keeps the uuid counter under control
|
||||
while (diff++ < 0) {
|
||||
tabIds.push((0, _helpersUuid2['default'])());
|
||||
panelIds.push((0, _helpersUuid2['default'])());
|
||||
}
|
||||
|
||||
// Map children to dynamically setup refs
|
||||
return _reactAddons2['default'].Children.map(children, function (child) {
|
||||
var result = null;
|
||||
|
||||
// Clone TabList and Tab components to have refs
|
||||
if (count++ === 0) {
|
||||
// TODO try setting the uuid in the "constructor" for `Tab`/`TabPanel`
|
||||
result = _reactAddons2['default'].addons.cloneWithProps(child, {
|
||||
ref: 'tablist',
|
||||
children: _reactAddons2['default'].Children.map(child.props.children, function (tab) {
|
||||
var ref = 'tabs-' + index;
|
||||
var id = tabIds[index];
|
||||
var panelId = panelIds[index];
|
||||
var selected = state.selectedIndex === index;
|
||||
var focus = selected && state.focus;
|
||||
|
||||
index++;
|
||||
|
||||
return _reactAddons2['default'].addons.cloneWithProps(tab, {
|
||||
ref: ref,
|
||||
id: id,
|
||||
panelId: panelId,
|
||||
selected: selected,
|
||||
focus: focus
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// Reset index for panels
|
||||
index = 0;
|
||||
}
|
||||
// Clone TabPanel components to have refs
|
||||
else {
|
||||
var ref = 'panels-' + index;
|
||||
var id = panelIds[index];
|
||||
var tabId = tabIds[index];
|
||||
var selected = state.selectedIndex === index;
|
||||
|
||||
index++;
|
||||
|
||||
result = _reactAddons2['default'].addons.cloneWithProps(child, {
|
||||
ref: ref,
|
||||
id: id,
|
||||
tabId: tabId,
|
||||
selected: selected
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
},
|
||||
|
||||
render: function render() {
|
||||
var _this = this;
|
||||
|
||||
// This fixes an issue with focus management.
|
||||
//
|
||||
// Ultimately, when focus is true, and an input has focus,
|
||||
// and any change on that input causes a state change/re-render,
|
||||
// focus gets sent back to the active tab, and input loses focus.
|
||||
//
|
||||
// Since the focus state only needs to be remembered
|
||||
// for the current render, we can reset it once the
|
||||
// render has happened.
|
||||
//
|
||||
// Don't use setState, because we don't want to re-render.
|
||||
//
|
||||
// See https://github.com/mzabriskie/react-tabs/pull/7
|
||||
if (this.state.focus) {
|
||||
setTimeout(function () {
|
||||
_this.state.focus = false;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return _reactAddons2['default'].createElement(
|
||||
'div',
|
||||
{
|
||||
className: 'react-tabs',
|
||||
onClick: this.handleClick,
|
||||
onKeyDown: this.handleKeyDown
|
||||
},
|
||||
this.getChildren()
|
||||
);
|
||||
},
|
||||
|
||||
// This is an anti-pattern, so sue me
|
||||
copyPropsToState: function copyPropsToState(props) {
|
||||
var selectedIndex = props.selectedIndex;
|
||||
|
||||
// If no selectedIndex prop was supplied, then try
|
||||
// preserving the existing selectedIndex from state.
|
||||
// If the state has not selectedIndex, default
|
||||
// to the first tab in the TabList.
|
||||
//
|
||||
// TODO: Need automation testing around this
|
||||
// Manual testing can be done using examples/focus
|
||||
// See 'should preserve selectedIndex when typing' in specs/Tabs.spec.js
|
||||
if (selectedIndex === -1) {
|
||||
if (this.state && this.state.selectedIndex) {
|
||||
selectedIndex = this.state.selectedIndex;
|
||||
} else {
|
||||
selectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selectedIndex: selectedIndex,
|
||||
focus: props.focus
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/***/ },
|
||||
/* 2 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
module.exports = __WEBPACK_EXTERNAL_MODULE_2__;
|
||||
|
||||
/***/ },
|
||||
/* 3 */
|
||||
/***/ function(module, exports, __webpack_require__) {
|
||||
|
||||
!(function() {
|
||||
function jss(blocks) {
|
||||
var css = [];
|
||||
for (var block in blocks)
|
||||
css.push(createStyleBlock(block, blocks[block]));
|
||||
injectCSS(css);
|
||||
}
|
||||
|
||||
function createStyleBlock(selector, rules) {
|
||||
return selector + ' {\n' + parseRules(rules) + '\n}';
|
||||
}
|
||||
|
||||
function parseRules(rules) {
|
||||
var css = [];
|
||||
for (var rule in rules)
|
||||
css.push(' '+rule+': '+rules[rule]+';');
|
||||
return css.join('\n');
|
||||
}
|
||||
|
||||
function injectCSS(css) {
|
||||
var style = document.getElementById('jss-styles');
|
||||
if (!style) {
|
||||
style = document.createElement('style');
|
||||
style.setAttribute('id', 'jss-styles');
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
head.insertBefore(style, head.firstChild);
|
||||
}
|
||||
var node = document.createTextNode(css.join('\n\n'));
|
||||
style.appendChild(node);
|
||||
}
|
||||
|
||||
if (true)
|
||||
module.exports = jss;
|
||||
else
|
||||
window.jss = jss;
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
/***/ },
|
||||
/* 4 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
// Get a universally unique identifier
|
||||
'use strict';
|
||||
|
||||
var count = 0;
|
||||
module.exports = function uuid() {
|
||||
return 'react-tabs-' + count++;
|
||||
};
|
||||
|
||||
/***/ },
|
||||
/* 5 */
|
||||
/***/ function(module, exports, __webpack_require__) {
|
||||
|
||||
'use strict';
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
|
||||
|
||||
var _react = __webpack_require__(2);
|
||||
|
||||
var _react2 = _interopRequireDefault(_react);
|
||||
|
||||
var _componentsTab = __webpack_require__(6);
|
||||
|
||||
var _componentsTab2 = _interopRequireDefault(_componentsTab);
|
||||
|
||||
var _componentsTabList = __webpack_require__(7);
|
||||
|
||||
var _componentsTabList2 = _interopRequireDefault(_componentsTabList);
|
||||
|
||||
module.exports = function childrenPropTypes(props, propName) {
|
||||
var error = undefined;
|
||||
var tabsCount = 0;
|
||||
var panelsCount = 0;
|
||||
var children = props[propName];
|
||||
|
||||
_react2['default'].Children.forEach(children, function (child) {
|
||||
if (child.type === _componentsTabList2['default']) {
|
||||
_react2['default'].Children.forEach(child.props.children, function (c) {
|
||||
if (c.type === _componentsTab2['default']) {
|
||||
tabsCount++;
|
||||
} else {
|
||||
error = new Error('Expected `Tab` but found `' + (c.type.displayName || c.type) + '`');
|
||||
}
|
||||
});
|
||||
} else if (child.type.displayName === 'TabPanel') {
|
||||
panelsCount++;
|
||||
} else {
|
||||
error = new Error('Expected `TabList` or `TabPanel` but found `' + (child.type.displayName || child.type) + '`');
|
||||
}
|
||||
});
|
||||
|
||||
if (tabsCount !== panelsCount) {
|
||||
error = new Error('There should be an equal number of `Tabs` and `TabPanels`. ' + 'Received ' + tabsCount + ' `Tabs` and ' + panelsCount + ' `TabPanels`.');
|
||||
}
|
||||
|
||||
return error;
|
||||
};
|
||||
|
||||
/***/ },
|
||||
/* 6 */
|
||||
/***/ function(module, exports, __webpack_require__) {
|
||||
|
||||
/* eslint indent:0 */
|
||||
'use strict';
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
|
||||
|
||||
var _react = __webpack_require__(2);
|
||||
|
||||
var _react2 = _interopRequireDefault(_react);
|
||||
|
||||
function syncNodeAttributes(node, props) {
|
||||
if (props.selected) {
|
||||
node.setAttribute('tabindex', 0);
|
||||
node.setAttribute('selected', 'selected');
|
||||
if (props.focus) {
|
||||
node.focus();
|
||||
}
|
||||
} else {
|
||||
node.removeAttribute('tabindex');
|
||||
node.removeAttribute('selected');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = _react2['default'].createClass({
|
||||
displayName: 'Tab',
|
||||
|
||||
propTypes: {
|
||||
id: _react.PropTypes.string,
|
||||
selected: _react.PropTypes.bool,
|
||||
disabled: _react.PropTypes.bool,
|
||||
panelId: _react.PropTypes.string,
|
||||
children: _react.PropTypes.oneOfType([_react.PropTypes.array, _react.PropTypes.object, _react.PropTypes.string])
|
||||
},
|
||||
|
||||
getDefaultProps: function getDefaultProps() {
|
||||
return {
|
||||
focus: false,
|
||||
selected: false,
|
||||
id: null,
|
||||
panelId: null
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function componentDidMount() {
|
||||
syncNodeAttributes(this.getDOMNode(), this.props);
|
||||
},
|
||||
|
||||
componentDidUpdate: function componentDidUpdate() {
|
||||
syncNodeAttributes(this.getDOMNode(), this.props);
|
||||
},
|
||||
|
||||
render: function render() {
|
||||
return _react2['default'].createElement(
|
||||
'li',
|
||||
{
|
||||
role: 'tab',
|
||||
id: this.props.id,
|
||||
'aria-selected': this.props.selected ? 'true' : 'false',
|
||||
'aria-expanded': this.props.selected ? 'true' : 'false',
|
||||
'aria-disabled': this.props.disabled ? 'true' : 'false',
|
||||
'aria-controls': this.props.panelId
|
||||
},
|
||||
this.props.children
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/***/ },
|
||||
/* 7 */
|
||||
/***/ function(module, exports, __webpack_require__) {
|
||||
|
||||
/* eslint indent:0 */
|
||||
'use strict';
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
|
||||
|
||||
var _react = __webpack_require__(2);
|
||||
|
||||
var _react2 = _interopRequireDefault(_react);
|
||||
|
||||
module.exports = _react2['default'].createClass({
|
||||
displayName: 'TabList',
|
||||
|
||||
propTypes: {
|
||||
children: _react.PropTypes.oneOfType([_react.PropTypes.object, _react.PropTypes.array])
|
||||
},
|
||||
|
||||
render: function render() {
|
||||
return _react2['default'].createElement(
|
||||
'ul',
|
||||
{ role: 'tablist' },
|
||||
this.props.children
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/***/ },
|
||||
/* 8 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
'.react-tabs [role=tablist]': {
|
||||
'border-bottom': '1px solid #aaa',
|
||||
'margin': '0 0 10px',
|
||||
'padding': '0'
|
||||
},
|
||||
|
||||
'.react-tabs [role=tab]': {
|
||||
'display': 'inline-block',
|
||||
'border': '1px solid transparent',
|
||||
'border-bottom': 'none',
|
||||
'bottom': '-1px',
|
||||
'position': 'relative',
|
||||
'list-style': 'none',
|
||||
'padding': '6px 12px',
|
||||
'cursor': 'pointer'
|
||||
},
|
||||
|
||||
'.react-tabs [role=tab][aria-selected=true]': {
|
||||
'background': '#fff',
|
||||
'border-color': '#aaa',
|
||||
'border-radius': '5px 5px 0 0',
|
||||
'-moz-border-radius': '5px 5px 0 0',
|
||||
'-webkit-border-radius': '5px 5px 0 0'
|
||||
},
|
||||
|
||||
'.react-tabs [role=tab][aria-disabled=true]': {
|
||||
'color': 'GrayText',
|
||||
'cursor': 'default'
|
||||
},
|
||||
|
||||
'.react-tabs [role=tab]:focus': {
|
||||
'box-shadow': '0 0 5px hsl(208, 99%, 50%)',
|
||||
'border-color': 'hsl(208, 99%, 50%)',
|
||||
'outline': 'none'
|
||||
},
|
||||
|
||||
'.react-tabs [role=tab]:focus:after': {
|
||||
'content': '""',
|
||||
'position': 'absolute',
|
||||
'height': '5px',
|
||||
'left': '-4px',
|
||||
'right': '-4px',
|
||||
'bottom': '-5px',
|
||||
'background': '#fff'
|
||||
}
|
||||
};
|
||||
|
||||
/***/ },
|
||||
/* 9 */
|
||||
/***/ function(module, exports, __webpack_require__) {
|
||||
|
||||
/* eslint indent:0 */
|
||||
'use strict';
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
|
||||
|
||||
var _react = __webpack_require__(2);
|
||||
|
||||
var _react2 = _interopRequireDefault(_react);
|
||||
|
||||
module.exports = _react2['default'].createClass({
|
||||
displayName: 'TabPanel',
|
||||
|
||||
propTypes: {
|
||||
selected: _react.PropTypes.bool,
|
||||
id: _react.PropTypes.string,
|
||||
tabId: _react.PropTypes.string,
|
||||
children: _react.PropTypes.oneOfType([_react.PropTypes.array, _react.PropTypes.object, _react.PropTypes.string])
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
forceRenderTabPanel: _react.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps: function getDefaultProps() {
|
||||
return {
|
||||
selected: false,
|
||||
id: null,
|
||||
tabId: null
|
||||
};
|
||||
},
|
||||
|
||||
render: function render() {
|
||||
var children = this.context.forceRenderTabPanel || this.props.selected ? this.props.children : null;
|
||||
|
||||
return _react2['default'].createElement(
|
||||
'div',
|
||||
{
|
||||
role: 'tabpanel',
|
||||
id: this.props.id,
|
||||
'aria-labeledby': this.props.tabId,
|
||||
style: { display: this.props.selected ? null : 'none' }
|
||||
},
|
||||
children
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/***/ }
|
||||
/******/ ])
|
||||
});
|
||||
;
|
||||
//# sourceMappingURL=react-tabs.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,109 @@
|
|||
require File.expand_path(File.dirname(__FILE__) + '/../common')
|
||||
|
||||
describe "new account course search" do
|
||||
include_context "in-process server selenium tests"
|
||||
|
||||
before :once do
|
||||
account_model
|
||||
@account.enable_feature!(:course_user_search)
|
||||
account_admin_user(:account => @account, :active_all => true)
|
||||
end
|
||||
|
||||
before do
|
||||
user_session(@user)
|
||||
end
|
||||
|
||||
def get_rows
|
||||
ff('.courses-list > tbody > tr')
|
||||
end
|
||||
|
||||
it "should not show the courses tab without permission" do
|
||||
@account.role_overrides.create! :role => admin_role, :permission => 'read_course_list', :enabled => false
|
||||
|
||||
get "/accounts/#{@account.id}"
|
||||
|
||||
expect(".react-tabs > ul").to_not include_text("Courses")
|
||||
end
|
||||
|
||||
it "should hide courses without enrollments if checked" do
|
||||
empty_course = course(:account => @account, :course_name => "no enrollments")
|
||||
not_empty_course = course(:account => @account, :course_name => "yess enrollments", :active_all => true)
|
||||
student_in_course(:course => not_empty_course, :active_all => true)
|
||||
|
||||
get "/accounts/#{@account.id}"
|
||||
|
||||
expect(get_rows.count).to eq 2
|
||||
|
||||
f('.course_search_bar input[type=checkbox]').click # hide anymore
|
||||
f('.course_search_bar button').click
|
||||
wait_for_ajaximations
|
||||
|
||||
rows = get_rows
|
||||
expect(rows.count).to eq 1
|
||||
expect(rows.first).to include_text(not_empty_course.name)
|
||||
expect(rows.first).to_not include_text(empty_course.name)
|
||||
end
|
||||
|
||||
it "should paginate" do
|
||||
11.times do |x|
|
||||
course(:account => @account, :course_name => "course #{x + 1}")
|
||||
end
|
||||
|
||||
get "/accounts/#{@account.id}"
|
||||
|
||||
expect(get_rows.count).to eq 10
|
||||
|
||||
f(".load_more_courses").click
|
||||
wait_for_ajaximations
|
||||
|
||||
expect(get_rows.count).to eq 11
|
||||
expect(f(".load_more_courses")).to be_nil
|
||||
end
|
||||
|
||||
it "should search by term" do
|
||||
|
||||
term = @account.enrollment_terms.create!(:name => "some term")
|
||||
term_course = course(:account => @account, :course_name => "term course")
|
||||
term_course.enrollment_term = term
|
||||
term_course.save!
|
||||
|
||||
other_course = course(:account => @account, :course_name => "other course")
|
||||
|
||||
get "/accounts/#{@account.id}"
|
||||
|
||||
click_option(".course_search_bar select", term.name)
|
||||
f('.course_search_bar button').click
|
||||
wait_for_ajaximations
|
||||
|
||||
rows = get_rows
|
||||
expect(rows.count).to eq 1
|
||||
expect(rows.first).to include_text(term_course.name)
|
||||
end
|
||||
|
||||
it "should search by name" do
|
||||
match_course = course(:account => @account, :course_name => "course with a search term")
|
||||
not_match_course = course(:account => @account, :course_name => "diffrient cuorse")
|
||||
|
||||
get "/accounts/#{@account.id}"
|
||||
|
||||
f('.course_search_bar input[type=text]').send_keys('search')
|
||||
f('.course_search_bar button').click
|
||||
wait_for_ajaximations
|
||||
|
||||
rows = get_rows
|
||||
expect(rows.count).to eq 1
|
||||
expect(rows.first).to include_text(match_course.name)
|
||||
end
|
||||
|
||||
it "should show teachers" do
|
||||
course(:account => @account)
|
||||
user(:name => "some teacher")
|
||||
teacher_in_course(:course => @course, :user => @user)
|
||||
|
||||
get "/accounts/#{@account.id}"
|
||||
|
||||
user_link = get_rows.first.find("a.user_link")
|
||||
expect(user_link).to include_text(@user.name)
|
||||
expect(user_link['href']).to eq user_url(@user)
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue