diff --git a/.gitignore b/.gitignore index c83ae1043..bcc76c008 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ migrate .idea/ package-lock.json custom_modules +*.iml diff --git a/actions/activityfeed/loadActivities.js b/actions/activityfeed/loadActivities.js index c58bf0dc1..b02856d96 100644 --- a/actions/activityfeed/loadActivities.js +++ b/actions/activityfeed/loadActivities.js @@ -17,6 +17,8 @@ export default function loadActivities(context, payload, done) { return; } + context.dispatch('LOAD_ACTIVITIES_LOAD', {loadingIndicator: true}); + context.service.read('activities.list', payload, {timeout: 20 * 1000}, (err, res) => { if (err) { log.error(context, {filepath: __filename}); diff --git a/actions/loadContributors.js b/actions/loadContributors.js index 0a3050ac6..26293b20a 100644 --- a/actions/loadContributors.js +++ b/actions/loadContributors.js @@ -18,7 +18,11 @@ export default function loadContributors(context, payload, done) { return; } - if (!payload.params.language) payload.params.language = context.getStore(TranslationStore).currentLang; + context.dispatch('LOAD_CONTRIBUTORS_LOAD', {loadingIndicator: true}); + + if (!payload.params.language) { + payload.params.language = context.getStore(TranslationStore).currentLang; + } context.service.read('contributors.list', payload, {timeout: 20 * 1000}, (err, res) => { if (err) { @@ -30,9 +34,6 @@ export default function loadContributors(context, payload, done) { // context.dispatch('UPDATE_MODULE_TYPE_SUCCESS', {moduleType: 'contributors'}); } let pageTitle = shortTitle + ' | Contributors | ' + payload.params.stype + ' | ' + payload.params.sid; - //context.dispatch('UPDATE_PAGE_TITLE', { - // pageTitle: pageTitle - //}); done(); }); } diff --git a/common.js b/common.js index c424b8234..a84bfe79d 100644 --- a/common.js +++ b/common.js @@ -175,6 +175,42 @@ export default { return a.substring(0,2).toLowerCase() === b.substring(0,2).toLowerCase(); }, + equals: (x, y) => { + if (x === y) return true; + // if both x and y are null or undefined and exactly the same + + if (! (x instanceof Object) || ! (y instanceof Object)) return false; + // if they are not strictly equal, they both need to be Objects + + if (x.constructor !== y.constructor) return false; + // they must have the exact same prototype chain, the closest we can do is + // test there constructor. + + for (let p in x) { + if (! x.hasOwnProperty(p)) continue; + // other properties were tested using x.constructor === y.constructor + + if (! y.hasOwnProperty(p)) return false; + // allows to compare x[p] and y[p] when set to undefined + + if (x[p] === y[p]) continue; + // if they have the same strict value or identity then they are equal + + if (typeof(x[p]) !== 'object') return false; + // Numbers, Strings, Functions, Booleans must be strictly equal + + if (! exports.default.equals(x[p], y[p])) return false; + // Objects and Arrays must be tested recursively + } + + for (let p in y) { + if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) return false; + // allows x[p] to be set to undefined + } + + return true; + }, + //ISO6391 language codes from https://pkgstore.datahub.io/core/language-codes/language-codes_csv/data/b65af208b52970a4683fa8fde9af8e9f/language-codes_csv.csv translationLanguages: [ 'de', diff --git a/components/Deck/ActivityFeedPanel/ActivityFeedPanel.js b/components/Deck/ActivityFeedPanel/ActivityFeedPanel.js index 495b3a18d..a75c33b28 100644 --- a/components/Deck/ActivityFeedPanel/ActivityFeedPanel.js +++ b/components/Deck/ActivityFeedPanel/ActivityFeedPanel.js @@ -34,8 +34,7 @@ class ActivityFeedPanel extends React.Component { return (
-

Activity Feed -

+

Activity Feed

{activityDIV}
diff --git a/components/Deck/ActivityFeedPanel/ActivityList.js b/components/Deck/ActivityFeedPanel/ActivityList.js index 7ed3e30fe..97950ba56 100644 --- a/components/Deck/ActivityFeedPanel/ActivityList.js +++ b/components/Deck/ActivityFeedPanel/ActivityList.js @@ -35,6 +35,8 @@ class ActivityList extends React.Component { // TODO: same as in the ActivityFeedStore; check if there is more elegant way to tell the component that action loadMoreActivities (in the onScroll function) was executed if (!nextProps.ActivityFeedStore.wasFetch) return; this.loading = false; + let activitiesCount = this.props.ActivityFeedStore.activities.length; + console.log('ActivityList.componentWillReceiveProps() [' + 'activitiesCount=' + activitiesCount + ']'); } render() { return ( diff --git a/components/Deck/Deck.js b/components/Deck/Deck.js index 80cabf23f..9e4de87e2 100644 --- a/components/Deck/Deck.js +++ b/components/Deck/Deck.js @@ -12,22 +12,49 @@ import SlideEditLeftPanel from './SlideEditLeftPanel/SlideEditLeftPanel'; import ContentPanel from './ContentPanel/ContentPanel'; import NavigationPanel from './NavigationPanel/NavigationPanel'; import ContentModulesPanel from './ContentModulesPanel/ContentModulesPanel'; -//import ActivityFeedPanel from './ActivityFeedPanel/ActivityFeedPanel'; -//import ServiceUnavailable from '../Error/ServiceUnavailable';//NOTE error code has been refactored - this component doesn't exist anymore, code was moved to Error.js in same directory import InfoPanelInfoView from './InfoPanel/InfoPanelInfoView'; import TranslationStore from '../../stores/TranslationStore'; -import { FormattedMessage, defineMessages } from 'react-intl'; +import ContributorsStore from '../../stores/ContributorsStore'; +import {equals} from '../../common'; class Deck extends React.Component { - handleExpandClick(){ - this.context.executeAction(hideLeftColumn, {}); - return false; + constructor(props) { + super(props); + this.isLoading = this.isContentUndefined(); } - handleCollapseClick(){ - this.context.executeAction(restoreDeckPageLayout, {}); - return false; + + // handleExpandClick(){ + // this.context.executeAction(hideLeftColumn, {}); + // return false; + // } + // + // handleCollapseClick(){ + // this.context.executeAction(restoreDeckPageLayout, {}); + // return false; + // } + + shouldComponentUpdate(nextProps, nextState) { + let samePropsState = equals(this.props, nextProps); + this.isLoading = this.isContentUndefined(); + // Content should be updated only when properties have changed. + return !this.isLoading && !samePropsState; } + + componentWillReceiveProps(nextProps) { + this.isLoading = this.isContentUndefined(); + } + + componentWillUnmount() { + this.props.ContributorsStore.contributors = []; + this.isLoading = true; + } + + isContentUndefined() { + return this.props.ContributorsStore.contributors === undefined + || this.props.ContributorsStore.contributors === []; + } + render() { const error = this.props.ServiceErrorStore.error; let status = this.props.DeckPageStore.componentsStatus; @@ -212,7 +239,7 @@ class Deck extends React.Component { rightPanel = (
- +
@@ -251,12 +278,13 @@ Deck.contextTypes = { executeAction: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; -Deck = connectToStores(Deck, [DeckPageStore, ServiceErrorStore, UserProfileStore, TranslationStore], (context, props) => { +Deck = connectToStores(Deck, [DeckPageStore, ServiceErrorStore, UserProfileStore, TranslationStore, ContributorsStore], (context, props) => { return { DeckPageStore: context.getStore(DeckPageStore).getState(), ServiceErrorStore: context.getStore(ServiceErrorStore).getState(), UserProfileStore: context.getStore(UserProfileStore).getState(), - TranslationStore: context.getStore(TranslationStore).getState() + TranslationStore: context.getStore(TranslationStore).getState(), + ContributorsStore: context.getStore(ContributorsStore).getState() }; }); export default Deck; diff --git a/components/Deck/InfoPanel/InfoPanelInfoView.js b/components/Deck/InfoPanel/InfoPanelInfoView.js index 2cb9f034c..5f14bc6aa 100644 --- a/components/Deck/InfoPanel/InfoPanelInfoView.js +++ b/components/Deck/InfoPanel/InfoPanelInfoView.js @@ -7,6 +7,7 @@ import ActivityFeedPanel from '../ActivityFeedPanel/ActivityFeedPanel'; import ContributorsPanel from '../ContentModulesPanel/ContributorsPanel/ContributorsPanel'; import PresentationsPanel from './PresentationsPanel'; import ActivityFeedStore from '../../../stores/ActivityFeedStore'; +import {getLanguageName, equals} from '../../../common'; import TranslationStore from '../../../stores/TranslationStore'; import PermissionsStore from '../../../stores/PermissionsStore'; import {defineMessages} from 'react-intl'; @@ -16,8 +17,9 @@ import ContentStore from '../../../stores/ContentStore'; class InfoPanelInfoView extends React.Component { - constructor(props){ + constructor(props) { super(props); + this.isLoading = true; this.messages = defineMessages({ }); @@ -26,6 +28,13 @@ class InfoPanelInfoView extends React.Component { this.resetZoom = this.resetZoom.bind(this); } + shouldComponentUpdate(nextProps, nextState) { + const samePropsState = equals(this.props, nextProps); + this.isLoading = nextProps.loadingIndicator; + // Content should be updated only when properties have changed. + return !samePropsState; + } + zoomIn() { this.context.executeAction(zoom, { mode: this.props.ContentStore.mode, direction: 'in' }); } @@ -43,12 +52,13 @@ class InfoPanelInfoView extends React.Component { let showZoomControls = this.props.ContentStore.selector.stype === 'slide'; let deckId = selector.get('id'); - if (deckId) { + if (deckId) { deckId = deckId.split('-')[0]; } return (
+ {this.isLoading &&
Loading
} { showZoomControls &&
@@ -78,8 +88,8 @@ class InfoPanelInfoView extends React.Component {
} -
+
@@ -110,6 +120,7 @@ InfoPanelInfoView.contextTypes = { executeAction: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; + InfoPanelInfoView= connectToStores(InfoPanelInfoView, [ActivityFeedStore, DeckTreeStore, TranslationStore, PermissionsStore, ContentStore], (context, props) => { return { ActivityFeedStore: context.getStore(ActivityFeedStore).getState(), diff --git a/stores/ActivityFeedStore.js b/stores/ActivityFeedStore.js index db71c784d..428ff1a3b 100644 --- a/stores/ActivityFeedStore.js +++ b/stores/ActivityFeedStore.js @@ -11,12 +11,14 @@ class ActivityFeedStore extends BaseStore { this.selector = {}; this.hasMore = true; this.presentations = []; + this.loadingIndicator = false; } updateActivities(payload) { this.activities = payload.activities; this.activityType = payload.activityType; this.selector = payload.selector; this.hasMore = payload.hasMore; + this.loadingIndicator = false; this.emitChange(); } loadMoreActivities(payload) { @@ -29,6 +31,7 @@ class ActivityFeedStore extends BaseStore { } updateActivityType(payload) { this.activityType = payload.activityType; + this.loadingIndicator = false; this.emitChange(); } // incrementLikes(payload) { @@ -61,6 +64,7 @@ class ActivityFeedStore extends BaseStore { // } addActivity(payload) { const activity = payload.activity; + this.loadingIndicator = false; if (activity_types_to_display.includes(activity.activity_type) && (this.selector.stype === activity.content_kind && this.selector.sid.split('-')[0] === activity.content_id.split('-')[0] || activity.activity_type === 'move' && this.selector.stype === 'deck' && this.selector.sid.split('-')[0] === activity.move_info.source_id.split('-')[0])) { this.activities.unshift(activity);//add to the beginning @@ -72,6 +76,7 @@ class ActivityFeedStore extends BaseStore { } } addActivities(payload) { + this.loadingIndicator = false; payload.activities.forEach((activity) => { if (activity_types_to_display.includes(activity.activity_type) && (this.selector.stype === activity.content_kind && this.selector.sid.split('-')[0] === activity.content_id.split('-')[0])) { this.activities.unshift(activity);//add to the beginning @@ -84,6 +89,7 @@ class ActivityFeedStore extends BaseStore { this.emitChange(); } addLikeActivity(payload) { + this.loadingIndicator = false; if (payload.selector.stype === 'deck') { let activity = { activity_type: 'react', @@ -105,6 +111,7 @@ class ActivityFeedStore extends BaseStore { } } removeLikeActivity(payload) { + this.loadingIndicator = false; //find like activity and remove it if (payload.selector.stype === 'deck') { let i = 0; @@ -121,6 +128,7 @@ class ActivityFeedStore extends BaseStore { } } updatePresentations(payload) { + this.loadingIndicator = false; // console.log('ActivityFeedStore: updatePresentations', payload); this.presentations = payload; this.emitChange(); @@ -145,6 +153,10 @@ class ActivityFeedStore extends BaseStore { this.hasMore = state.hasMore; this.presentations = state.presentations; } + loading(payload){ + this.loadingIndicator = payload.loadingIndicator; + this.emitChange(); + } } ActivityFeedStore.storeName = 'ActivityFeedStore'; @@ -159,7 +171,8 @@ ActivityFeedStore.handlers = { 'ADD_ACTIVITIES_SUCCESS': 'addActivities', 'LIKE_ACTIVITY_SUCCESS': 'addLikeActivity', 'DISLIKE_ACTIVITY_SUCCESS': 'removeLikeActivity', - 'LOAD_PRESENTATIONS_SUCCESS': 'updatePresentations' + 'LOAD_PRESENTATIONS_SUCCESS': 'updatePresentations', + 'LOAD_ACTIVITIES_LOAD': 'loading' }; export default ActivityFeedStore; diff --git a/stores/ContributorsStore.js b/stores/ContributorsStore.js index 03fd9cf67..17e34d09e 100644 --- a/stores/ContributorsStore.js +++ b/stores/ContributorsStore.js @@ -9,6 +9,11 @@ class ContributorsStore extends BaseStore { this.translators = []; this.listName = ''; this.selector = {}; + this.loadingIndicator = false; + } + loading(payload){ + this.loadingIndicator = payload.loadingIndicator; + this.emitChange(); } updateContributors(payload) { this.contributors = this.getContributors(payload.contributors); @@ -16,6 +21,7 @@ class ContributorsStore extends BaseStore { this.translators = this.getTranslators(payload.contributors); this.listName = payload.listName; this.selector = payload.selector; + this.loadingIndicator = false; this.emitChange(); } @@ -24,7 +30,8 @@ class ContributorsStore extends BaseStore { contributors: this.contributors, creator: this.creator, translators: this.translators, - selector: this.selector + selector: this.selector, + loadingIndicator: this.loadingIndicator }; } dehydrate() { @@ -35,6 +42,7 @@ class ContributorsStore extends BaseStore { this.creator = state.creator; this.translators = state.translators; this.selector = state.selector; + this.loadingIndicator = state.loadingIndicator; } getBasedonRole(role, list) { @@ -60,7 +68,8 @@ class ContributorsStore extends BaseStore { ContributorsStore.storeName = 'ContributorsStore'; ContributorsStore.handlers = { - 'LOAD_CONTRIBUTORS_SUCCESS': 'updateContributors' + 'LOAD_CONTRIBUTORS_SUCCESS': 'updateContributors', + 'LOAD_CONTRIBUTORS_LOAD': 'loading' }; export default ContributorsStore;