Skip to content

Commit 8a2a12f

Browse files
eschiebelFelix Milea-Ciobanu
authored andcommitted
Add unsynched changes link and page
add unsynched changes link to the master course sidebar, and the resulting page. fixes MC-156 test plan: - create a master/minion course pair - from master course, create some stuff - sync - create something new, change something, delete something - open the disebar and click on "Unsynched Changes" > expected result: your unsynched changes are listed - click the sync buton on the page > expected result: the modal closes and the sync button on the sidebar > is spinning - after sync completes, reopen unsynced changes > expected result: a message that there are no changes Change-Id: I597694c8a831a6b365db812300be337d49c66d61 Reviewed-on: https://gerrit.instructure.com/110211 Reviewed-by: Felix Milea-Ciobanu <fmileaciobanu@instructure.com> QA-Review: Heath Hales <hhales@instructure.com> Tested-by: Jenkins Product-Review: Kendall Chadwick <kchadwick@instructure.com>
1 parent 399e826 commit 8a2a12f

19 files changed

Lines changed: 589 additions & 58 deletions

File tree

app/jsx/blueprint_course_settings/actions.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const types = [
3838
'ADD_COURSE_ASSOCIATIONS', 'UNDO_ADD_COURSE_ASSOCIATIONS',
3939
'REMOVE_COURSE_ASSOCIATIONS', 'UNDO_REMOVE_COURSE_ASSOCIATIONS',
4040
'CLEAR_ASSOCIATIONS',
41+
'LOAD_UNSYNCHED_CHANGES_START', 'LOAD_UNSYNCHED_CHANGES_SUCCESS', 'LOAD_UNSYNCHED_CHANGES_FAIL',
42+
'ENABLE_SEND_NOTIFICATION'
4143
]
4244
const actions = createActions(...types)
4345

@@ -143,6 +145,13 @@ actions.saveAssociations = () => (dispatch, getState) => {
143145
.catch(handleError(I18n.t('An error occurred while saving associations'), dispatch, actions.saveAssociationsFail))
144146
}
145147

148+
actions.loadUnsynchedChanges = () => (dispatch, getState) => {
149+
dispatch(actions.loadUnsynchedChangesStart())
150+
api.loadUnsynchedChanges(getState())
151+
.then(res => dispatch(actions.loadUnsynchedChangesSuccess(res.data)))
152+
.catch(err => dispatch(actions.loadUnsynchedChangesFail(err)))
153+
}
154+
146155
const actionTypes = types.reduce((typesMap, actionType) =>
147156
Object.assign(typesMap, { [actionType]: actionType }), {})
148157

app/jsx/blueprint_course_settings/apiClient.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ const ApiClient = {
110110
.then(res => Object.assign(mig, { changes: res.data }))
111111
)))
112112
},
113+
loadUnsynchedChanges ({ course }) {
114+
return axios.get(`/api/v1/courses/${course.id}/blueprint_templates/default/unsynced_changes`)
115+
},
113116
}
114117

115118
export default ApiClient

app/jsx/blueprint_course_settings/components/BlueprintModal.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export default class BlueprintModal extends Component {
3131
children: PropTypes.func.isRequired,
3232
hasChanges: PropTypes.bool,
3333
isSaving: PropTypes.bool,
34+
doneButton: PropTypes.element,
3435
}
3536

