add locked view for mastery scales and proficiency calculation
Add read-only view of mastery scale and of proficiency calculation closes OUT-3767 flag=account_level_mastery_scales test plan: - as Admin, enable "Account-level Mastery Scales" - log in using an account with permissions "Outcome Proficiency Calculations - add / edit" and "Outcome Proficiency Scales - add / edit" disabled - Go to outcomes, Mastery tab, you should not see a form, only the data - Go to outcomes, Calculation tab, you should not see a form, only the data Change-Id: I3d6e7fefcd56ceb567dc59c3238ce8cc27553054 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/248943 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Pat Renner <prenner@instructure.com> Reviewed-by: Augusto Callejas <acallejas@instructure.com> QA-Review: Pat Renner <prenner@instructure.com> Product-Review: Jody Sailor
This commit is contained in:
parent
718eb4a736
commit
dc68ff2f37
|
@ -45,7 +45,9 @@ class OutcomesController < ApplicationController
|
|||
:manage_outcomes => @context.grants_right?(@current_user, session, :manage_outcomes),
|
||||
:manage_rubrics => @context.grants_right?(@current_user, session, :manage_rubrics),
|
||||
:manage_courses => @context.grants_right?(@current_user, session, :manage_courses),
|
||||
:import_outcomes => @context.grants_right?(@current_user, session, :import_outcomes)
|
||||
:import_outcomes => @context.grants_right?(@current_user, session, :import_outcomes),
|
||||
:manage_proficiency_scales => @context.grants_right?(@current_user, session, :manage_proficiency_scales),
|
||||
:manage_proficiency_calculations => @context.grants_right?(@current_user, session, :manage_proficiency_calculations)
|
||||
})
|
||||
|
||||
set_tutorial_js_env
|
||||
|
|
|
@ -109,7 +109,9 @@ const Display = ({calculationInt, currentMethod}) => {
|
|||
</Text>
|
||||
{currentMethod.validRange && (
|
||||
<>
|
||||
<Heading level="h4">{I18n.t('Parameter')}</Heading>
|
||||
<Heading margin="medium none none" level="h4">
|
||||
{I18n.t('Parameter')}
|
||||
</Heading>
|
||||
<Text color="primary" weight="normal">
|
||||
{calculationInt}
|
||||
</Text>
|
||||
|
@ -176,8 +178,8 @@ const Example = ({currentMethod}) => {
|
|||
)
|
||||
}
|
||||
|
||||
const ProficiencyCalculation = ({method, update: rawUpdate, updateError}) => {
|
||||
const {calculationMethod: initialMethodKey, calculationInt: initialInt, locked} = method
|
||||
const ProficiencyCalculation = ({method, update: rawUpdate, updateError, canManage}) => {
|
||||
const {calculationMethod: initialMethodKey, calculationInt: initialInt} = method
|
||||
|
||||
const [calculationMethodKey, setCalculationMethodKey] = useState(initialMethodKey)
|
||||
const [calculationInt, setCalculationInt] = useState(initialInt)
|
||||
|
@ -216,18 +218,9 @@ const ProficiencyCalculation = ({method, update: rawUpdate, updateError}) => {
|
|||
|
||||
return (
|
||||
<View as="div">
|
||||
{locked && (
|
||||
<Heading level="h5" margin="medium 0">
|
||||
{I18n.t(
|
||||
'The proficiency calculation was set by your admin and applies to all courses in this account.'
|
||||
)}
|
||||
</Heading>
|
||||
)}
|
||||
<Flex alignItems="start" wrap="wrap">
|
||||
<Flex.Item size="240px" padding="small">
|
||||
{locked ? (
|
||||
<Display currentMethod={currentMethod} calculationInt={calculationInt} />
|
||||
) : (
|
||||
{canManage ? (
|
||||
<Form
|
||||
calculationMethodKey={calculationMethodKey}
|
||||
calculationInt={calculationInt}
|
||||
|
@ -236,6 +229,8 @@ const ProficiencyCalculation = ({method, update: rawUpdate, updateError}) => {
|
|||
updateCalculationMethod={updateCalculationMethod}
|
||||
setCalculationInt={setCalculationInt}
|
||||
/>
|
||||
) : (
|
||||
<Display currentMethod={currentMethod} calculationInt={calculationInt} />
|
||||
)}
|
||||
</Flex.Item>
|
||||
<Flex.Item size="240px" shouldGrow padding="small">
|
||||
|
@ -249,9 +244,9 @@ const ProficiencyCalculation = ({method, update: rawUpdate, updateError}) => {
|
|||
ProficiencyCalculation.propTypes = {
|
||||
method: PropTypes.shape({
|
||||
calculationMethod: PropTypes.string.isRequired,
|
||||
calculationInt: PropTypes.number,
|
||||
locked: PropTypes.bool
|
||||
calculationInt: PropTypes.number
|
||||
}),
|
||||
canManage: PropTypes.bool,
|
||||
update: PropTypes.func.isRequired,
|
||||
updateError: PropTypes.string
|
||||
}
|
||||
|
@ -259,8 +254,7 @@ ProficiencyCalculation.propTypes = {
|
|||
ProficiencyCalculation.defaultProps = {
|
||||
method: {
|
||||
calculationMethod: 'decaying_average',
|
||||
calculationInt: 65,
|
||||
locked: false
|
||||
calculationInt: 65
|
||||
},
|
||||
updateError: null
|
||||
}
|
||||
|
|
|
@ -43,20 +43,18 @@ describe('ProficiencyCalculation', () => {
|
|||
|
||||
const makeProps = (overrides = {}) => ({
|
||||
update: Function.prototype,
|
||||
canManage: true,
|
||||
...overrides,
|
||||
method: {
|
||||
calculationMethod: 'decaying_average',
|
||||
calculationInt: 75,
|
||||
locked: false,
|
||||
...(overrides.method || {})
|
||||
}
|
||||
})
|
||||
|
||||
describe('locked', () => {
|
||||
it('renders method and int', () => {
|
||||
const {getByText} = render(
|
||||
<ProficiencyCalculation {...makeProps({method: {locked: true}})} />
|
||||
)
|
||||
const {getByText} = render(<ProficiencyCalculation {...makeProps({canManage: false})} />)
|
||||
expect(getByText('Decaying Average')).not.toBeNull()
|
||||
expect(getByText('75')).not.toBeNull()
|
||||
})
|
||||
|
@ -65,7 +63,8 @@ describe('ProficiencyCalculation', () => {
|
|||
const {getByText, queryByText} = render(
|
||||
<ProficiencyCalculation
|
||||
{...makeProps({
|
||||
method: {calculationMethod: 'latest', calculationInt: null, locked: true}
|
||||
canManage: false,
|
||||
method: {calculationMethod: 'latest', calculationInt: null}
|
||||
})}
|
||||
/>
|
||||
)
|
||||
|
@ -77,7 +76,7 @@ describe('ProficiencyCalculation', () => {
|
|||
const {getByText} = render(
|
||||
<ProficiencyCalculation
|
||||
{...makeProps({
|
||||
method: {locked: true}
|
||||
canManage: false
|
||||
})}
|
||||
/>
|
||||
)
|
||||
|
@ -89,7 +88,8 @@ describe('ProficiencyCalculation', () => {
|
|||
const {getByText} = render(
|
||||
<ProficiencyCalculation
|
||||
{...makeProps({
|
||||
method: {calculationMethod: 'latest', calculationInt: null, locked: true}
|
||||
canManage: false,
|
||||
method: {calculationMethod: 'latest', calculationInt: null}
|
||||
})}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {render, wait, fireEvent} from '@testing-library/react'
|
||||
import {render, wait, fireEvent, waitForElementToBeRemoved} from '@testing-library/react'
|
||||
import {MockedProvider} from '@apollo/react-testing'
|
||||
import {OUTCOME_PROFICIENCY_QUERY, SET_OUTCOME_CALCULATION_METHOD} from '../api'
|
||||
import MasteryCalculation from '../index'
|
||||
|
@ -27,7 +27,10 @@ describe('MasteryCalculation', () => {
|
|||
window.ENV = {
|
||||
PROFICIENCY_CALCULATION_METHOD_ENABLED_ROLES: [
|
||||
{id: '1', role: 'AccountAdmin', label: 'Account Admin', base_role_type: 'AccountMembership'}
|
||||
]
|
||||
],
|
||||
PERMISSIONS: {
|
||||
manage_proficiency_calculations: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -53,8 +56,7 @@ describe('MasteryCalculation', () => {
|
|||
contextType: 'Account',
|
||||
contextId: 1,
|
||||
calculationMethod: 'decaying_average',
|
||||
calculationInt: 65,
|
||||
locked: false
|
||||
calculationInt: 65
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,35 +65,35 @@ describe('MasteryCalculation', () => {
|
|||
]
|
||||
|
||||
it('loads proficiency data', async () => {
|
||||
const {getByText, getByDisplayValue} = render(
|
||||
const {getByText, queryByText, getByDisplayValue} = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MasteryCalculation contextType="Account" contextId="11" />
|
||||
</MockedProvider>
|
||||
)
|
||||
expect(getByText('Loading')).not.toEqual(null)
|
||||
await wait()
|
||||
await waitForElementToBeRemoved(() => queryByText('Loading'))
|
||||
expect(getByDisplayValue(/65/)).not.toEqual(null)
|
||||
})
|
||||
|
||||
it('loads role list', async () => {
|
||||
const {getByText} = render(
|
||||
const {getByText, queryByText} = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MasteryCalculation contextType="Account" contextId="11" />
|
||||
</MockedProvider>
|
||||
)
|
||||
expect(getByText('Loading')).not.toEqual(null)
|
||||
await wait()
|
||||
await waitForElementToBeRemoved(() => queryByText('Loading'))
|
||||
expect(getByText(/Permission to change this mastery calculation/)).not.toEqual(null)
|
||||
expect(getByText(/Account Admin/)).not.toEqual(null)
|
||||
})
|
||||
|
||||
it('displays an error on failed request', async () => {
|
||||
const {getByText} = render(
|
||||
const {getByText, queryByText} = render(
|
||||
<MockedProvider mocks={[]}>
|
||||
<MasteryCalculation contextType="Account" contextId="11" />
|
||||
</MockedProvider>
|
||||
)
|
||||
await wait()
|
||||
await waitForElementToBeRemoved(() => queryByText('Loading'))
|
||||
expect(getByText(/An error occurred/)).not.toEqual(null)
|
||||
})
|
||||
|
||||
|
@ -114,12 +116,12 @@ describe('MasteryCalculation', () => {
|
|||
}
|
||||
}
|
||||
]
|
||||
const {getByText} = render(
|
||||
const {getByText, queryByText} = render(
|
||||
<MockedProvider mocks={emptyMocks}>
|
||||
<MasteryCalculation contextType="Account" contextId="11" />
|
||||
</MockedProvider>
|
||||
)
|
||||
await wait()
|
||||
await waitForElementToBeRemoved(() => queryByText('Loading'))
|
||||
expect(getByText('Proficiency Calculation')).not.toBeNull()
|
||||
})
|
||||
|
||||
|
@ -164,4 +166,28 @@ describe('MasteryCalculation', () => {
|
|||
await wait(() => expect(updateCall).toHaveBeenCalled())
|
||||
})
|
||||
})
|
||||
|
||||
describe('locked', () => {
|
||||
beforeEach(() => {
|
||||
window.ENV.PERMISSIONS = {
|
||||
manage_proficiency_calculations: false
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
window.ENV.PERMISSIONS = null
|
||||
})
|
||||
|
||||
it('hides role list', async () => {
|
||||
const {getByText, queryByText} = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MasteryCalculation contextType="Account" contextId="11" />
|
||||
</MockedProvider>
|
||||
)
|
||||
expect(getByText('Loading')).not.toEqual(null)
|
||||
await waitForElementToBeRemoved(() => queryByText('Loading'))
|
||||
expect(queryByText(/Permission to change this mastery calculation/)).not.toBeInTheDocument()
|
||||
expect(queryByText(/Account Admin/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -58,20 +58,24 @@ const MasteryCalculation = ({contextType, contextId}) => {
|
|||
}
|
||||
const {outcomeCalculationMethod} = data.account
|
||||
const roles = ENV.PROFICIENCY_CALCULATION_METHOD_ENABLED_ROLES || []
|
||||
const canManage = ENV.PERMISSIONS.manage_proficiency_calculations
|
||||
return (
|
||||
<>
|
||||
<RoleList
|
||||
description={I18n.t(
|
||||
'Permission to change this mastery calculation is enabled at the account level for:'
|
||||
)}
|
||||
roles={roles}
|
||||
/>
|
||||
{canManage && (
|
||||
<RoleList
|
||||
description={I18n.t(
|
||||
'Permission to change this mastery calculation is enabled at the account level for:'
|
||||
)}
|
||||
roles={roles}
|
||||
/>
|
||||
)}
|
||||
<ProficiencyCalculation
|
||||
contextType={contextType}
|
||||
contextId={contextId}
|
||||
method={outcomeCalculationMethod || undefined} // send undefined when value is null
|
||||
update={setCalculationMethod}
|
||||
updateError={setCalculationMethodError}
|
||||
canManage={canManage}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -24,7 +24,9 @@ import {IconTrashLine} from '@instructure/ui-icons'
|
|||
import {Popover} from '@instructure/ui-overlays'
|
||||
import {RadioInput} from '@instructure/ui-forms'
|
||||
import {TextInput} from '@instructure/ui-text-input'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y'
|
||||
import {ScreenReaderContent, PresentationContent} from '@instructure/ui-a11y'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {View} from '@instructure/ui-layout'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
import ColorPicker, {PREDEFINED_COLORS} from '../../shared/ColorPicker'
|
||||
|
||||
|
@ -51,13 +53,15 @@ class ProficiencyRating extends React.Component {
|
|||
points: PropTypes.string.isRequired,
|
||||
pointsError: PropTypes.string,
|
||||
isMobileView: PropTypes.bool,
|
||||
position: PropTypes.number.isRequired
|
||||
position: PropTypes.number.isRequired,
|
||||
canManage: PropTypes.bool
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
descriptionError: null,
|
||||
focusField: null,
|
||||
pointsError: null,
|
||||
canManage: window.ENV?.PERMISSIONS ? ENV.PERMISSIONS.manage_proficiency_scales : true,
|
||||
isMobileView: false
|
||||
}
|
||||
|
||||
|
@ -71,21 +75,24 @@ class ProficiencyRating extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.focusField === 'mastery') {
|
||||
const {canManage, focusField} = this.props
|
||||
if (focusField === 'mastery' && canManage) {
|
||||
this.radioInput.focus()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.focusField === 'trash') {
|
||||
setTimeout(
|
||||
() => (this.props.disableDelete ? this.colorButton.focus() : this.trashButton.focus()),
|
||||
700
|
||||
)
|
||||
} else if (this.props.focusField === 'description') {
|
||||
this.descriptionInput.focus()
|
||||
} else if (this.props.focusField === 'points') {
|
||||
this.pointsInput.focus()
|
||||
if (this.props.canManage) {
|
||||
if (this.props.focusField === 'trash') {
|
||||
setTimeout(
|
||||
() => (this.props.disableDelete ? this.colorButton.focus() : this.trashButton.focus()),
|
||||
700
|
||||
)
|
||||
} else if (this.props.focusField === 'description') {
|
||||
this.descriptionInput.focus()
|
||||
} else if (this.props.focusField === 'points') {
|
||||
this.pointsInput.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,105 +143,160 @@ class ProficiencyRating extends React.Component {
|
|||
}
|
||||
|
||||
renderDescription = () => {
|
||||
const {description, descriptionError, position} = this.props
|
||||
const {description, descriptionError, position, canManage} = this.props
|
||||
return (
|
||||
<div className="description">
|
||||
<TextInput
|
||||
width="100%"
|
||||
messages={this.errorMessage(descriptionError)}
|
||||
renderLabel={
|
||||
{canManage ? (
|
||||
<TextInput
|
||||
width="100%"
|
||||
messages={this.errorMessage(descriptionError)}
|
||||
renderLabel={
|
||||
<ScreenReaderContent>
|
||||
{I18n.t(`Change description for proficiency rating %{position}`, {position})}
|
||||
</ScreenReaderContent>
|
||||
}
|
||||
onChange={this.handleDescriptionChange}
|
||||
inputRef={element => this.setDescriptionRef(element)}
|
||||
defaultValue={description}
|
||||
/>
|
||||
) : (
|
||||
<Text>
|
||||
<ScreenReaderContent>
|
||||
{I18n.t(`Change description for proficiency rating %{position}`, {position})}
|
||||
{I18n.t(`Description for proficiency rating %{position}: %{description}`, {
|
||||
position,
|
||||
description
|
||||
})}
|
||||
</ScreenReaderContent>
|
||||
}
|
||||
onChange={this.handleDescriptionChange}
|
||||
inputRef={element => this.setDescriptionRef(element)}
|
||||
defaultValue={description}
|
||||
/>
|
||||
|
||||
<PresentationContent>{description}</PresentationContent>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderMastery = () => {
|
||||
const {mastery, position} = this.props
|
||||
const {mastery, position, canManage} = this.props
|
||||
return (
|
||||
<div className="mastery">
|
||||
<RadioInput
|
||||
label={
|
||||
<ScreenReaderContent>
|
||||
{I18n.t(`Mastery %{mastery} for proficiency rating %{position}`, {
|
||||
position,
|
||||
mastery
|
||||
})}
|
||||
</ScreenReaderContent>
|
||||
}
|
||||
ref={input => (this.radioInput = input)}
|
||||
checked={mastery}
|
||||
onChange={this.handleMasteryChange}
|
||||
/>
|
||||
<div className={`mastery ${canManage ? null : 'view-only'}`}>
|
||||
{(mastery || canManage) && (
|
||||
<RadioInput
|
||||
label={
|
||||
<ScreenReaderContent>
|
||||
{I18n.t(`Mastery %{mastery} for proficiency rating %{position}`, {
|
||||
position,
|
||||
mastery
|
||||
})}
|
||||
</ScreenReaderContent>
|
||||
}
|
||||
ref={input => (this.radioInput = input)}
|
||||
checked={mastery}
|
||||
readOnly={!canManage}
|
||||
onChange={this.handleMasteryChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderPointsInput = () => {
|
||||
const {points, pointsError, position, isMobileView} = this.props
|
||||
const {points, pointsError, position, isMobileView, canManage} = this.props
|
||||
return (
|
||||
<div className="points">
|
||||
<TextInput
|
||||
type="text"
|
||||
inputRef={this.setPointsRef}
|
||||
messages={this.errorMessage(pointsError)}
|
||||
renderLabel={
|
||||
{canManage ? (
|
||||
<>
|
||||
<TextInput
|
||||
type="text"
|
||||
inputRef={this.setPointsRef}
|
||||
messages={this.errorMessage(pointsError)}
|
||||
renderLabel={
|
||||
<ScreenReaderContent>
|
||||
{I18n.t(`Change points for proficiency rating %{position}`, {position})}
|
||||
</ScreenReaderContent>
|
||||
}
|
||||
onChange={this.handlePointChange}
|
||||
defaultValue={I18n.n(points)}
|
||||
width={isMobileView ? '7rem' : '4rem'}
|
||||
/>
|
||||
|
||||
<div className="pointsDescription" aria-hidden="true">
|
||||
{I18n.t('points')}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<View margin={`0 0 0 ${isMobileView ? '0' : 'small'}`}>
|
||||
<ScreenReaderContent>
|
||||
{I18n.t(`Change points for proficiency rating %{position}`, {position})}
|
||||
{I18n.t(`Points for proficiency rating %{position}: %{points}`, {
|
||||
position,
|
||||
points
|
||||
})}
|
||||
</ScreenReaderContent>
|
||||
}
|
||||
onChange={this.handlePointChange}
|
||||
defaultValue={I18n.n(points)}
|
||||
width={isMobileView ? '7rem' : '4rem'}
|
||||
/>
|
||||
<div className="pointsDescription" aria-hidden="true">
|
||||
{I18n.t('points')}
|
||||
</div>
|
||||
|
||||
<PresentationContent>
|
||||
{I18n.n(points)}
|
||||
|
||||
<div className="pointsDescription view-only">{I18n.t('points')}</div>
|
||||
</PresentationContent>
|
||||
</View>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderColorPicker = () => {
|
||||
const {color, position} = this.props
|
||||
const {color, position, canManage, isMobileView} = this.props
|
||||
return (
|
||||
<div className="color">
|
||||
<Popover on="click" show={this.state.showColorPopover} onToggle={this.handleMenuToggle}>
|
||||
<Popover.Trigger>
|
||||
<Button ref={this.setColorRef} variant="link">
|
||||
<div>
|
||||
<span className="colorPickerIcon" style={{background: formatColor(color)}} />
|
||||
<ScreenReaderContent>
|
||||
{I18n.t(`Change color for proficiency rating %{position}`, {position})}
|
||||
</ScreenReaderContent>
|
||||
<span aria-hidden="true">{I18n.t('Change')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content>
|
||||
<ColorPicker
|
||||
parentComponent="ProficiencyRating"
|
||||
colors={PREDEFINED_COLORS}
|
||||
currentColor={formatColor(color)}
|
||||
isOpen
|
||||
hidePrompt
|
||||
nonModal
|
||||
hideOnScroll={false}
|
||||
withAnimation={false}
|
||||
withBorder={false}
|
||||
withBoxShadow={false}
|
||||
withArrow={false}
|
||||
focusOnMount={false}
|
||||
afterClose={this.handleMenuClose}
|
||||
setStatusColor={this.setColor}
|
||||
/>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
{canManage ? (
|
||||
<Popover on="click" show={this.state.showColorPopover} onToggle={this.handleMenuToggle}>
|
||||
<Popover.Trigger>
|
||||
<Button ref={this.setColorRef} variant="link">
|
||||
<div>
|
||||
<span className="colorPickerIcon" style={{background: formatColor(color)}} />
|
||||
<ScreenReaderContent>
|
||||
{I18n.t(`Change color for proficiency rating %{position}`, {position})}
|
||||
</ScreenReaderContent>
|
||||
<span aria-hidden="true">{I18n.t('Change')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content>
|
||||
<ColorPicker
|
||||
parentComponent="ProficiencyRating"
|
||||
colors={PREDEFINED_COLORS}
|
||||
currentColor={formatColor(color)}
|
||||
isOpen
|
||||
hidePrompt
|
||||
nonModal
|
||||
hideOnScroll={false}
|
||||
withAnimation={false}
|
||||
withBorder={false}
|
||||
withBoxShadow={false}
|
||||
withArrow={false}
|
||||
focusOnMount={false}
|
||||
afterClose={this.handleMenuClose}
|
||||
setStatusColor={this.setColor}
|
||||
/>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
) : (
|
||||
<>
|
||||
<span
|
||||
className="colorPickerIcon"
|
||||
style={{
|
||||
background: formatColor(color),
|
||||
marginLeft: isMobileView ? 0 : '2rem'
|
||||
}}
|
||||
>
|
||||
<ScreenReaderContent>
|
||||
{I18n.t(`Color %{color} for proficiency rating %{position}`, {
|
||||
color: ColorPicker.getColorName(color) || formatColor(color),
|
||||
position
|
||||
})}
|
||||
</ScreenReaderContent>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -259,7 +321,7 @@ class ProficiencyRating extends React.Component {
|
|||
errorMessage = error => (error ? [{text: error, type: 'error'}] : null)
|
||||
|
||||
render() {
|
||||
const {isMobileView} = this.props
|
||||
const {isMobileView, canManage} = this.props
|
||||
return (
|
||||
<Flex
|
||||
padding={`${isMobileView ? '0 0 small 0' : '0 small small small'}`}
|
||||
|
@ -274,9 +336,9 @@ class ProficiencyRating extends React.Component {
|
|||
{isMobileView && (
|
||||
<>
|
||||
{this.renderPointsInput()}
|
||||
<div className="mobileRow">
|
||||
<div className={`mobileRow ${canManage ? null : 'view-only'}`}>
|
||||
{this.renderColorPicker()}
|
||||
{this.renderDeleteButton()}
|
||||
{canManage && this.renderDeleteButton()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
@ -287,9 +349,11 @@ class ProficiencyRating extends React.Component {
|
|||
{this.renderPointsInput()}
|
||||
</Flex.Item>
|
||||
<Flex.Item>{this.renderColorPicker()}</Flex.Item>
|
||||
<Flex.Item size="10%" padding="0 small 0 small">
|
||||
{this.renderDeleteButton()}
|
||||
</Flex.Item>
|
||||
{canManage && (
|
||||
<Flex.Item size="10%" padding="0 small 0 small">
|
||||
{this.renderDeleteButton()}
|
||||
</Flex.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
|
|
@ -57,13 +57,13 @@ const configToState = data => {
|
|||
return {
|
||||
masteryIndex,
|
||||
rows,
|
||||
locked: data.locked,
|
||||
allowSave: false
|
||||
}
|
||||
}
|
||||
class ProficiencyTable extends React.Component {
|
||||
static propTypes = {
|
||||
proficiency: PropTypes.object,
|
||||
canManage: PropTypes.bool.isRequired,
|
||||
update: PropTypes.func.isRequired,
|
||||
focusTab: PropTypes.func,
|
||||
breakpoints: breakpointsShape
|
||||
|
@ -81,6 +81,7 @@ class ProficiencyTable extends React.Component {
|
|||
]
|
||||
}
|
||||
},
|
||||
canManage: window.ENV?.PERMISSIONS ? ENV.PERMISSIONS.manage_proficiency_scales : true,
|
||||
focusTab: null,
|
||||
breakpoints: {}
|
||||
}
|
||||
|
@ -282,7 +283,7 @@ class ProficiencyTable extends React.Component {
|
|||
|
||||
render() {
|
||||
const {allowSave, masteryIndex} = this.state
|
||||
const {breakpoints} = this.props
|
||||
const {breakpoints, canManage} = this.props
|
||||
const isMobileView = breakpoints.mobileOnly
|
||||
return (
|
||||
<>
|
||||
|
@ -331,30 +332,36 @@ class ProficiencyTable extends React.Component {
|
|||
onPointsChange={this.handlePointsChange(index)}
|
||||
position={index + 1}
|
||||
isMobileView={isMobileView}
|
||||
canManage={canManage}
|
||||
/>
|
||||
{this.renderBorder()}
|
||||
</React.Fragment>
|
||||
))}
|
||||
<View
|
||||
width="100%"
|
||||
textAlign="start"
|
||||
padding="small small medium small"
|
||||
as="div"
|
||||
borderWidth="none none small none"
|
||||
>
|
||||
<Button onClick={this.addRow} renderIcon={<IconPlusLine />}>
|
||||
{I18n.t('Add Proficiency Level')}
|
||||
</Button>
|
||||
</View>
|
||||
<div className="save">
|
||||
<Button
|
||||
variant="primary"
|
||||
interaction={allowSave ? 'enabled' : 'disabled'}
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
{I18n.t('Save Mastery Scale')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{canManage && (
|
||||
<>
|
||||
<View
|
||||
width="100%"
|
||||
textAlign="start"
|
||||
padding="small small medium small"
|
||||
as="div"
|
||||
borderWidth="none none small none"
|
||||
>
|
||||
<Button onClick={this.addRow} renderIcon={<IconPlusLine />}>
|
||||
{I18n.t('Add Proficiency Level')}
|
||||
</Button>
|
||||
</View>
|
||||
<div className="save">
|
||||
<Button
|
||||
variant="primary"
|
||||
interaction={allowSave ? 'enabled' : 'disabled'}
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
{I18n.t('Save Mastery Scale')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -35,104 +35,195 @@ const defaultProps = (props = {}) => ({
|
|||
...props
|
||||
})
|
||||
|
||||
it('renders the ProficiencyRating component', () => {
|
||||
const wrapper = shallow(<ProficiencyRating {...defaultProps()} />)
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
describe('ProficiencyRating', () => {
|
||||
describe('can not manage', () => {
|
||||
it('renders the ProficiencyRating component', () => {
|
||||
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: false})} />)
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('mastery checkbox is checked if mastery', () => {
|
||||
const wrapper = shallow(
|
||||
<ProficiencyRating
|
||||
{...defaultProps({
|
||||
mastery: true
|
||||
})}
|
||||
/>
|
||||
)
|
||||
const radio = wrapper.find('RadioInput')
|
||||
expect(radio.props().checked).toBe(true)
|
||||
})
|
||||
it('mastery checkbox is checked if mastery', () => {
|
||||
const wrapper = shallow(
|
||||
<ProficiencyRating
|
||||
{...defaultProps({
|
||||
mastery: true,
|
||||
canManage: false
|
||||
})}
|
||||
/>
|
||||
)
|
||||
const radio = wrapper.find('RadioInput')
|
||||
expect(radio.props().checked).toBe(true)
|
||||
})
|
||||
|
||||
it('mastery checkbox receives focus', () => {
|
||||
const wrapper = mount(
|
||||
<div>
|
||||
<ProficiencyRating {...defaultProps({focusField: 'mastery'})} />
|
||||
</div>
|
||||
)
|
||||
expect(
|
||||
wrapper
|
||||
.find('RadioInput')
|
||||
.find('input')
|
||||
.instance()
|
||||
).toBe(document.activeElement)
|
||||
})
|
||||
it('mastery checkbox does not appear if not mastery', () => {
|
||||
const wrapper = shallow(
|
||||
<ProficiencyRating
|
||||
{...defaultProps({
|
||||
mastery: false,
|
||||
canManage: false
|
||||
})}
|
||||
/>
|
||||
)
|
||||
const radio = wrapper.find('RadioInput')
|
||||
expect(radio.exists()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('clicking mastery checkbox triggers change', () => {
|
||||
const onMasteryChange = jest.fn()
|
||||
const wrapper = mount(<ProficiencyRating {...defaultProps({onMasteryChange})} />)
|
||||
wrapper
|
||||
.find('RadioInput')
|
||||
.find('input')
|
||||
.simulate('change')
|
||||
expect(onMasteryChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
it('mastery checkbox does not receive focus', () => {
|
||||
const wrapper = mount(
|
||||
<div>
|
||||
<ProficiencyRating
|
||||
{...defaultProps({focusField: 'mastery', canManage: false, mastery: true})}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
expect(
|
||||
wrapper
|
||||
.find('RadioInput')
|
||||
.find('input')
|
||||
.instance()
|
||||
).not.toBe(document.activeElement)
|
||||
})
|
||||
|
||||
it('includes the rating description', () => {
|
||||
const wrapper = shallow(<ProficiencyRating {...defaultProps()} />)
|
||||
const input = wrapper.find('TextInput').at(0)
|
||||
expect(input.prop('defaultValue')).toBe('Stellar')
|
||||
})
|
||||
it('clicking mastery checkbox does not trigger change', () => {
|
||||
const onMasteryChange = jest.fn()
|
||||
const wrapper = mount(
|
||||
<ProficiencyRating {...defaultProps({onMasteryChange, mastery: true, canManage: false})} />
|
||||
)
|
||||
wrapper
|
||||
.find('RadioInput')
|
||||
.find('input')
|
||||
.simulate('change')
|
||||
expect(onMasteryChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('changing description triggers change', () => {
|
||||
const onDescriptionChange = jest.fn()
|
||||
const wrapper = mount(<ProficiencyRating {...defaultProps({onDescriptionChange})} />)
|
||||
wrapper
|
||||
.find('TextInput')
|
||||
.at(0)
|
||||
.find('input')
|
||||
.simulate('change')
|
||||
expect(onDescriptionChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
it('does not render TextInput', () => {
|
||||
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: false})} />)
|
||||
expect(wrapper.find('TextInput').exists()).toBeFalsy()
|
||||
})
|
||||
it('does not render delete button', () => {
|
||||
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: false})} />)
|
||||
expect(wrapper.find('.deleteButton').exists()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('includes the points', () => {
|
||||
const wrapper = shallow(<ProficiencyRating {...defaultProps()} />)
|
||||
const input = wrapper.find('TextInput').at(1)
|
||||
expect(input.prop('defaultValue')).toBe('10')
|
||||
})
|
||||
it('includes the points', () => {
|
||||
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: false})} />)
|
||||
const content = wrapper
|
||||
.find('.points')
|
||||
.find('PresentationContent')
|
||||
.at(0)
|
||||
expect(content.childAt(0).text()).toBe('10')
|
||||
})
|
||||
})
|
||||
|
||||
it('changing points triggers change', () => {
|
||||
const onPointsChange = jest.fn()
|
||||
const wrapper = mount(<ProficiencyRating {...defaultProps({onPointsChange})} />)
|
||||
wrapper
|
||||
.find('TextInput')
|
||||
.at(1)
|
||||
.find('input')
|
||||
.simulate('change')
|
||||
expect(onPointsChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
describe('can manage', () => {
|
||||
it('renders the ProficiencyRating component', () => {
|
||||
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: true})} />)
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('clicking delete button triggers delete', () => {
|
||||
const onDelete = jest.fn()
|
||||
const wrapper = mount(<ProficiencyRating {...defaultProps({onDelete})} />)
|
||||
wrapper
|
||||
.find('IconButton')
|
||||
.at(0)
|
||||
.simulate('click')
|
||||
expect(onDelete).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
it('mastery checkbox is checked if mastery', () => {
|
||||
const wrapper = shallow(
|
||||
<ProficiencyRating
|
||||
{...defaultProps({
|
||||
mastery: true,
|
||||
canManage: true
|
||||
})}
|
||||
/>
|
||||
)
|
||||
const radio = wrapper.find('RadioInput')
|
||||
expect(radio.props().checked).toBe(true)
|
||||
})
|
||||
|
||||
it('clicking disabled delete button does not triggers delete', () => {
|
||||
const onDelete = jest.fn()
|
||||
const wrapper = mount(
|
||||
<ProficiencyRating
|
||||
{...defaultProps({
|
||||
onDelete,
|
||||
disableDelete: true
|
||||
})}
|
||||
/>
|
||||
)
|
||||
wrapper
|
||||
.find('IconButton')
|
||||
.at(0)
|
||||
.simulate('click')
|
||||
expect(onDelete).toHaveBeenCalledTimes(0)
|
||||
it('mastery checkbox receives focus', () => {
|
||||
const wrapper = mount(
|
||||
<div>
|
||||
<ProficiencyRating {...defaultProps({focusField: 'mastery', canManage: true})} />
|
||||
</div>
|
||||
)
|
||||
expect(
|
||||
wrapper
|
||||
.find('RadioInput')
|
||||
.find('input')
|
||||
.instance()
|
||||
).toBe(document.activeElement)
|
||||
})
|
||||
|
||||
it('clicking mastery checkbox triggers change', () => {
|
||||
const onMasteryChange = jest.fn()
|
||||
const wrapper = mount(
|
||||
<ProficiencyRating {...defaultProps({onMasteryChange, canManage: true})} />
|
||||
)
|
||||
wrapper
|
||||
.find('RadioInput')
|
||||
.find('input')
|
||||
.simulate('change')
|
||||
expect(onMasteryChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('includes the rating description', () => {
|
||||
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: true})} />)
|
||||
const input = wrapper.find('TextInput').at(0)
|
||||
expect(input.prop('defaultValue')).toBe('Stellar')
|
||||
})
|
||||
|
||||
it('changing description triggers change', () => {
|
||||
const onDescriptionChange = jest.fn()
|
||||
const wrapper = mount(
|
||||
<ProficiencyRating {...defaultProps({onDescriptionChange, canManage: true})} />
|
||||
)
|
||||
wrapper
|
||||
.find('TextInput')
|
||||
.at(0)
|
||||
.find('input')
|
||||
.simulate('change')
|
||||
expect(onDescriptionChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('includes the points', () => {
|
||||
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: true})} />)
|
||||
const input = wrapper.find('TextInput').at(1)
|
||||
expect(input.prop('defaultValue')).toBe('10')
|
||||
})
|
||||
|
||||
it('changing points triggers change', () => {
|
||||
const onPointsChange = jest.fn()
|
||||
const wrapper = mount(
|
||||
<ProficiencyRating {...defaultProps({onPointsChange, canManage: true})} />
|
||||
)
|
||||
wrapper
|
||||
.find('TextInput')
|
||||
.at(1)
|
||||
.find('input')
|
||||
.simulate('change')
|
||||
expect(onPointsChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('clicking delete button triggers delete', () => {
|
||||
const onDelete = jest.fn()
|
||||
const wrapper = mount(<ProficiencyRating {...defaultProps({onDelete, canManage: true})} />)
|
||||
wrapper
|
||||
.find('IconButton')
|
||||
.at(0)
|
||||
.simulate('click')
|
||||
expect(onDelete).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('clicking disabled delete button does not triggers delete', () => {
|
||||
const onDelete = jest.fn()
|
||||
const wrapper = mount(
|
||||
<ProficiencyRating
|
||||
{...defaultProps({
|
||||
onDelete,
|
||||
disableDelete: true,
|
||||
canManage: true
|
||||
})}
|
||||
/>
|
||||
)
|
||||
wrapper
|
||||
.find('IconButton')
|
||||
.at(0)
|
||||
.simulate('click')
|
||||
expect(onDelete).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -252,4 +252,39 @@ describe('custom proficiency', () => {
|
|||
expect(deleteButtons.length).toEqual(1)
|
||||
expect(deleteButtons[0].disabled).toEqual(true)
|
||||
})
|
||||
|
||||
describe('can not manage', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
canManage: false,
|
||||
proficiency: {
|
||||
proficiencyRatingsConnection: {
|
||||
nodes: [
|
||||
{
|
||||
description: 'Great',
|
||||
points: 10,
|
||||
color: '0000ff',
|
||||
mastery: true
|
||||
},
|
||||
{
|
||||
description: 'Poor',
|
||||
points: 0,
|
||||
color: 'ff0000',
|
||||
mastery: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('does not render Save button', () => {
|
||||
const {queryByText} = render(<ProficiencyTable {...props} />)
|
||||
expect(queryByText('Save Mastery Scale')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render Add button', () => {
|
||||
const {queryByText} = render(<ProficiencyTable {...props} />)
|
||||
expect(queryByText('Add Proficiency Level')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders the ProficiencyRating component 1`] = `
|
||||
exports[`ProficiencyRating can manage renders the ProficiencyRating component 1`] = `
|
||||
<Flex
|
||||
alignItems="start"
|
||||
as="span"
|
||||
|
@ -23,7 +23,7 @@ exports[`renders the ProficiencyRating component 1`] = `
|
|||
textAlign="center"
|
||||
>
|
||||
<div
|
||||
className="mastery"
|
||||
className="mastery null"
|
||||
>
|
||||
<RadioInput
|
||||
checked={false}
|
||||
|
@ -324,3 +324,132 @@ exports[`renders the ProficiencyRating component 1`] = `
|
|||
</Item>
|
||||
</Flex>
|
||||
`;
|
||||
|
||||
exports[`ProficiencyRating can not manage renders the ProficiencyRating component 1`] = `
|
||||
<Flex
|
||||
alignItems="start"
|
||||
as="span"
|
||||
direction="row"
|
||||
display="flex"
|
||||
elementRef={[Function]}
|
||||
justifyItems="start"
|
||||
padding="0 small small small"
|
||||
width="100%"
|
||||
withVisualDebug={false}
|
||||
wrap="no-wrap"
|
||||
>
|
||||
<Item
|
||||
as="span"
|
||||
elementRef={[Function]}
|
||||
padding="0 medium 0 0"
|
||||
shouldGrow={false}
|
||||
shouldShrink={false}
|
||||
size="15%"
|
||||
textAlign="center"
|
||||
>
|
||||
<div
|
||||
className="mastery view-only"
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
align="start"
|
||||
as="span"
|
||||
elementRef={[Function]}
|
||||
padding="0 small 0 0"
|
||||
shouldGrow={false}
|
||||
shouldShrink={false}
|
||||
size="40%"
|
||||
>
|
||||
<div
|
||||
className="description"
|
||||
>
|
||||
<Text
|
||||
as="span"
|
||||
letterSpacing="normal"
|
||||
size="medium"
|
||||
wrap="normal"
|
||||
>
|
||||
<ScreenReaderContent
|
||||
as="span"
|
||||
>
|
||||
Description for proficiency rating 1: Stellar
|
||||
</ScreenReaderContent>
|
||||
<PresentationContent
|
||||
as="span"
|
||||
>
|
||||
Stellar
|
||||
</PresentationContent>
|
||||
</Text>
|
||||
</div>
|
||||
</Item>
|
||||
<Item
|
||||
align="start"
|
||||
as="span"
|
||||
elementRef={[Function]}
|
||||
padding="0 small 0 0"
|
||||
shouldGrow={false}
|
||||
shouldShrink={false}
|
||||
size="15%"
|
||||
>
|
||||
<div
|
||||
className="points"
|
||||
>
|
||||
<View
|
||||
borderColor="default"
|
||||
debug={false}
|
||||
display="auto"
|
||||
focusColor="info"
|
||||
focusPosition="offset"
|
||||
focused={false}
|
||||
margin="0 0 0 small"
|
||||
overflowX="visible"
|
||||
overflowY="visible"
|
||||
position="static"
|
||||
shouldAnimateFocus={true}
|
||||
>
|
||||
<ScreenReaderContent
|
||||
as="span"
|
||||
>
|
||||
Points for proficiency rating 1: 10.0
|
||||
</ScreenReaderContent>
|
||||
<PresentationContent
|
||||
as="span"
|
||||
>
|
||||
10
|
||||
<div
|
||||
className="pointsDescription view-only"
|
||||
>
|
||||
points
|
||||
</div>
|
||||
</PresentationContent>
|
||||
</View>
|
||||
</div>
|
||||
</Item>
|
||||
<Item
|
||||
as="span"
|
||||
elementRef={[Function]}
|
||||
shouldGrow={false}
|
||||
shouldShrink={false}
|
||||
>
|
||||
<div
|
||||
className="color"
|
||||
>
|
||||
<span
|
||||
className="colorPickerIcon"
|
||||
style={
|
||||
Object {
|
||||
"background": "#00ff00",
|
||||
"marginLeft": "2rem",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ScreenReaderContent
|
||||
as="span"
|
||||
>
|
||||
Color #00ff00 for proficiency rating 1
|
||||
</ScreenReaderContent>
|
||||
</span>
|
||||
</div>
|
||||
</Item>
|
||||
</Flex>
|
||||
`;
|
||||
|
|
|
@ -28,7 +28,10 @@ describe('MasteryScale', () => {
|
|||
window.ENV = {
|
||||
PROFICIENCY_SCALES_ENABLED_ROLES: [
|
||||
{id: '1', role: 'AccountAdmin', label: 'Account Admin', base_role_type: 'AccountMembership'}
|
||||
]
|
||||
],
|
||||
PERMISSIONS: {
|
||||
manage_proficiency_scales: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -168,4 +171,28 @@ describe('MasteryScale', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('can not manage', () => {
|
||||
beforeEach(() => {
|
||||
window.ENV.PERMISSIONS = {
|
||||
manage_proficiency_scales: false
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
window.ENV.PERMISSIONS = null
|
||||
})
|
||||
|
||||
it('hides role list', async () => {
|
||||
const {getByText, queryByText} = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MasteryScale contextType="Account" contextId="11" />
|
||||
</MockedProvider>
|
||||
)
|
||||
expect(getByText('Loading')).not.toEqual(null)
|
||||
await wait()
|
||||
expect(queryByText(/Permission to change this mastery calculation/)).not.toBeInTheDocument()
|
||||
expect(queryByText(/Account Admin/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -72,21 +72,28 @@ const MasteryScale = ({contextType, contextId}) => {
|
|||
}
|
||||
const {outcomeProficiency} = data.account
|
||||
const roles = ENV.PROFICIENCY_SCALES_ENABLED_ROLES || []
|
||||
const canManage = ENV.PERMISSIONS.manage_proficiency_scales
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<Text>
|
||||
{I18n.t(
|
||||
'This mastery scale will be used as the default for all courses within your account.'
|
||||
)}
|
||||
</Text>
|
||||
</p>
|
||||
<RoleList
|
||||
description={I18n.t(
|
||||
'Permission to change this mastery scale is enabled at the account level for:'
|
||||
)}
|
||||
roles={roles}
|
||||
/>
|
||||
{canManage && (
|
||||
<>
|
||||
<p>
|
||||
<Text>
|
||||
{I18n.t(
|
||||
'This mastery scale will be used as the default for all courses within your account.'
|
||||
)}
|
||||
</Text>
|
||||
</p>
|
||||
|
||||
<RoleList
|
||||
description={I18n.t(
|
||||
'Permission to change this mastery scale is enabled at the account level for:'
|
||||
)}
|
||||
roles={roles}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ProficiencyTable
|
||||
proficiency={outcomeProficiency || undefined} // send undefined when value is null
|
||||
update={updateProficiencyRatings}
|
||||
|
|
|
@ -574,4 +574,17 @@ const ColorPicker = createReactClass({
|
|||
return this.props.nonModal ? body : this.modalWrapping(body)
|
||||
}
|
||||
})
|
||||
|
||||
ColorPicker.getColorName = colorHex => {
|
||||
const colorWithoutHash = colorHex.replace('#', '')
|
||||
|
||||
const definedColor = PREDEFINED_COLORS.find(
|
||||
color => color.hexcode.replace('#', '') === colorWithoutHash
|
||||
)
|
||||
|
||||
if (definedColor) {
|
||||
return definedColor.name
|
||||
}
|
||||
}
|
||||
|
||||
export default ColorPicker
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (C) 2016 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ColorPicker from '../ColorPicker'
|
||||
|
||||
describe('ColorPicker', () => {
|
||||
describe('getColorName', () => {
|
||||
it('returns name', () => {
|
||||
expect(ColorPicker.getColorName('#BD3C14')).toBe('Brick')
|
||||
})
|
||||
|
||||
it('returns name without hash', () => {
|
||||
expect(ColorPicker.getColorName('BD3C14')).toBe('Brick')
|
||||
})
|
||||
|
||||
it('returns undefined if color does not exists in PREDEFINED_COLORS', () => {
|
||||
expect(ColorPicker.getColorName('#111111')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -15,6 +15,12 @@
|
|||
padding-#{direction(left)}: 16px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
&.view-only {
|
||||
@include desktop-only {
|
||||
padding-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete {
|
||||
|
@ -65,6 +71,12 @@
|
|||
padding-#{direction(left)}: 8px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
&.view-only {
|
||||
@include mobile-only {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
|
@ -85,4 +97,10 @@
|
|||
justify-content: space-between;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.view-only {
|
||||
@include mobile-only {
|
||||
margin-#{direction(left)}: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,8 +78,11 @@ describe OutcomesController do
|
|||
user_session(@admin)
|
||||
get 'index', params: {:account_id => @account.id}
|
||||
permissions = assigns[:js_env][:PERMISSIONS]
|
||||
[:manage_outcomes, :manage_rubrics, :manage_courses, :import_outcomes].each do |permission|
|
||||
expect(permissions.key?(permission)).to be_truthy
|
||||
[
|
||||
:manage_outcomes, :manage_rubrics, :manage_courses, :import_outcomes, :manage_proficiency_scales,
|
||||
:manage_proficiency_calculations
|
||||
].each do |permission|
|
||||
expect(permissions).to have_key(permission)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue