From c7ef23ee40496ce04e88d05f741f47e6ee4b5ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Wed, 28 Oct 2015 17:53:26 +0100 Subject: [PATCH 01/36] Implement functionality for feature-detecting webStorage --- js/utils/feature_detection_utils.js | 37 ++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/js/utils/feature_detection_utils.js b/js/utils/feature_detection_utils.js index d086c68c..8e113469 100644 --- a/js/utils/feature_detection_utils.js +++ b/js/utils/feature_detection_utils.js @@ -23,4 +23,39 @@ * @type {bool} Is drag and drop available on this browser */ export const dragAndDropAvailable = 'draggable' in document.createElement('div') && - !/Mobile|Android|Slick\/|Kindle|BlackBerry|Opera Mini|Opera Mobi/i.test(navigator.userAgent); \ No newline at end of file + !/Mobile|Android|Slick\/|Kindle|BlackBerry|Opera Mini|Opera Mobi/i.test(navigator.userAgent); + +/** + * Function that detects whether localStorage/sessionStorage is both supported + * and available. + * Taken from: + * https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API + * + * We're explicitly NOT exporting this function, as we want for both localStorage, as well as + * sessionStorage proxy functions to be exported. + * + * @param {oneOfType(['localStorage', 'sessionStorage'])} + * @return {bool} Is localStorage or sessionStorage available on this browser + */ +function storageAvailable(type) { + try { + var storage = window[type], + x = '__storage_test__'; + storage.setItem(x, x); + storage.removeItem(x); + return true; + } + catch(e) { + return false; + } +} + +/** + * Function detects whether sessionStorage is both supported + * and available. + * @return {bool} Is sessionStorage available on this browser + */ +export function sessionStorageAvailable() { + return storageAvailable('sessionStorage'); +} + From bed067f9bc05f1ef65e3158fab37dfe57fb03c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Thu, 29 Oct 2015 17:15:26 +0100 Subject: [PATCH 02/36] Add first cut on persistent stores --- js/actions/user_actions.js | 15 ++++ js/models/ascribe_storage.js | 98 +++++++++++++++++++++++++++ js/stores/session_persistent_store.js | 21 ++++++ js/stores/user_store.js | 10 ++- js/utils/feature_detection_utils.js | 7 +- js/utils/general_utils.js | 9 ++- 6 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 js/models/ascribe_storage.js create mode 100644 js/stores/session_persistent_store.js diff --git a/js/actions/user_actions.js b/js/actions/user_actions.js index 3780a802..59fdfa25 100644 --- a/js/actions/user_actions.js +++ b/js/actions/user_actions.js @@ -3,6 +3,8 @@ import { altUser } from '../alt'; import UserFetcher from '../fetchers/user_fetcher'; +import UserStore from '../stores/user_store'; + class UserActions { constructor() { @@ -22,6 +24,19 @@ class UserActions { this.actions.updateCurrentUser({}); }); } + + /*fetchCurrentUser() { + if(UserStore.getState().currentUser && !UserStore.getState().currentUser.email) { + UserFetcher.fetchOne() + .then((res) => { + this.actions.updateCurrentUser(res.users[0]); + }) + .catch((err) => { + console.logGlobal(err); + this.actions.updateCurrentUser({}); + }); + } + }*/ logoutCurrentUser() { UserFetcher.logout() diff --git a/js/models/ascribe_storage.js b/js/models/ascribe_storage.js new file mode 100644 index 00000000..9f71e243 --- /dev/null +++ b/js/models/ascribe_storage.js @@ -0,0 +1,98 @@ +'use strict'; + +import { sanitize } from '../utils/general_utils'; + +/** + * A tiny wrapper around HTML5's `webStorage`, + * to enable saving JSON objects directly into `webStorage` + */ +export default class AscribeStorage { + /** + * @param {String} `name` A unique storage name + */ + constructor(type, name) { + if(type === 'localStorage' || type === 'sessionStorage') { + this.storage = window[type]; + } else { + throw new Error('"type" can only be either "localStorage" or "sessionStorage"'); + } + + this.name = name; + } + + /** + * Private method, do not use from the outside. + * Constructs a unique identifier for a item in the global `webStorage`, + * by appending the `ÀscribeStorage`'s name + * @param {string} key A unique identifier + * @return {string} A globally unique identifier for a value in `webStorage` + */ + _constructStorageKey(key) { + return `${this.name}-${key}`; + } + + _deconstructStorageKey(key) { + return key.replace(`${this.name}-`, ''); + } + + /** + * Saves a JSON-serializeble object or a string into `webStorage` + * @param {string} key Used as a unique identifier + * @param {oneOfType([String, object])} value Either JSON-serializeble or a string + */ + setItem(key, value) { + // We're "try-catching" errors in this method ourselves, to be able to + // yield more readable error messages + + if(!key || !value) { + throw new Error('"key" or "value" cannot be "falsy" values'); + } else if(typeof value === 'string') { + // since `value` is a string, we can directly write + // it into `this.storage` + this.storage.setItem(this._constructStorageKey(key), value); + } else { + // if `value` is not a string, we need to JSON-serialize it and then + // put it into `this.storage` + + let serializedValue; + try { + serializedValue = JSON.stringify(value); + } catch(err) { + throw new Error('You didn\'t pass valid JSON as "value" into setItem.'); + } + + try { + this.storage.setItem(this._constructStorageKey(key), serializedValue); + } catch(err) { + throw new Error('Failure saving a "serializedValue" in setItem'); + } + } + } + + getItem(key) { + let deserializedValue; + const serializedValue = this.storage.getItem(this._constructStorageKey(key)); + + try { + deserializedValue = JSON.parse(serializedValue); + } catch(err) { + deserializedValue = serializedValue; + } + + return deserializedValue; + } + + toObject() { + let obj = {}; + const storageCopy = JSON.parse(JSON.stringify(this.storage)); + const sanitizedStore = sanitize(storageCopy, s => !s.match(`${this.name}-`), true); + + Object + .keys(sanitizedStore) + .forEach((key) => { + obj[this._deconstructStorageKey(key)] = JSON.parse(sanitizedStore[key]); + }); + + return obj; + } +} \ No newline at end of file diff --git a/js/stores/session_persistent_store.js b/js/stores/session_persistent_store.js new file mode 100644 index 00000000..cd2fa1ca --- /dev/null +++ b/js/stores/session_persistent_store.js @@ -0,0 +1,21 @@ +'use strict'; + +import AscribeStorage from '../models/ascribe_storage'; + + +export default class SessionPersistentStore extends AscribeStorage { + constructor(name) { + super('sessionStorage', name); + } + + setItem(key, value) { + this[key] = value; + super.setItem(key, value); + } +} + +SessionPersistentStore.config = { + getState() { + return new AscribeStorage('sessionStorage', this.displayName).toObject(); + } +}; \ No newline at end of file diff --git a/js/stores/user_store.js b/js/stores/user_store.js index 8ea18eea..dc55f0c2 100644 --- a/js/stores/user_store.js +++ b/js/stores/user_store.js @@ -3,15 +3,21 @@ import { altUser } from '../alt'; import UserActions from '../actions/user_actions'; +import SessionPersistentStore from './session_persistent_store'; -class UserStore { +// import AscribeStorage from '../models/ascribe_storage'; +// import { sessionStorageAvailable } from '../utils/feature_detection_utils'; + + +class UserStore extends SessionPersistentStore { constructor() { + super('UserStore'); this.currentUser = {}; this.bindActions(UserActions); } onUpdateCurrentUser(user) { - this.currentUser = user; + this.setItem('currentUser', user); } onDeleteCurrentUser() { this.currentUser = {}; diff --git a/js/utils/feature_detection_utils.js b/js/utils/feature_detection_utils.js index 8e113469..66e623d2 100644 --- a/js/utils/feature_detection_utils.js +++ b/js/utils/feature_detection_utils.js @@ -51,11 +51,8 @@ function storageAvailable(type) { } /** - * Function detects whether sessionStorage is both supported + * Const that detects whether sessionStorage is both supported * and available. - * @return {bool} Is sessionStorage available on this browser */ -export function sessionStorageAvailable() { - return storageAvailable('sessionStorage'); -} +export const sessionStorageAvailable = storageAvailable('sessionStorage'); diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index 7c13f9b5..07d42327 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -6,9 +6,12 @@ * tagged as false by the passed in filter function * * @param {object} obj regular javascript object + * @param {function} filterFn a filter function for filtering either by key or value + * @param {bool} filterByKey a boolean for choosing whether the object should be filtered by + * key or value * @return {object} regular javascript object without null values or empty strings */ -export function sanitize(obj, filterFn) { +export function sanitize(obj, filterFn, filterByKey) { if(!filterFn) { // By matching null with a double equal, we can match undefined and null // http://stackoverflow.com/a/15992131 @@ -18,7 +21,9 @@ export function sanitize(obj, filterFn) { Object .keys(obj) .map((key) => { - if(filterFn(obj[key])) { + const filterCondition = filterByKey ? filterFn(key) : filterFn(obj[key]); + + if(filterCondition) { delete obj[key]; } }); From 469f5108a86d1f9f24f527b74cc42e347a40fd47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Fri, 30 Oct 2015 16:57:03 +0100 Subject: [PATCH 03/36] Implement cached source for user endpoint --- js/actions/user_actions.js | 29 ++------------------------- js/sources/user_source.js | 25 +++++++++++++++++++++++ js/stores/session_persistent_store.js | 8 +------- js/stores/user_store.js | 17 +++++++++++----- 4 files changed, 40 insertions(+), 39 deletions(-) create mode 100644 js/sources/user_source.js diff --git a/js/actions/user_actions.js b/js/actions/user_actions.js index 59fdfa25..f9666cdb 100644 --- a/js/actions/user_actions.js +++ b/js/actions/user_actions.js @@ -3,41 +3,16 @@ import { altUser } from '../alt'; import UserFetcher from '../fetchers/user_fetcher'; -import UserStore from '../stores/user_store'; - class UserActions { constructor() { this.generateActions( - 'updateCurrentUser', + 'fetchCurrentUser', + 'receiveCurrentUser', 'deleteCurrentUser' ); } - fetchCurrentUser() { - UserFetcher.fetchOne() - .then((res) => { - this.actions.updateCurrentUser(res.users[0]); - }) - .catch((err) => { - console.logGlobal(err); - this.actions.updateCurrentUser({}); - }); - } - - /*fetchCurrentUser() { - if(UserStore.getState().currentUser && !UserStore.getState().currentUser.email) { - UserFetcher.fetchOne() - .then((res) => { - this.actions.updateCurrentUser(res.users[0]); - }) - .catch((err) => { - console.logGlobal(err); - this.actions.updateCurrentUser({}); - }); - } - }*/ - logoutCurrentUser() { UserFetcher.logout() .then(() => { diff --git a/js/sources/user_source.js b/js/sources/user_source.js new file mode 100644 index 00000000..0ac9c6ba --- /dev/null +++ b/js/sources/user_source.js @@ -0,0 +1,25 @@ +'use strict'; + +import requests from '../utils/requests'; +import UserActions from '../actions/user_actions'; + + +const UserSource = { + fetchUser: { + remote() { + return requests.get('user'); + }, + + local(state) { + return state.currentUser && state.currentUser.email ? state.currentUser : {}; + }, + + success: UserActions.receiveCurrentUser, + + shouldFetch(state) { + return state.currentUser && !state.currentUser.email; + } + } +}; + +export default UserSource; \ No newline at end of file diff --git a/js/stores/session_persistent_store.js b/js/stores/session_persistent_store.js index cd2fa1ca..0036740a 100644 --- a/js/stores/session_persistent_store.js +++ b/js/stores/session_persistent_store.js @@ -12,10 +12,4 @@ export default class SessionPersistentStore extends AscribeStorage { this[key] = value; super.setItem(key, value); } -} - -SessionPersistentStore.config = { - getState() { - return new AscribeStorage('sessionStorage', this.displayName).toObject(); - } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/js/stores/user_store.js b/js/stores/user_store.js index dc55f0c2..cdbb40ea 100644 --- a/js/stores/user_store.js +++ b/js/stores/user_store.js @@ -3,22 +3,29 @@ import { altUser } from '../alt'; import UserActions from '../actions/user_actions'; -import SessionPersistentStore from './session_persistent_store'; +import UserSource from '../sources/user_source'; // import AscribeStorage from '../models/ascribe_storage'; // import { sessionStorageAvailable } from '../utils/feature_detection_utils'; -class UserStore extends SessionPersistentStore { +class UserStore { constructor() { - super('UserStore'); this.currentUser = {}; this.bindActions(UserActions); + this.registerAsync(UserSource); } - onUpdateCurrentUser(user) { - this.setItem('currentUser', user); + onFetchCurrentUser() { + if(!this.getInstance().isLoading()) { + this.getInstance().fetchUser(); + } } + + onReceiveCurrentUser({users: [user]}) { + this.currentUser = user; + } + onDeleteCurrentUser() { this.currentUser = {}; } From 11f3ab51a3af0f5cb5ae74146e9ef5ed3bce1b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Mon, 2 Nov 2015 09:55:53 +0100 Subject: [PATCH 04/36] Revert "Implement functionality for feature-detecting webStorage" This reverts commit c7ef23ee40496ce04e88d05f741f47e6ee4b5ecb. Conflicts: js/utils/feature_detection_utils.js --- js/utils/feature_detection_utils.js | 34 +---------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/js/utils/feature_detection_utils.js b/js/utils/feature_detection_utils.js index 66e623d2..d086c68c 100644 --- a/js/utils/feature_detection_utils.js +++ b/js/utils/feature_detection_utils.js @@ -23,36 +23,4 @@ * @type {bool} Is drag and drop available on this browser */ export const dragAndDropAvailable = 'draggable' in document.createElement('div') && - !/Mobile|Android|Slick\/|Kindle|BlackBerry|Opera Mini|Opera Mobi/i.test(navigator.userAgent); - -/** - * Function that detects whether localStorage/sessionStorage is both supported - * and available. - * Taken from: - * https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API - * - * We're explicitly NOT exporting this function, as we want for both localStorage, as well as - * sessionStorage proxy functions to be exported. - * - * @param {oneOfType(['localStorage', 'sessionStorage'])} - * @return {bool} Is localStorage or sessionStorage available on this browser - */ -function storageAvailable(type) { - try { - var storage = window[type], - x = '__storage_test__'; - storage.setItem(x, x); - storage.removeItem(x); - return true; - } - catch(e) { - return false; - } -} - -/** - * Const that detects whether sessionStorage is both supported - * and available. - */ -export const sessionStorageAvailable = storageAvailable('sessionStorage'); - + !/Mobile|Android|Slick\/|Kindle|BlackBerry|Opera Mini|Opera Mobi/i.test(navigator.userAgent); \ No newline at end of file From ff4067e637c71c9be257b0feb6415827051ea4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Mon, 2 Nov 2015 09:59:59 +0100 Subject: [PATCH 05/36] Revert "Add first cut on persistent stores" This reverts commit bed067f9bc05f1ef65e3158fab37dfe57fb03c2c. Conflicts: js/actions/user_actions.js js/stores/session_persistent_store.js js/stores/user_store.js js/utils/feature_detection_utils.js --- js/models/ascribe_storage.js | 98 ----------------------------- js/stores/user_store.js | 3 - js/utils/feature_detection_utils.js | 2 +- js/utils/general_utils.js | 9 +-- 4 files changed, 3 insertions(+), 109 deletions(-) delete mode 100644 js/models/ascribe_storage.js diff --git a/js/models/ascribe_storage.js b/js/models/ascribe_storage.js deleted file mode 100644 index 9f71e243..00000000 --- a/js/models/ascribe_storage.js +++ /dev/null @@ -1,98 +0,0 @@ -'use strict'; - -import { sanitize } from '../utils/general_utils'; - -/** - * A tiny wrapper around HTML5's `webStorage`, - * to enable saving JSON objects directly into `webStorage` - */ -export default class AscribeStorage { - /** - * @param {String} `name` A unique storage name - */ - constructor(type, name) { - if(type === 'localStorage' || type === 'sessionStorage') { - this.storage = window[type]; - } else { - throw new Error('"type" can only be either "localStorage" or "sessionStorage"'); - } - - this.name = name; - } - - /** - * Private method, do not use from the outside. - * Constructs a unique identifier for a item in the global `webStorage`, - * by appending the `ÀscribeStorage`'s name - * @param {string} key A unique identifier - * @return {string} A globally unique identifier for a value in `webStorage` - */ - _constructStorageKey(key) { - return `${this.name}-${key}`; - } - - _deconstructStorageKey(key) { - return key.replace(`${this.name}-`, ''); - } - - /** - * Saves a JSON-serializeble object or a string into `webStorage` - * @param {string} key Used as a unique identifier - * @param {oneOfType([String, object])} value Either JSON-serializeble or a string - */ - setItem(key, value) { - // We're "try-catching" errors in this method ourselves, to be able to - // yield more readable error messages - - if(!key || !value) { - throw new Error('"key" or "value" cannot be "falsy" values'); - } else if(typeof value === 'string') { - // since `value` is a string, we can directly write - // it into `this.storage` - this.storage.setItem(this._constructStorageKey(key), value); - } else { - // if `value` is not a string, we need to JSON-serialize it and then - // put it into `this.storage` - - let serializedValue; - try { - serializedValue = JSON.stringify(value); - } catch(err) { - throw new Error('You didn\'t pass valid JSON as "value" into setItem.'); - } - - try { - this.storage.setItem(this._constructStorageKey(key), serializedValue); - } catch(err) { - throw new Error('Failure saving a "serializedValue" in setItem'); - } - } - } - - getItem(key) { - let deserializedValue; - const serializedValue = this.storage.getItem(this._constructStorageKey(key)); - - try { - deserializedValue = JSON.parse(serializedValue); - } catch(err) { - deserializedValue = serializedValue; - } - - return deserializedValue; - } - - toObject() { - let obj = {}; - const storageCopy = JSON.parse(JSON.stringify(this.storage)); - const sanitizedStore = sanitize(storageCopy, s => !s.match(`${this.name}-`), true); - - Object - .keys(sanitizedStore) - .forEach((key) => { - obj[this._deconstructStorageKey(key)] = JSON.parse(sanitizedStore[key]); - }); - - return obj; - } -} \ No newline at end of file diff --git a/js/stores/user_store.js b/js/stores/user_store.js index cdbb40ea..94d7777e 100644 --- a/js/stores/user_store.js +++ b/js/stores/user_store.js @@ -5,9 +5,6 @@ import UserActions from '../actions/user_actions'; import UserSource from '../sources/user_source'; -// import AscribeStorage from '../models/ascribe_storage'; -// import { sessionStorageAvailable } from '../utils/feature_detection_utils'; - class UserStore { constructor() { diff --git a/js/utils/feature_detection_utils.js b/js/utils/feature_detection_utils.js index d086c68c..6cbd533d 100644 --- a/js/utils/feature_detection_utils.js +++ b/js/utils/feature_detection_utils.js @@ -23,4 +23,4 @@ * @type {bool} Is drag and drop available on this browser */ export const dragAndDropAvailable = 'draggable' in document.createElement('div') && - !/Mobile|Android|Slick\/|Kindle|BlackBerry|Opera Mini|Opera Mobi/i.test(navigator.userAgent); \ No newline at end of file + !/Mobile|Android|Slick\/|Kindle|BlackBerry|Opera Mini|Opera Mobi/i.test(navigator.userAgent); diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index 07d42327..7c13f9b5 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -6,12 +6,9 @@ * tagged as false by the passed in filter function * * @param {object} obj regular javascript object - * @param {function} filterFn a filter function for filtering either by key or value - * @param {bool} filterByKey a boolean for choosing whether the object should be filtered by - * key or value * @return {object} regular javascript object without null values or empty strings */ -export function sanitize(obj, filterFn, filterByKey) { +export function sanitize(obj, filterFn) { if(!filterFn) { // By matching null with a double equal, we can match undefined and null // http://stackoverflow.com/a/15992131 @@ -21,9 +18,7 @@ export function sanitize(obj, filterFn, filterByKey) { Object .keys(obj) .map((key) => { - const filterCondition = filterByKey ? filterFn(key) : filterFn(obj[key]); - - if(filterCondition) { + if(filterFn(obj[key])) { delete obj[key]; } }); From d50aa2913f306a21a20b644491c8c632f38cf75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Mon, 2 Nov 2015 10:00:50 +0100 Subject: [PATCH 06/36] Remove SessionPersistentStore --- js/stores/session_persistent_store.js | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 js/stores/session_persistent_store.js diff --git a/js/stores/session_persistent_store.js b/js/stores/session_persistent_store.js deleted file mode 100644 index 0036740a..00000000 --- a/js/stores/session_persistent_store.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -import AscribeStorage from '../models/ascribe_storage'; - - -export default class SessionPersistentStore extends AscribeStorage { - constructor(name) { - super('sessionStorage', name); - } - - setItem(key, value) { - this[key] = value; - super.setItem(key, value); - } -} \ No newline at end of file From 0770a1ed61b9d0d3c83bd88c0644de675ffdbcf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Mon, 2 Nov 2015 11:31:02 +0100 Subject: [PATCH 07/36] Implement cache invalidation functionality for UserStore & UserSources --- .../ascribe_routes/proxy_routes/auth_proxy_handler.js | 6 +++++- js/components/ascribe_settings/account_settings.js | 2 +- js/components/ascribe_settings/settings_container.js | 4 ++-- js/sources/user_source.js | 4 ++-- js/stores/user_store.js | 7 ++++++- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js b/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js index cdfc129b..aff1eb9c 100644 --- a/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js +++ b/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js @@ -47,7 +47,11 @@ export default function AuthProxyHandler({to, when}) { }, componentDidUpdate() { - this.redirectConditionally(); + // Only refresh this component, when UserSources are not loading + // data from the server + if(!UserStore.isLoading()) { + this.redirectConditionally(); + } }, componentWillUnmount() { diff --git a/js/components/ascribe_settings/account_settings.js b/js/components/ascribe_settings/account_settings.js index f337a17b..c650358c 100644 --- a/js/components/ascribe_settings/account_settings.js +++ b/js/components/ascribe_settings/account_settings.js @@ -27,7 +27,7 @@ let AccountSettings = React.createClass({ }, handleSuccess(){ - this.props.loadUser(); + this.props.loadUser(true); let notification = new GlobalNotificationModel(getLangText('Settings succesfully updated'), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); }, diff --git a/js/components/ascribe_settings/settings_container.js b/js/components/ascribe_settings/settings_container.js index 923759fd..5b05e708 100644 --- a/js/components/ascribe_settings/settings_container.js +++ b/js/components/ascribe_settings/settings_container.js @@ -46,8 +46,8 @@ let SettingsContainer = React.createClass({ UserStore.unlisten(this.onChange); }, - loadUser(){ - UserActions.fetchCurrentUser(); + loadUser(invalidateCache){ + UserActions.fetchCurrentUser(invalidateCache); }, onChange(state) { diff --git a/js/sources/user_source.js b/js/sources/user_source.js index 0ac9c6ba..ac73093a 100644 --- a/js/sources/user_source.js +++ b/js/sources/user_source.js @@ -11,13 +11,13 @@ const UserSource = { }, local(state) { - return state.currentUser && state.currentUser.email ? state.currentUser : {}; + return state.currentUser && state.currentUser.email ? state : {}; }, success: UserActions.receiveCurrentUser, shouldFetch(state) { - return state.currentUser && !state.currentUser.email; + return state.invalidateCache || state.currentUser && !state.currentUser.email; } } }; diff --git a/js/stores/user_store.js b/js/stores/user_store.js index 94d7777e..ae19c89c 100644 --- a/js/stores/user_store.js +++ b/js/stores/user_store.js @@ -9,17 +9,22 @@ import UserSource from '../sources/user_source'; class UserStore { constructor() { this.currentUser = {}; + this.invalidateCache = false; + this.bindActions(UserActions); this.registerAsync(UserSource); } - onFetchCurrentUser() { + onFetchCurrentUser(invalidateCache) { + this.invalidateCache = invalidateCache; + if(!this.getInstance().isLoading()) { this.getInstance().fetchUser(); } } onReceiveCurrentUser({users: [user]}) { + this.invalidateCache = false; this.currentUser = user; } From 0157c048ab016280443a1429d7dee9d43e03a69e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Mon, 2 Nov 2015 15:38:37 +0100 Subject: [PATCH 08/36] Add cache invalidation for signup and login --- js/components/ascribe_forms/form_login.js | 2 +- js/components/ascribe_forms/form_signup.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/components/ascribe_forms/form_login.js b/js/components/ascribe_forms/form_login.js index 2f0265b6..8b7c1e23 100644 --- a/js/components/ascribe_forms/form_login.js +++ b/js/components/ascribe_forms/form_login.js @@ -60,7 +60,7 @@ let LoginForm = React.createClass({ GlobalNotificationActions.appendGlobalNotification(notification); if(success) { - UserActions.fetchCurrentUser(); + UserActions.fetchCurrentUser(true); } }, diff --git a/js/components/ascribe_forms/form_signup.js b/js/components/ascribe_forms/form_signup.js index 87b7f47a..22f3e120 100644 --- a/js/components/ascribe_forms/form_signup.js +++ b/js/components/ascribe_forms/form_signup.js @@ -61,7 +61,7 @@ let SignupForm = React.createClass({ // Refactor this to its own component this.props.handleSuccess(getLangText('We sent an email to your address') + ' ' + response.user.email + ', ' + getLangText('please confirm') + '.'); } else { - UserActions.fetchCurrentUser(); + UserActions.fetchCurrentUser(true); } }, From 7ce7f4d17d62270566dfb39016fbdc19536c19b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Mon, 2 Nov 2015 15:47:57 +0100 Subject: [PATCH 09/36] Completing prototype for using alt.js's sources instead of fetchers --- js/actions/user_actions.js | 15 +++------------ js/sources/user_source.js | 15 ++++++++++++--- js/stores/user_store.js | 8 +++++++- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/js/actions/user_actions.js b/js/actions/user_actions.js index f9666cdb..8c127e63 100644 --- a/js/actions/user_actions.js +++ b/js/actions/user_actions.js @@ -1,7 +1,6 @@ 'use strict'; import { altUser } from '../alt'; -import UserFetcher from '../fetchers/user_fetcher'; class UserActions { @@ -9,19 +8,11 @@ class UserActions { this.generateActions( 'fetchCurrentUser', 'receiveCurrentUser', - 'deleteCurrentUser' + 'logoutCurrentUser', + 'deleteCurrentUser', + 'currentUserFailed' ); } - - logoutCurrentUser() { - UserFetcher.logout() - .then(() => { - this.actions.deleteCurrentUser(); - }) - .catch((err) => { - console.logGlobal(err); - }); - } } export default altUser.createActions(UserActions); diff --git a/js/sources/user_source.js b/js/sources/user_source.js index ac73093a..28b9448d 100644 --- a/js/sources/user_source.js +++ b/js/sources/user_source.js @@ -1,11 +1,13 @@ 'use strict'; import requests from '../utils/requests'; +import ApiUrls from '../constants/api_urls'; + import UserActions from '../actions/user_actions'; const UserSource = { - fetchUser: { + fetchCurrentUser: { remote() { return requests.get('user'); }, @@ -13,12 +15,19 @@ const UserSource = { local(state) { return state.currentUser && state.currentUser.email ? state : {}; }, - success: UserActions.receiveCurrentUser, - + error: UserActions.currentUserFailed, shouldFetch(state) { return state.invalidateCache || state.currentUser && !state.currentUser.email; } + }, + + logoutCurrentUser: { + remote() { + return requests.get(ApiUrls.users_logout); + }, + success: UserActions.deleteCurrentUser, + error: UserActions.currentUserFailed } }; diff --git a/js/stores/user_store.js b/js/stores/user_store.js index ae19c89c..3bd69255 100644 --- a/js/stores/user_store.js +++ b/js/stores/user_store.js @@ -10,6 +10,7 @@ class UserStore { constructor() { this.currentUser = {}; this.invalidateCache = false; + this.errorMessage = null; this.bindActions(UserActions); this.registerAsync(UserSource); @@ -19,7 +20,7 @@ class UserStore { this.invalidateCache = invalidateCache; if(!this.getInstance().isLoading()) { - this.getInstance().fetchUser(); + this.getInstance().fetchCurrentUser(); } } @@ -31,6 +32,11 @@ class UserStore { onDeleteCurrentUser() { this.currentUser = {}; } + + onCurrentUserFailed(err) { + console.logGlobal(err); + this.errorMessage = err; + } } export default altUser.createStore(UserStore, 'UserStore'); From f0325f24730701c55923e5231b2b970bf8556dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Mon, 2 Nov 2015 16:32:55 +0100 Subject: [PATCH 10/36] Specify and applying naming conventions :page_with_curl: for source and store methods --- js/actions/user_actions.js | 4 +-- js/sources/NAMING_CONVENTIONS.md | 59 ++++++++++++++++++++++++++++++++ js/sources/user_source.js | 8 ++--- js/stores/user_store.js | 10 ++++-- 4 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 js/sources/NAMING_CONVENTIONS.md diff --git a/js/actions/user_actions.js b/js/actions/user_actions.js index 8c127e63..334b6e6c 100644 --- a/js/actions/user_actions.js +++ b/js/actions/user_actions.js @@ -7,9 +7,9 @@ class UserActions { constructor() { this.generateActions( 'fetchCurrentUser', - 'receiveCurrentUser', + 'successFetchCurrentUser', 'logoutCurrentUser', - 'deleteCurrentUser', + 'successLogoutCurrentUser', 'currentUserFailed' ); } diff --git a/js/sources/NAMING_CONVENTIONS.md b/js/sources/NAMING_CONVENTIONS.md new file mode 100644 index 00000000..4bd0e14f --- /dev/null +++ b/js/sources/NAMING_CONVENTIONS.md @@ -0,0 +1,59 @@ +# Naming conventions for sources + +## Introduction + +When using alt.js's sources, we don't want the source's method to clash with the store/action's method names. + +While actions will still be named by the following schema: + +``` + +``` + +e.g. + +``` +fetchCurrentUser +logoutCurrentUser +fetchApplication +refreshApplicationToken +``` + +we cannot repeat this for a sources' methods as patterns like this would emerge in the stores: + +```javascript +onFetchCurrentUser(invalidateCache) { + this.invalidateCache = invalidateCache; + + if(!this.getInstance().isLoading()) { + this.getInstance().fetchCurrentUser(); // does not call a flux "action" but a method in user_source.js - which is confusing + } +} +``` + +Therefore we're introducing the following naming convention: + +## Rules + +1. All source methods that perform a data lookup of any kind (be it cached or not), are called `lookup`. As a mnemonic aid, "You *lookup* something in a *source*". +2. For all methods that do not fit 1.), we prepend `perform`. + +## Examples + +### Examples for Rule 1.) +*HTTP GET'ing the current User* + +```javascript +UserActions.fetchCurrentUser +UserStore.onFetchCurrentUser +UserSource.lookupCurrentUser +``` + +### Examples for Rule 2.) +*HTTP GET'ing a certain user endpoint, that logs the user out :sad_face:(, as this should not be a GET request anyway)* + +```javascript +UserActions.logoutCurrentUser +UserStore.onLogoutCurrentUser +UserSource.performLogoutCurrentUser +``` \ No newline at end of file diff --git a/js/sources/user_source.js b/js/sources/user_source.js index 28b9448d..05165391 100644 --- a/js/sources/user_source.js +++ b/js/sources/user_source.js @@ -7,7 +7,7 @@ import UserActions from '../actions/user_actions'; const UserSource = { - fetchCurrentUser: { + lookupCurrentUser: { remote() { return requests.get('user'); }, @@ -15,18 +15,18 @@ const UserSource = { local(state) { return state.currentUser && state.currentUser.email ? state : {}; }, - success: UserActions.receiveCurrentUser, + success: UserActions.successFetchCurrentUser, error: UserActions.currentUserFailed, shouldFetch(state) { return state.invalidateCache || state.currentUser && !state.currentUser.email; } }, - logoutCurrentUser: { + performLogoutCurrentUser: { remote() { return requests.get(ApiUrls.users_logout); }, - success: UserActions.deleteCurrentUser, + success: UserActions.successLogoutCurrentUser, error: UserActions.currentUserFailed } }; diff --git a/js/stores/user_store.js b/js/stores/user_store.js index 3bd69255..cbd7d582 100644 --- a/js/stores/user_store.js +++ b/js/stores/user_store.js @@ -20,16 +20,20 @@ class UserStore { this.invalidateCache = invalidateCache; if(!this.getInstance().isLoading()) { - this.getInstance().fetchCurrentUser(); + this.getInstance().lookupCurrentUser(); } } - onReceiveCurrentUser({users: [user]}) { + onSuccessFetchCurrentUser({users: [user]}) { this.invalidateCache = false; this.currentUser = user; } - onDeleteCurrentUser() { + onLogoutCurrentUser() { + this.getInstance().performLogoutCurrentUser(); + } + + onSuccessLogoutCurrentUser() { this.currentUser = {}; } From 6a0c4b4272eeadeb020a843c7cbf2895c6f6771f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Tue, 3 Nov 2015 09:52:54 +0100 Subject: [PATCH 11/36] Remove user_fetcher.js --- js/fetchers/user_fetcher.js | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 js/fetchers/user_fetcher.js diff --git a/js/fetchers/user_fetcher.js b/js/fetchers/user_fetcher.js deleted file mode 100644 index eca7494d..00000000 --- a/js/fetchers/user_fetcher.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -import requests from '../utils/requests'; -import ApiUrls from '../constants/api_urls'; - -let UserFetcher = { - /** - * Fetch one user from the API. - * If no arg is supplied, load the current user - */ - fetchOne() { - return requests.get('user'); - }, - - logout() { - return requests.get(ApiUrls.users_logout); - } -}; - -export default UserFetcher; From 35e9bedf045d86770efc418f27835d3a3095fd81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Tue, 3 Nov 2015 10:07:44 +0100 Subject: [PATCH 12/36] Convert WhitelabelFetcher to WhitelabelSource --- js/actions/whitelabel_actions.js | 19 +++---------------- js/fetchers/whitelabel_fetcher.js | 17 ----------------- js/stores/user_store.js | 12 +++++++----- js/stores/whitelabel_store.js | 23 ++++++++++++++++++++++- 4 files changed, 32 insertions(+), 39 deletions(-) delete mode 100644 js/fetchers/whitelabel_fetcher.js diff --git a/js/actions/whitelabel_actions.js b/js/actions/whitelabel_actions.js index a1460fb8..be7fb59f 100644 --- a/js/actions/whitelabel_actions.js +++ b/js/actions/whitelabel_actions.js @@ -1,29 +1,16 @@ 'use strict'; import { altWhitelabel } from '../alt'; -import WhitelabelFetcher from '../fetchers/whitelabel_fetcher'; class WhitelabelActions { constructor() { this.generateActions( - 'updateWhitelabel' + 'fetchWhitelabel', + 'successFetchWhitelabel', + 'whitelabelFailed' ); } - - fetchWhitelabel() { - WhitelabelFetcher.fetch() - .then((res) => { - if(res && res.whitelabel) { - this.actions.updateWhitelabel(res.whitelabel); - } else { - this.actions.updateWhitelabel({}); - } - }) - .catch((err) => { - console.logGlobal(err); - }); - } } export default altWhitelabel.createActions(WhitelabelActions); diff --git a/js/fetchers/whitelabel_fetcher.js b/js/fetchers/whitelabel_fetcher.js deleted file mode 100644 index 7a39f676..00000000 --- a/js/fetchers/whitelabel_fetcher.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -import requests from '../utils/requests'; - -import { getSubdomain } from '../utils/general_utils'; - - -let WhitelabelFetcher = { - /** - * Fetch the custom whitelabel data from the API. - */ - fetch() { - return requests.get('whitelabel_settings', {'subdomain': getSubdomain()}); - } -}; - -export default WhitelabelFetcher; diff --git a/js/stores/user_store.js b/js/stores/user_store.js index cbd7d582..57c769d9 100644 --- a/js/stores/user_store.js +++ b/js/stores/user_store.js @@ -9,15 +9,17 @@ import UserSource from '../sources/user_source'; class UserStore { constructor() { this.currentUser = {}; - this.invalidateCache = false; - this.errorMessage = null; + this.userMeta = { + invalidateCache: false, + err: null + }; this.bindActions(UserActions); this.registerAsync(UserSource); } onFetchCurrentUser(invalidateCache) { - this.invalidateCache = invalidateCache; + this.userMeta.invalidateCache = invalidateCache; if(!this.getInstance().isLoading()) { this.getInstance().lookupCurrentUser(); @@ -25,7 +27,7 @@ class UserStore { } onSuccessFetchCurrentUser({users: [user]}) { - this.invalidateCache = false; + this.userMeta.invalidateCache = false; this.currentUser = user; } @@ -39,7 +41,7 @@ class UserStore { onCurrentUserFailed(err) { console.logGlobal(err); - this.errorMessage = err; + this.userMeta.err = err; } } diff --git a/js/stores/whitelabel_store.js b/js/stores/whitelabel_store.js index 017fb98e..477e061c 100644 --- a/js/stores/whitelabel_store.js +++ b/js/stores/whitelabel_store.js @@ -2,17 +2,38 @@ import { altWhitelabel } from '../alt'; import WhitelabelActions from '../actions/whitelabel_actions'; +import WhitelabelSource from '../sources/whitelabel_source'; class WhitelabelStore { constructor() { this.whitelabel = {}; + this.whitelabelMeta = { + invalidateCache: false, + err: null + }; + this.bindActions(WhitelabelActions); + this.registerAsync(WhitelabelSource); } - onUpdateWhitelabel(whitelabel) { + onFetchWhitelabel(invalidateCache) { + this.whitelabelMeta.invalidateCache = invalidateCache; + + if(!this.getInstance().isLoading()) { + this.getInstance().lookupWhitelabel(); + } + } + + onSuccessFetchWhitelabel(whitelabel) { + this.whitelabelMeta.invalidateCache = false; this.whitelabel = whitelabel; } + + onWhitelabelFailed(err) { + console.logGlobal(err); + this.whitelabelMeta.err = err; + } } export default altWhitelabel.createStore(WhitelabelStore, 'WhitelabelStore'); From ecdcbbc25e4d6d958385e62bef8433caeb4c8c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Tue, 3 Nov 2015 10:09:44 +0100 Subject: [PATCH 13/36] Fix minor issue --- js/stores/whitelabel_store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/stores/whitelabel_store.js b/js/stores/whitelabel_store.js index 477e061c..8385efc8 100644 --- a/js/stores/whitelabel_store.js +++ b/js/stores/whitelabel_store.js @@ -25,7 +25,7 @@ class WhitelabelStore { } } - onSuccessFetchWhitelabel(whitelabel) { + onSuccessFetchWhitelabel({ whitelabel }) { this.whitelabelMeta.invalidateCache = false; this.whitelabel = whitelabel; } From c17685731a424d36412674bbf00d7a4b03574740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Wed, 4 Nov 2015 11:36:42 +0100 Subject: [PATCH 14/36] Fix invalidateCache functionality --- js/sources/user_source.js | 2 +- js/sources/whitelabel_source.js | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 js/sources/whitelabel_source.js diff --git a/js/sources/user_source.js b/js/sources/user_source.js index 05165391..5940169b 100644 --- a/js/sources/user_source.js +++ b/js/sources/user_source.js @@ -18,7 +18,7 @@ const UserSource = { success: UserActions.successFetchCurrentUser, error: UserActions.currentUserFailed, shouldFetch(state) { - return state.invalidateCache || state.currentUser && !state.currentUser.email; + return state.userMeta.invalidateCache || state.currentUser && !state.currentUser.email; } }, diff --git a/js/sources/whitelabel_source.js b/js/sources/whitelabel_source.js new file mode 100644 index 00000000..eda0ba19 --- /dev/null +++ b/js/sources/whitelabel_source.js @@ -0,0 +1,25 @@ +'use strict'; + +import requests from '../utils/requests'; +import WhitelabelActions from '../actions/whitelabel_actions'; + +import { getSubdomain } from '../utils/general_utils'; + + +const WhitelabelSource = { + lookupWhitelabel: { + remote() { + return requests.get('whitelabel_settings', {'subdomain': getSubdomain()}); + }, + local(state) { + return Object.keys(state.whitelabel).length > 0 ? state : {}; + }, + success: WhitelabelActions.successFetchWhitelabel, + error: WhitelabelActions.whitelabelFailed, + shouldFetch(state) { + return state.whitelabelMeta.invalidateCache || Object.keys(state.whitelabel).length === 0; + } + } +}; + +export default WhitelabelSource; \ No newline at end of file From 7c73b7fac78a60fb3c3518114699835d8993f1ec Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 9 Nov 2015 14:32:14 +0100 Subject: [PATCH 15/36] Refactor InjectInHeadMixin to be a util class instead --- js/components/ascribe_media/media_player.js | 29 ++++----- js/mixins/inject_in_head_mixin.js | 71 -------------------- js/utils/inject_utils.js | 72 +++++++++++++++++++++ 3 files changed, 84 insertions(+), 88 deletions(-) delete mode 100644 js/mixins/inject_in_head_mixin.js create mode 100644 js/utils/inject_utils.js diff --git a/js/components/ascribe_media/media_player.js b/js/components/ascribe_media/media_player.js index a7e56fb7..23e84945 100644 --- a/js/components/ascribe_media/media_player.js +++ b/js/components/ascribe_media/media_player.js @@ -3,12 +3,13 @@ import React from 'react'; import Q from 'q'; -import { escapeHTML } from '../../utils/general_utils'; - -import InjectInHeadMixin from '../../mixins/inject_in_head_mixin'; import Panel from 'react-bootstrap/lib/Panel'; import ProgressBar from 'react-bootstrap/lib/ProgressBar'; -import AppConstants from '../../constants/application_constants.js'; + +import AppConstants from '../../constants/application_constants'; + +import { escapeHTML } from '../../utils/general_utils'; +import { InjectInHeadUtils } from '../../utils/inject_utils'; /** * This is the component that implements display-specific functionality. @@ -54,14 +55,12 @@ let Image = React.createClass({ preview: React.PropTypes.string.isRequired }, - mixins: [InjectInHeadMixin], - componentDidMount() { - this.inject('https://code.jquery.com/jquery-2.1.4.min.js') + InjectInHeadUtils.inject('https://code.jquery.com/jquery-2.1.4.min.js') .then(() => Q.all([ - this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/shmui.css'), - this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/jquery.shmui.js') + InjectInHeadUtils.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/shmui.css'), + InjectInHeadUtils.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/jquery.shmui.js') ]).then(() => { window.jQuery('.shmui-ascribe').shmui(); })); }, @@ -77,10 +76,8 @@ let Audio = React.createClass({ url: React.PropTypes.string.isRequired }, - mixins: [InjectInHeadMixin], - componentDidMount() { - this.inject(AppConstants.baseUrl + 'static/thirdparty/audiojs/audiojs/audio.min.js').then(this.ready); + InjectInHeadUtils.inject(AppConstants.baseUrl + 'static/thirdparty/audiojs/audiojs/audio.min.js').then(this.ready); }, ready() { @@ -111,7 +108,7 @@ let Video = React.createClass({ * `false` if we failed to load the external library) * 2) render the cover using the `` component (because libraryLoaded is null) * 3) on `componentDidMount`, we load the external `css` and `js` resources using - * the `InjectInHeadMixin`, attaching a function to `Promise.then` to change + * the `InjectInHeadUtils`, attaching a function to `Promise.then` to change * `state.libraryLoaded` to true * 4) when the promise is succesfully resolved, we change `state.libraryLoaded` triggering * a re-render @@ -129,16 +126,14 @@ let Video = React.createClass({ encodingStatus: React.PropTypes.number }, - mixins: [InjectInHeadMixin], - getInitialState() { return { libraryLoaded: null, videoMounted: false }; }, componentDidMount() { Q.all([ - this.inject('//vjs.zencdn.net/4.12/video-js.css'), - this.inject('//vjs.zencdn.net/4.12/video.js')]) + InjectInHeadUtils.inject('//vjs.zencdn.net/4.12/video-js.css'), + InjectInHeadUtils.inject('//vjs.zencdn.net/4.12/video.js')]) .then(() => this.setState({libraryLoaded: true})) .fail(() => this.setState({libraryLoaded: false})); }, diff --git a/js/mixins/inject_in_head_mixin.js b/js/mixins/inject_in_head_mixin.js deleted file mode 100644 index 6eacacad..00000000 --- a/js/mixins/inject_in_head_mixin.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict'; - -import Q from 'q'; - -let mapAttr = { - link: 'href', - script: 'src' -}; - -let mapTag = { - js: 'script', - css: 'link' -}; - - -let InjectInHeadMixin = { - /** - * Provide functions to inject `