3637
static defaultProps = {
@@ -39,6 +40,7 @@ export default class BlueprintModal extends Component {
3940
isSaving: false,
4041
onSave: () => {},
4142
onCancel: () => {},
43+
doneButton: null,
4244
}
4345

4446
componentDidUpdate (prevProps) {
@@ -73,12 +75,13 @@ export default class BlueprintModal extends Component {
7375
</div>
7476
</ModalBody>
7577
<ModalFooter ref={(c) => { this.footer = c }}>
76-
{this.props.hasChanges && !this.props.isSaving ? (
77-
<span>
78-
<Button onClick={this.props.onCancel}>{I18n.t('Cancel')}</Button>&nbsp;
79-
<Button onClick={this.props.onSave} variant="primary">{I18n.t('Save')}</Button>
80-
</span>
81-
) : (
78+
{this.props.hasChanges && !this.props.isSaving ? [
79+
<Button onClick={this.props.onCancel}>{I18n.t('Cancel')}</Button>,
80+
<span>&nbsp;</span>,
81+
this.props.doneButton
82+
? this.props.doneButton
83+
: <Button onClick={this.props.onSave} variant="primary">{I18n.t('Save')}</Button>
84+
] : (
8285
<Button ref={(c) => { this.doneBtn = c }} onClick={this.props.onCancel} variant="primary">{I18n.t('Done')}</Button>
8386
)}
8487
</ModalFooter>

app/jsx/blueprint_course_settings/components/CourseSidebar.js

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,21 @@ import select from 'jsx/shared/select'
2424

2525
import Button from 'instructure-ui/lib/components/Button'
2626
import Typography from 'instructure-ui/lib/components/Typography'
27+
import Checkbox from 'instructure-ui/lib/components/Checkbox'
28+
import Spinner from 'instructure-ui/lib/components/Spinner'
2729

2830
import propTypes from '../propTypes'
2931
import actions from '../actions'
32+
import MigrationStates from '../migrationStates'
33+
3034
import BlueprintSidebar from './BlueprintSidebar'
3135
import BlueprintModal from './BlueprintModal'
3236
import { ConnectedMigrationSync as MigrationSync } from './MigrationSync'
3337
import { ConnectedBlueprintAssociations as BlueprintAssociations } from './BlueprintAssociations'
3438
import { ConnectedSyncHistory as SyncHistory } from './SyncHistory'
3539

40+
let UnsynchedChanges = null
41+
3642
export default class CourseSidebar extends Component {
3743
static propTypes = {
3844
hasLoadedAssociations: PropTypes.bool.isRequired,
@@ -42,6 +48,17 @@ export default class CourseSidebar extends Component {
4248
clearAssociations: PropTypes.func.isRequired,
4349
hasAssociationChanges: PropTypes.bool.isRequired,
4450
isSavingAssociations: PropTypes.bool.isRequired,
51+
willSendNotification: PropTypes.bool.isRequired,
52+
enableSendNotification: PropTypes.func.isRequired,
53+
loadUnsynchedChanges: PropTypes.func.isRequired,
54+
isLoadingUnsynchedChanges: PropTypes.bool.isRequired,
55+
hasLoadedUnsynchedChanges: PropTypes.bool.isRequired,
56+
unsynchedChanges: propTypes.unsynchedChanges,
57+
migrationStatus: PropTypes.oneOf(MigrationStates.states)
58+
}
59+
static defaultProps = {
60+
unsynchedChanges: [],
61+
migrationStatus: MigrationStates.unknown,
4562
}
4663

4764
constructor (props) {
@@ -52,10 +69,23 @@ export default class CourseSidebar extends Component {
5269
}
5370
}
5471

72+
componentWillReceiveProps (nextProps) {
73+
// if migration is going from a loading state to a non-loading state
74+
// aka a migration probably just ended and we should refresh the list
75+
// of unsynched changes
76+
if (MigrationStates.isLoadingState(this.props.migrationStatus) &&
77+
!MigrationStates.isLoadingState(nextProps.migrationStatus)) {
78+
this.props.loadUnsynchedChanges()
79+
}
80+
}
81+
5582
onOpenSidebar = () => {
5683
if (!this.props.hasLoadedAssociations) {
5784
this.props.loadAssociations()
5885
}
86+
if (!this.props.hasLoadedUnsynchedChanges) {
87+
this.props.loadUnsynchedChanges()
88+
}
5989
}
6090

6191
modals = {
@@ -85,6 +115,23 @@ export default class CourseSidebar extends Component {
85115
},
86116
children: () => <SyncHistory />,
87117
}),
118+
unsynchedChanges: () => ({
119+
props: {
120+
hasChanges: this.props.unsynchedChanges.length > 0,
121+
willSendNotification: this.props.willSendNotification,
122+
enableSendNotification: this.props.enableSendNotification,
123+
onCancel: () => this.closeModal(() => {
124+
this.unsynchedChangesBtn.focus()
125+
}),
126+
doneButton: <MigrationSync
127+
showProgress={false}
128+
onClick={() => this.closeModal(() => {
129+
this.unsynchedChangesBtn.focus()
130+
})}
131+
/>
132+
},
133+
children: () => <UnsynchedChanges />,
134+
})
88135
}
89136

90137
closeModal = (cb) => {
@@ -105,6 +152,73 @@ export default class CourseSidebar extends Component {
105152
})
106153
}
107154

155+
handleUnsynchedChangesClick = () => {
156+
require.ensure([], (require) => {
157+
// lazy load UnsynchedChanges component
158+
const UnsynchedChangesModule = require('./UnsynchedChanges')
159+
if (UnsynchedChanges === null) {
160+
UnsynchedChanges = UnsynchedChangesModule.ConnectedUnsynchedChanges
161+
}
162+
163+
this.setState({
164+
isModalOpen: true,
165+
modalId: 'unsynchedChanges',
166+
})
167+
})
168+
}
169+
170+
handleSendNotificationClick = (event) => {
171+
const enabled = event.target.checked
172+
this.props.enableSendNotification(enabled)
173+
}
174+
175+
// if we have unsynched changes, show the sync button
176+
maybeRenderSyncButton () {
177+
if (this.props.hasLoadedUnsynchedChanges && this.props.unsynchedChanges.length > 0) {
178+
return <MigrationSync />
179+
}
180+
return null
181+
}
182+
183+
// if we have unsynched changes, show the button
184+
maybeRenderUnsynchedChanges () {
185+
if (!this.props.hasLoadedUnsynchedChanges || this.props.isLoadingUnsynchedChanges) {
186+
return this.renderSpinner(I18n.t('Loading Unsynched Changes'))
187+
}
188+
if (this.props.hasLoadedUnsynchedChanges && this.props.unsynchedChanges.length > 0) {
189+
return (
190+
<div className="bcs__row">
191+
<Button
192+
id="mcUnsynchedChangesBtn"
193+
ref={(c) => { this.unsynchedChangesBtn = c }}
194+
variant="link"
195+
onClick={this.handleUnsynchedChangesClick}
196+
>
197+
<Typography>{I18n.t('Unsynched Changes')}</Typography>
198+
</Button>
199+
<Typography><span className="bcs__row-right-content">{this.props.unsynchedChanges.length}</span></Typography>
200+
<div className="bcs__history-notification">
201+
<Checkbox
202+
label={I18n.t('Send Notification')}
203+
checked={this.props.willSendNotification}
204+
onChange={this.handleSendNotificationClick}
205+
/>
206+
</div>
207+
</div>
208+
)
209+
}
210+
return null
211+
}
212+
213+
renderSpinner (title) {
214+
return (
215+
<div style={{textAlign: 'center'}}>
216+
<Spinner size="small" title={title} />
217+
<Typography size="small" as="p">{title}</Typography>
218+
</div>
219+
)
220+
}
221+
108222
renderModal () {
109223
if (this.modals[this.state.modalId]) {
110224
const modal = this.modals[this.state.modalId]()
@@ -118,17 +232,18 @@ export default class CourseSidebar extends Component {
118232
return (
119233
<BlueprintSidebar onOpen={this.onOpenSidebar}>
120234
<div className="bcs__row">
121-
<Button ref={(c) => { this.asscBtn = c }} variant="link" onClick={this.handleAssociationsClick}>
235+
<Button id="mcSidebarAsscBtn" ref={(c) => { this.asscBtn = c }} variant="link" onClick={this.handleAssociationsClick}>
122236
<Typography>{I18n.t('Associations')}</Typography>
123237
</Button>
124238
<Typography><span className="bcs__row-right-content">{this.props.associations.length}</span></Typography>
125239
</div>
126240
<div className="bcs__row">
127-
<Button ref={(c) => { this.syncHistoryBtn = c }} variant="link" onClick={this.handleSyncHistoryClick}>
241+
<Button is="mcSyncHistoryBtn" ref={(c) => { this.syncHistoryBtn = c }} variant="link" onClick={this.handleSyncHistoryClick}>
128242
<Typography>{I18n.t('Sync History')}</Typography>
129243
</Button>
130244
</div>
131-
<MigrationSync />
245+
{this.maybeRenderUnsynchedChanges()}
246+
{this.maybeRenderSyncButton()}
132247
{this.renderModal()}
133248
</BlueprintSidebar>
134249
)
@@ -138,11 +253,15 @@ export default class CourseSidebar extends Component {
138253
const connectState = state =>
139254
Object.assign(select(state, [
140255
'hasLoadedAssociations',
141-
'migrationStatus',
142256
'isLoadingBeginMigration',
143257
'hasCheckedMigration',
144258
'isSavingAssociations',
145259
['existingAssociations', 'associations'],
260+
'willSendNotification',
261+
'unsynchedChanges',
262+
'isLoadingUnsynchedChanges',
263+
'hasLoadedUnsynchedChanges',
264+
'migrationStatus'
146265
]), {
147266
hasAssociationChanges: (state.addedAssociations.length + state.removedAssociations.length) > 0,
148267
})

app/jsx/blueprint_course_settings/components/MigrationSync.js

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,16 @@ export default class MigrationSync extends Component {
4141
checkMigration: PropTypes.func.isRequired,
4242
beginMigration: PropTypes.func.isRequired,
4343
intervalDuration: PropTypes.number,
44+
showProgress: PropTypes.bool,
45+
willSendNotification: PropTypes.bool,
46+
onClick: PropTypes.func
4447
}
4548

4649
static defaultProps = {
4750
intervalDuration: 3000,
51+
showProgress: true,
52+
willSendNotification: false,
53+
onClick: null
4854
}
4955

5056
constructor (props) {
@@ -81,6 +87,10 @@ export default class MigrationSync extends Component {
8187
}
8288
}
8389

90+
componentWillUnmount () {
91+
this.clearMigrationInterval()
92+
}
93+
8494
clearMigrationInterval () {
8595
if (this.intId !== null) {
8696
clearInterval(this.intId)
@@ -90,6 +100,9 @@ export default class MigrationSync extends Component {
90100

91101
handleSyncClick = () => {
92102
this.props.beginMigration()
103+
if (this.props.onClick) {
104+
this.props.onClick()
105+
}
93106
}
94107

95108
render () {
@@ -101,8 +114,8 @@ export default class MigrationSync extends Component {
101114
})
102115
return (
103116
<div className="bcs__migration-sync">
104-
{ isSyncing && (
105-
<span className="bcs__migration-sync__loading">
117+
{ this.props.showProgress && isSyncing && (
118+
<div className="bcs__migration-sync__loading">
106119
<Typography as="p">{I18n.t('Processing')}</Typography>
107120
<Typography as="p" size="small">{I18n.t('This may take a bit...')}</Typography>
108121
<Progress
@@ -111,12 +124,15 @@ export default class MigrationSync extends Component {
111124
valueNow={MigrationStates.getLoadingValue(migrationStatus)}
112125
valueMax={MigrationStates.maxLoadingValue}
113126
/>
114-
<Typography as="p" size="small">
115-
{I18n.t('You can leave the page and you will get a notification when the sync process is complete.')}
116-
</Typography>
117-
</span>
127+
{
128+
this.props.willSendNotification &&
129+
<Typography as="p" size="small">
130+
{I18n.t('You can leave the page and you will get a notification when the sync process is complete.')}
131+
</Typography>
132+
}
133+
</div>
118134
)}
119-
<span style={{float: 'right'}}>
135+
<div className="bcs__migration-sync__button">
120136
<Button
121137
variant="primary"
122138
onClick={this.handleSyncClick}
@@ -132,7 +148,7 @@ export default class MigrationSync extends Component {
132148
{isSyncing ? I18n.t('Syncing...') : I18n.t('Sync')}
133149
</span>
134150
</Button>
135-
</span>
151+
</div>
136152
</div>
137153
)
138154
}
@@ -143,6 +159,7 @@ const connectState = state =>
143159
'migrationStatus',
144160
'isLoadingBeginMigration',
145161
'hasCheckedMigration',
162+
'willSendNotification',
146163
])
147164
const connectActions = dispatch => bindActionCreators(actions, dispatch)
148165
export const ConnectedMigrationSync = connect(connectState, connectActions)(MigrationSync)

0 commit comments

Comments
 (0)