Permalink
Comparing changes
Open a pull request
- 11 commits
- 14 files changed
- 0 commit comments
- 2 contributors
Unified
Split
Showing
with
229 additions
and 35 deletions.
- +58 −0 CHANGES.txt
- +4 −3 h/static/scripts/app-controller.coffee
- +9 −3 h/static/scripts/directive/group-list.js
- +25 −12 h/static/scripts/directive/test/group-list-test.js
- +4 −1 h/static/scripts/events.js
- +1 −1 h/static/scripts/groups.js
- +16 −2 h/static/scripts/session.js
- +58 −1 h/static/scripts/store.js
- +8 −2 h/static/scripts/test/app-controller-test.coffee
- +1 −1 h/static/scripts/test/groups-test.js
- +30 −4 h/static/scripts/test/session-test.js
- +10 −1 h/static/scripts/test/store-test.js
- +4 −3 h/static/styles/help-page.scss
- +1 −1 h/templates/client/group_list.html
| @@ -1,3 +1,61 @@ | ||
| 0.7.19 (2015-10-28) | ||
| ================== | ||
| Bug fixes | ||
| --------- | ||
| - Fix a problem where an incorrect search query was sent to our server due to | ||
| semicolons in the page URL (6513184). | ||
| 0.7.9 (2015-10-28) | ||
| ================== | ||
| Bug fixes | ||
| --------- | ||
| - Fix a problem where activating the Chrome extension would obliterate a version | ||
| of Hypothesis embedded on the page (#2657). | ||
| - Fix a visual issue causing the "Clear selection" and "Clear search" buttons to | ||
| be briefly visible when they shouldn't have been (#2668). | ||
| - Fix a crash triggered when the set of connected WebSocket clients changed | ||
| while handling a message (#2647). | ||
| - Fix a bug where cancelling leaving a group nonetheless resulted in group focus | ||
| changing (#2669). | ||
| Features | ||
| -------- | ||
| - Improved appearance and behaviour of the sort control for annotations (feature | ||
| flagged: groups) (#2643). | ||
| - Replies now inherit the publication scope of their parents. That is: replies | ||
| to group annotations will go to the same group (#2650). | ||
| - Support HTTP conditional responses (ETag/If-None-Match and | ||
| Last-Modified/If-Modified-Since) under appropriate conditions (#2664). | ||
| - Groups landing pages now show a list of recently annotated pages (feature | ||
| flagged: groups) (#2667). | ||
| Miscellanea | ||
| ----------- | ||
| - Upgrade to Angular 1.4.7 (#2629). | ||
| - Account settings and profile forms are now rendered by the server (#2636). | ||
| - The Chrome extension can now be built in a way that allows distinguishing | ||
| between development versions of the extension and production ones (#2639). | ||
| - No longer perform the URI expansion step when searching for annotations on | ||
| URLs which have been marked "canonical". This hopefully reduces the number of | ||
| false-positive annotations we load on pages with appropriate metadata (#2652). | ||
| - Replace group public IDs (hashids) with randomly generated IDs (#2662). | ||
| 0.7.8 (2015-10-20) | ||
| ================== | ||
| @@ -48,10 +48,11 @@ module.exports = class AppController | ||
| # Reload the view when the focused group changes or the | ||
| # list of groups that the user is a member of changes | ||
| reloadEvents = [events.SESSION_CHANGED, events.GROUP_FOCUSED]; | ||
| reloadEvents = [events.USER_CHANGED, events.GROUP_FOCUSED]; | ||
| reloadEvents.forEach((eventName) -> | ||
| $scope.$on(eventName, (event) -> | ||
| $route.reload() | ||
| $scope.$on(eventName, (event, data) -> | ||
| if !data || !data.initialLoad | ||
| $route.reload() | ||
| ) | ||
| ); | ||
| @@ -1,7 +1,9 @@ | ||
| 'use strict'; | ||
| var events = require('../events'); | ||
| // @ngInject | ||
| function GroupListController($scope, $window) { | ||
| function GroupListController($scope, $window, groups) { | ||
| $scope.expandedGroupId = undefined; | ||
| // show the share link for the specified group or clear it if | ||
| @@ -19,13 +21,17 @@ function GroupListController($scope, $window) { | ||
| }; | ||
| $scope.leaveGroup = function (groupId) { | ||
| var groupName = $scope.groups.get(groupId).name; | ||
| var groupName = groups.get(groupId).name; | ||
| var message = 'Are you sure you want to leave the group "' + | ||
| groupName + '"?'; | ||
| if ($window.confirm(message)) { | ||
| $scope.groups.leave(groupId); | ||
| groups.leave(groupId); | ||
| } | ||
| } | ||
| $scope.focusGroup = function (groupId) { | ||
| groups.focus(groupId); | ||
| } | ||
| } | ||
| /** | ||
| @@ -1,5 +1,6 @@ | ||
| 'use strict'; | ||
| var events = require('../../events'); | ||
| var groupList = require('../group-list'); | ||
| var util = require('./util'); | ||
| @@ -8,8 +9,16 @@ describe('GroupListController', function () { | ||
| var $scope; | ||
| beforeEach(function () { | ||
| $scope = {}; | ||
| controller = new groupList.Controller($scope); | ||
| $scope = { | ||
| $on: sinon.stub(), | ||
| $apply: sinon.stub(), | ||
| }; | ||
| var fakeWindow = {}; | ||
| var fakeGroups = { | ||
| all: sinon.stub(), | ||
| focused: sinon.stub(), | ||
| }; | ||
| controller = new groupList.Controller($scope, fakeWindow, fakeGroups); | ||
| }); | ||
| it('toggles share links', function () { | ||
| @@ -44,19 +53,12 @@ function isElementHidden(element) { | ||
| } | ||
| describe('groupList', function () { | ||
| var $rootScope; | ||
| var $window; | ||
| var GROUP_LINK = 'https://hypothes.is/groups/hdevs'; | ||
| var groups = [{ | ||
| id: 'public', | ||
| public: true | ||
| },{ | ||
| id: 'h-devs', | ||
| name: 'Hypothesis Developers', | ||
| url: GROUP_LINK | ||
| }]; | ||
| var groups; | ||
| var fakeGroups; | ||
| before(function() { | ||
| @@ -72,9 +74,19 @@ describe('groupList', function () { | ||
| angular.mock.module('h.templates'); | ||
| }); | ||
| beforeEach(angular.mock.inject(function (_$window_) { | ||
| beforeEach(angular.mock.inject(function (_$rootScope_, _$window_) { | ||
| $rootScope = _$rootScope_; | ||
| $window = _$window_; | ||
| groups = [{ | ||
| id: 'public', | ||
| public: true | ||
| },{ | ||
| id: 'h-devs', | ||
| name: 'Hypothesis Developers', | ||
| url: GROUP_LINK | ||
| }]; | ||
| fakeGroups = { | ||
| all: function () { | ||
| return groups; | ||
| @@ -87,6 +99,7 @@ describe('groupList', function () { | ||
| }, | ||
| leave: sinon.stub(), | ||
| focus: sinon.stub(), | ||
| focused: sinon.stub(), | ||
| }; | ||
| })); | ||
| @@ -2,10 +2,13 @@ | ||
| * This module defines the set of global events that are dispatched | ||
| * on $rootScope | ||
| */ | ||
| module.exports = { | ||
| /** Broadcast when the currently selected group changes */ | ||
| GROUP_FOCUSED: 'groupFocused', | ||
| /** Broadcast when the list of groups changes */ | ||
| GROUPS_CHANGED: 'groupsChanged', | ||
| /** Broadcast when the signed-in user changes */ | ||
| USER_CHANGED: 'userChanged', | ||
| /** Broadcast when the session state is updated. | ||
| * This event is NOT broadcast after the initial session load. | ||
| */ | ||
| @@ -84,7 +84,7 @@ function groups(localStorage, session, $rootScope, features, $http) { | ||
| } | ||
| // reset the focused group if the user leaves it | ||
| $rootScope.$on(events.SESSION_CHANGED, function () { | ||
| $rootScope.$on(events.GROUPS_CHANGED, function () { | ||
| if (focusedGroup) { | ||
| focusedGroup = get(focusedGroup.id); | ||
| if (!focusedGroup) { | ||
| @@ -116,6 +116,9 @@ function session($document, $http, $resource, $rootScope, flash) { | ||
| resource.update = function (model) { | ||
| var isInitialLoad = !resource.state.csrf; | ||
| var userChanged = model.userid !== resource.state.userid; | ||
| var groupsChanged = !angular.equals(model.groups, resource.state.groups); | ||
| // Copy the model data (including the CSRF token) into `resource.state`. | ||
| angular.copy(model, resource.state); | ||
| @@ -128,8 +131,19 @@ function session($document, $http, $resource, $rootScope, flash) { | ||
| lastLoad = {$promise: Promise.resolve(model), $resolved: true}; | ||
| lastLoadTime = Date.now(); | ||
| if (!isInitialLoad) { | ||
| $rootScope.$broadcast(events.SESSION_CHANGED); | ||
| $rootScope.$broadcast(events.SESSION_CHANGED, { | ||
| initialLoad: isInitialLoad, | ||
| }); | ||
| if (userChanged) { | ||
| $rootScope.$broadcast(events.USER_CHANGED, { | ||
| initialLoad: isInitialLoad, | ||
| }); | ||
| } | ||
| if (groupsChanged) { | ||
| $rootScope.$broadcast(events.GROUPS_CHANGED, { | ||
| initialLoad: isInitialLoad, | ||
| }); | ||
| } | ||
| // Return the model | ||
| @@ -24,6 +24,56 @@ function stripInternalProperties(obj) { | ||
| return result; | ||
| } | ||
| function forEachSorted(obj, iterator, context) { | ||
| var keys = Object.keys(obj).sort(); | ||
| for (var i = 0; i < keys.length; i++) { | ||
| iterator.call(context, obj[keys[i]], keys[i]); | ||
| } | ||
| return keys; | ||
| } | ||
| function serializeValue(v) { | ||
| if (angular.isObject(v)) { | ||
| return angular.isDate(v) ? v.toISOString() : angular.toJson(v); | ||
| } | ||
| return v; | ||
| } | ||
| function encodeUriQuery(val) { | ||
| return encodeURIComponent(val).replace(/%20/g, '+'); | ||
| } | ||
| // Serialize an object containing parameters into a form suitable for a query | ||
| // string. | ||
| // | ||
| // This is an almost identical copy of the default Angular parameter serializer | ||
| // ($httpParamSerializer), with one important change. In Angular 1.4.x | ||
| // semicolons are not encoded in query parameter values. This is a problem for | ||
| // us as URIs around the web may well contain semicolons, which our backend will | ||
| // then proceed to parse as a delimiter in the query string. To avoid this | ||
| // problem we use a very conservative encoder, found above. | ||
| function serializeParams(params) { | ||
| if (!params) return ''; | ||
| var parts = []; | ||
| forEachSorted(params, function(value, key) { | ||
| if (value === null || typeof value === 'undefined') return; | ||
| if (angular.isArray(value)) { | ||
| angular.forEach(value, function(v, k) { | ||
| parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v))); | ||
| }); | ||
| } else { | ||
| parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(value))); | ||
| } | ||
| }); | ||
| return parts.join('&'); | ||
| } | ||
| /** | ||
| * @ngdoc factory | ||
| * @name store | ||
| @@ -41,6 +91,7 @@ function stripInternalProperties(obj) { | ||
| function store($http, $resource, settings) { | ||
| var instance = {}; | ||
| var defaultOptions = { | ||
| paramSerializer: serializeParams, | ||
| transformRequest: prependTransform( | ||
| $http.defaults.transformRequest, | ||
| stripInternalProperties | ||
| @@ -54,9 +105,15 @@ function store($http, $resource, settings) { | ||
| .then(function (response) { | ||
| var links = response.data.links; | ||
| instance.SearchResource = $resource(links.search.url, {}, defaultOptions); | ||
| // N.B. in both cases below we explicitly override the default `get` | ||
| // action because there is no way to provide defaultOptions to the default | ||
| // action. | ||
| instance.SearchResource = $resource(links.search.url, {}, { | ||
| get: angular.extend({url: links.search.url}, defaultOptions), | ||
| }); | ||
| instance.AnnotationResource = $resource(links.annotation.read.url, {}, { | ||
| get: angular.extend(links.annotation.read, defaultOptions), | ||
| create: angular.extend(links.annotation.create, defaultOptions), | ||
| update: angular.extend(links.annotation.update, defaultOptions), | ||
| delete: angular.extend(links.annotation.delete, defaultOptions), | ||
| @@ -151,8 +151,14 @@ describe 'AppController', -> | ||
| $scope.$broadcast(events.GROUP_FOCUSED) | ||
| assert.calledOnce(fakeRoute.reload) | ||
| it 'reloads the view when the session state changes', -> | ||
| it 'does not reload the view when the logged-in user changes on first load', -> | ||
| createController() | ||
| fakeRoute.reload = sinon.spy() | ||
| $scope.$broadcast(events.SESSION_CHANGED) | ||
| $scope.$broadcast(events.USER_CHANGED, {initialLoad: true}) | ||
| assert.notCalled(fakeRoute.reload) | ||
| it 'reloads the view when the logged-in user changes after first load', -> | ||
| createController() | ||
| fakeRoute.reload = sinon.spy() | ||
| $scope.$broadcast(events.USER_CHANGED, {initialLoad: false}) | ||
| assert.calledOnce(fakeRoute.reload) | ||
| @@ -40,7 +40,7 @@ describe('groups', function() { | ||
| $broadcast: sandbox.stub(), | ||
| $on: function(event, callback) { | ||
| if (event === events.SESSION_CHANGED) { | ||
| if (event === events.GROUPS_CHANGED) { | ||
| this.eventCallbacks.push(callback); | ||
| } | ||
| } | ||
Oops, something went wrong.