Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
b104d018
Unverified
Commit
b104d018
authored
Aug 07, 2019
by
Kyle Keating
Committed by
GitHub
Aug 07, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1282 from hypothesis/focused-user-mode
Add focused user mode for speed grader
parents
59dbd146
56bfa23b
Changes
17
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
383 additions
and
8 deletions
+383
-8
live-reload-server.js
scripts/gulp/live-reload-server.js
+10
-0
index.js
src/annotator/config/index.js
+1
-0
focused-mode-header.js
src/sidebar/components/focused-mode-header.js
+45
-0
sidebar-content.js
src/sidebar/components/sidebar-content.js
+8
-1
focused-mode-header-test.js
src/sidebar/components/test/focused-mode-header-test.js
+60
-0
sidebar-content-test.js
src/sidebar/components/test/sidebar-content-test.js
+11
-0
host-config.js
src/sidebar/host-config.js
+3
-0
index.js
src/sidebar/index.js
+4
-0
root-thread.js
src/sidebar/services/root-thread.js
+18
-4
search-filter.js
src/sidebar/services/search-filter.js
+5
-3
root-thread-test.js
src/sidebar/services/test/root-thread-test.js
+26
-0
search-filter-test.js
src/sidebar/services/test/search-filter-test.js
+18
-0
selection.js
src/sidebar/store/modules/selection.js
+80
-0
selection-test.js
src/sidebar/store/modules/test/selection-test.js
+78
-0
sidebar-content.html
src/sidebar/templates/sidebar-content.html
+4
-0
focused-mode-header.scss
src/styles/sidebar/components/focused-mode-header.scss
+11
-0
sidebar.scss
src/styles/sidebar/sidebar.scss
+1
-0
No files found.
scripts/gulp/live-reload-server.js
View file @
b104d018
...
...
@@ -97,6 +97,16 @@ function LiveReloadServer(port, config) {
window.hypothesisConfig = function () {
return {
liveReloadServer: 'ws://' + appHost + ':
${
port
}
',
// Force into focused user mode
// Example focused user mode
// focus: {
// user: {
// username: 'foo',
// authority: 'lms',
// displayName: 'Foo Bar',
// }
// },
// Open the sidebar when the page loads
openSidebar: true,
...
...
src/annotator/config/index.js
View file @
b104d018
...
...
@@ -24,6 +24,7 @@ function configFrom(window_) {
'enableExperimentalNewNoteButton'
),
group
:
settings
.
group
,
focus
:
settings
.
hostPageSetting
(
'focus'
),
theme
:
settings
.
hostPageSetting
(
'theme'
),
usernameUrl
:
settings
.
hostPageSetting
(
'usernameUrl'
),
onLayoutChange
:
settings
.
hostPageSetting
(
'onLayoutChange'
),
...
...
src/sidebar/components/focused-mode-header.js
0 → 100644
View file @
b104d018
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
useStore
=
require
(
'../store/use-store'
);
function
FocusedModeHeader
()
{
const
store
=
useStore
(
store
=>
({
actions
:
{
setFocusModeFocused
:
store
.
setFocusModeFocused
,
},
selectors
:
{
focusModeFocused
:
store
.
focusModeFocused
,
focusModeUserPrettyName
:
store
.
focusModeUserPrettyName
,
},
}));
const
toggleFocusedMode
=
()
=>
{
store
.
actions
.
setFocusModeFocused
(
!
store
.
selectors
.
focusModeFocused
());
};
const
buttonText
=
()
=>
{
if
(
store
.
selectors
.
focusModeFocused
())
{
return
`Annotations by
${
store
.
selectors
.
focusModeUserPrettyName
()}
`
;
}
else
{
return
'All annotations'
;
}
};
return
(
<
div
className
=
"focused-mode-header"
>
<
button
onClick
=
{
toggleFocusedMode
}
className
=
"primary-action-btn primary-action-btn--short"
title
=
{
`Toggle to show annotations only by
${
store
.
selectors
.
focusModeUserPrettyName
()}
`
}
>
{
buttonText
()}
<
/button
>
<
/div
>
);
}
FocusedModeHeader
.
propTypes
=
{};
module
.
exports
=
FocusedModeHeader
;
src/sidebar/components/sidebar-content.js
View file @
b104d018
...
...
@@ -125,6 +125,10 @@ function SidebarContentController(
true
);
this
.
showFocusedHeader
=
()
=>
{
return
store
.
focusModeEnabled
();
};
this
.
showSelectedTabs
=
function
()
{
if
(
this
.
selectedAnnotationUnavailable
()
||
...
...
@@ -132,8 +136,11 @@ function SidebarContentController(
store
.
getState
().
filterQuery
)
{
return
false
;
}
}
else
if
(
store
.
focusModeFocused
())
{
return
false
;
}
else
{
return
true
;
}
};
this
.
setCollapsed
=
function
(
id
,
collapsed
)
{
...
...
src/sidebar/components/test/focused-mode-header-test.js
0 → 100644
View file @
b104d018
'use strict'
;
const
{
shallow
}
=
require
(
'enzyme'
);
const
{
createElement
}
=
require
(
'preact'
);
const
FocusedModeHeader
=
require
(
'../focused-mode-header'
);
describe
(
'FocusedModeHeader'
,
function
()
{
let
fakeStore
;
function
createComponent
()
{
return
shallow
(
<
FocusedModeHeader
/>
);
}
beforeEach
(
function
()
{
fakeStore
=
{
selection
:
{
focusMode
:
{
enabled
:
true
,
focused
:
true
,
},
},
focusModeFocused
:
sinon
.
stub
().
returns
(
false
),
focusModeUserPrettyName
:
sinon
.
stub
().
returns
(
'Fake User'
),
setFocusModeFocused
:
sinon
.
stub
(),
};
FocusedModeHeader
.
$imports
.
$mock
({
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
});
});
afterEach
(()
=>
{
FocusedModeHeader
.
$imports
.
$restore
();
});
it
(
'creates the component'
,
()
=>
{
const
wrapper
=
createComponent
();
assert
.
include
(
wrapper
.
text
(),
'All annotations'
);
});
it
(
"sets the button's text to the user's name when focused"
,
()
=>
{
fakeStore
.
focusModeFocused
=
sinon
.
stub
().
returns
(
true
);
const
wrapper
=
createComponent
();
assert
.
include
(
wrapper
.
text
(),
'Annotations by Fake User'
);
});
describe
(
'clicking the button shall toggle the focused mode'
,
function
()
{
it
(
'when focused is false, toggle to true'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'button'
).
simulate
(
'click'
);
assert
.
calledWith
(
fakeStore
.
setFocusModeFocused
,
true
);
});
it
(
'when focused is true, toggle to false'
,
()
=>
{
fakeStore
.
focusModeFocused
=
sinon
.
stub
().
returns
(
true
);
const
wrapper
=
createComponent
();
wrapper
.
find
(
'button'
).
simulate
(
'click'
);
assert
.
calledWith
(
fakeStore
.
setFocusModeFocused
,
false
);
});
});
});
src/sidebar/components/test/sidebar-content-test.js
View file @
b104d018
...
...
@@ -158,6 +158,17 @@ describe('sidebar.components.sidebar-content', function() {
});
});
describe
(
'showFocusedHeader'
,
()
=>
{
it
(
'returns true if focus mode is enabled'
,
()
=>
{
store
.
focusModeEnabled
=
sinon
.
stub
().
returns
(
true
);
assert
.
isTrue
(
ctrl
.
showFocusedHeader
());
});
it
(
'returns false if focus mode is not enabled'
,
()
=>
{
store
.
focusModeEnabled
=
sinon
.
stub
().
returns
(
false
);
assert
.
isFalse
(
ctrl
.
showFocusedHeader
());
});
});
function
connectFrameAndPerformInitialFetch
()
{
setFrames
([{
uri
:
'https://a-page.com'
}]);
$scope
.
$digest
();
...
...
src/sidebar/host-config.js
View file @
b104d018
...
...
@@ -38,6 +38,9 @@ function hostPageConfig(window) {
// This should be removed once new note button is enabled for everybody.
'enableExperimentalNewNoteButton'
,
// Forces the sidebar to filter annotations to a single user.
'focus'
,
// Fetch config from a parent frame.
'requestConfigFromFrame'
,
...
...
src/sidebar/index.js
View file @
b104d018
...
...
@@ -180,6 +180,10 @@ function startAngularApp(config) {
'searchStatusBar'
,
wrapReactComponent
(
require
(
'./components/search-status-bar'
))
)
.
component
(
'focusedModeHeader'
,
wrapReactComponent
(
require
(
'./components/focused-mode-header'
))
)
.
component
(
'selectionTabs'
,
wrapReactComponent
(
require
(
'./components/selection-tabs'
))
...
...
src/sidebar/services/root-thread.js
View file @
b104d018
...
...
@@ -48,17 +48,31 @@ function RootThread($rootScope, store, searchFilter, viewFilter) {
*/
function
buildRootThread
(
state
)
{
const
sortFn
=
sortFns
[
state
.
sortKey
];
const
shouldFilterThread
=
()
=>
{
// is there a query or focused truthy value from the config?
return
state
.
filterQuery
||
store
.
focusModeFocused
();
};
let
filterFn
;
if
(
state
.
filterQuery
)
{
const
filters
=
searchFilter
.
generateFacetedFilter
(
state
.
filterQuery
);
if
(
shouldFilterThread
())
{
const
userFilter
=
{};
// optional user filter object for focused mode
// look for a unique username, if present, add it to the user filter
const
focusedUsername
=
store
.
focusModeUsername
();
// may be null if no focused user
if
(
focusedUsername
)
{
// focused user found, add it to the filter object
userFilter
.
user
=
focusedUsername
;
}
const
filters
=
searchFilter
.
generateFacetedFilter
(
state
.
filterQuery
,
userFilter
);
filterFn
=
function
(
annot
)
{
return
viewFilter
.
filter
([
annot
],
filters
).
length
>
0
;
};
}
let
threadFilterFn
;
if
(
state
.
isSidebar
&&
!
s
tate
.
filterQuery
)
{
if
(
state
.
isSidebar
&&
!
s
houldFilterThread
()
)
{
threadFilterFn
=
function
(
thread
)
{
if
(
!
thread
.
annotation
)
{
return
false
;
...
...
src/sidebar/services/search-filter.js
View file @
b104d018
...
...
@@ -127,9 +127,12 @@ function toObject(searchtext) {
* facet.
*
* @param {string} searchtext
* @param {object} focusFilters - Map of the filter objects keyed to array values.
* Currently, only the `user` filter key is supported.
*
* @return {Object}
*/
function
generateFacetedFilter
(
searchtext
)
{
function
generateFacetedFilter
(
searchtext
,
focusFilters
=
{}
)
{
let
terms
;
const
any
=
[];
const
quote
=
[];
...
...
@@ -138,8 +141,7 @@ function generateFacetedFilter(searchtext) {
const
tag
=
[];
const
text
=
[];
const
uri
=
[];
const
user
=
[];
const
user
=
focusFilters
.
user
?
[
focusFilters
.
user
]
:
[];
if
(
searchtext
)
{
terms
=
tokenize
(
searchtext
);
for
(
const
term
of
terms
)
{
...
...
src/sidebar/services/test/root-thread-test.js
View file @
b104d018
...
...
@@ -28,6 +28,7 @@ describe('rootThread', function() {
let
fakeStore
;
let
fakeBuildThread
;
let
fakeSearchFilter
;
let
fakeSettings
;
let
fakeViewFilter
;
let
$rootScope
;
...
...
@@ -65,6 +66,8 @@ describe('rootThread', function() {
getDraftIfNotEmpty
:
sinon
.
stub
().
returns
(
null
),
removeDraft
:
sinon
.
stub
(),
createAnnotation
:
sinon
.
stub
(),
focusModeFocused
:
sinon
.
stub
().
returns
(
false
),
focusModeUsername
:
sinon
.
stub
().
returns
({}),
};
fakeBuildThread
=
sinon
.
stub
().
returns
(
fixtures
.
emptyThread
);
...
...
@@ -73,6 +76,8 @@ describe('rootThread', function() {
generateFacetedFilter
:
sinon
.
stub
(),
};
fakeSettings
=
{};
fakeViewFilter
=
{
filter
:
sinon
.
stub
(),
};
...
...
@@ -81,6 +86,7 @@ describe('rootThread', function() {
.
module
(
'app'
,
[])
.
value
(
'store'
,
fakeStore
)
.
value
(
'searchFilter'
,
fakeSearchFilter
)
.
value
(
'settings'
,
fakeSettings
)
.
value
(
'viewFilter'
,
fakeViewFilter
)
.
service
(
'rootThread'
,
rootThreadFactory
);
...
...
@@ -340,6 +346,26 @@ describe('rootThread', function() {
});
});
describe
(
'when the focus user is present'
,
()
=>
{
it
(
"generates a thread filter focused on the user's annotations"
,
()
=>
{
fakeBuildThread
.
reset
();
const
filters
=
[{
user
:
{
terms
:
[
'acct:bill@localhost'
]
}
}];
const
annotation
=
annotationFixtures
.
defaultAnnotation
();
fakeSearchFilter
.
generateFacetedFilter
.
returns
(
filters
);
fakeStore
.
focusModeFocused
=
sinon
.
stub
().
returns
(
true
);
rootThread
.
thread
(
fakeStore
.
state
);
const
filterFn
=
fakeBuildThread
.
args
[
0
][
1
].
filterFn
;
fakeViewFilter
.
filter
.
returns
([
annotation
]);
assert
.
isTrue
(
filterFn
(
annotation
));
assert
.
calledWith
(
fakeViewFilter
.
filter
,
sinon
.
match
([
annotation
]),
filters
);
});
});
context
(
'when annotation events occur'
,
function
()
{
const
annot
=
annotationFixtures
.
defaultAnnotation
();
...
...
src/sidebar/services/test/search-filter-test.js
View file @
b104d018
...
...
@@ -185,5 +185,23 @@ describe('sidebar.search-filter', () => {
}
});
});
it
(
'filters to a focused user'
,
()
=>
{
const
filter
=
searchFilter
.
generateFacetedFilter
(
null
,
{
user
:
'fakeusername'
,
});
// Remove empty facets.
Object
.
keys
(
filter
).
forEach
(
k
=>
{
if
(
filter
[
k
].
terms
.
length
===
0
)
{
delete
filter
[
k
];
}
});
assert
.
deepEqual
(
filter
,
{
user
:
{
operator
:
'or'
,
terms
:
[
'fakeusername'
],
},
});
});
});
});
src/sidebar/store/modules/selection.js
View file @
b104d018
...
...
@@ -113,6 +113,13 @@ function init(settings) {
selectedTab
:
TAB_DEFAULT
,
focusMode
:
{
enabled
:
settings
.
hasOwnProperty
(
'focus'
),
// readonly
focused
:
true
,
// Copy over the focus confg from settings object
config
:
{
...(
settings
.
focus
?
settings
.
focus
:
{})
},
},
// Key by which annotations are currently sorted.
sortKey
:
TAB_SORTKEY_DEFAULT
[
TAB_DEFAULT
],
// Keys by which annotations can be sorted.
...
...
@@ -146,6 +153,15 @@ const update = {
return
{
focusedAnnotationMap
:
action
.
focused
};
},
SET_FOCUS_MODE_FOCUSED
:
function
(
state
,
action
)
{
return
{
focusMode
:
{
...
state
.
focusMode
,
focused
:
action
.
focused
,
},
};
},
SET_FORCE_VISIBLE
:
function
(
state
,
action
)
{
return
{
forceVisible
:
action
.
forceVisible
};
},
...
...
@@ -307,6 +323,16 @@ function setFilterQuery(query) {
};
}
/**
* Set the focused to only show annotations by the focused user.
*/
function
setFocusModeFocused
(
focused
)
{
return
{
type
:
actions
.
SET_FOCUS_MODE_FOCUSED
,
focused
,
};
}
/** Sets the sort key for the annotation list. */
function
setSortKey
(
key
)
{
return
{
...
...
@@ -353,6 +379,55 @@ const getFirstSelectedAnnotationId = createSelector(
function
filterQuery
(
state
)
{
return
state
.
filterQuery
;
}
/**
* Returns the on/off state of the focus mode. This can be toggled on or off to
* filter to the focused user.
*
* @return {boolean}
*/
function
focusModeFocused
(
state
)
{
return
state
.
focusMode
.
enabled
&&
state
.
focusMode
.
focused
;
}
/**
* Returns the value of the focus mode from the config.
*
* @return {boolean}
*/
function
focusModeEnabled
(
state
)
{
return
state
.
focusMode
.
enabled
;
}
/**
* Returns the username of the focused mode or null if none is found.
*
* @return {object}
*/
function
focusModeUsername
(
state
)
{
if
(
state
.
focusMode
.
config
.
user
&&
state
.
focusMode
.
config
.
user
.
username
)
{
return
state
.
focusMode
.
config
.
user
.
username
;
}
return
null
;
}
/**
* Returns the display name for a user or the username
* if display name is not present. If both are missing
* then this returns an empty string.
*
* @return {string}
*/
function
focusModeUserPrettyName
(
state
)
{
const
user
=
state
.
focusMode
.
config
.
user
;
if
(
!
user
)
{
return
''
;
}
else
if
(
user
.
displayName
)
{
return
user
.
displayName
;
}
else
if
(
user
.
username
)
{
return
user
.
username
;
}
else
{
return
''
;
}
}
module
.
exports
=
{
init
:
init
,
...
...
@@ -367,6 +442,7 @@ module.exports = {
selectTab
:
selectTab
,
setCollapsed
:
setCollapsed
,
setFilterQuery
:
setFilterQuery
,
setFocusModeFocused
:
setFocusModeFocused
,
setForceVisible
:
setForceVisible
,
setSortKey
:
setSortKey
,
toggleSelectedAnnotations
:
toggleSelectedAnnotations
,
...
...
@@ -375,6 +451,10 @@ module.exports = {
selectors
:
{
hasSelectedAnnotations
,
filterQuery
,
focusModeFocused
,
focusModeEnabled
,
focusModeUsername
,
focusModeUserPrettyName
,
isAnnotationSelected
,
getFirstSelectedAnnotationId
,
},
...
...
src/sidebar/store/modules/test/selection-test.js
View file @
b104d018
...
...
@@ -186,6 +186,84 @@ describe('store/modules/selection', () => {
});
});
describe
(
'setFocusModeFocused()'
,
function
()
{
it
(
'sets the focus mode to enabled'
,
function
()
{
store
.
setFocusModeFocused
(
true
);
assert
.
equal
(
store
.
getState
().
focusMode
.
focused
,
true
);
});
it
(
'sets the focus mode to not enabled'
,
function
()
{
store
=
createStore
([
selection
],
[{
focus
:
{
user
:
{}
}
}]);
store
.
setFocusModeFocused
(
false
);
assert
.
equal
(
store
.
getState
().
focusMode
.
focused
,
false
);
});
});
describe
(
'focusModeEnabled()'
,
function
()
{
it
(
'should be true when the focus setting is present'
,
function
()
{
store
=
createStore
([
selection
],
[{
focus
:
{
user
:
{}
}
}]);
assert
.
equal
(
store
.
focusModeEnabled
(),
true
);
});
it
(
'should be false when the focus setting is not present'
,
function
()
{
assert
.
equal
(
store
.
focusModeEnabled
(),
false
);
});
});
describe
(
'focusModeFocused()'
,
function
()
{
it
(
'should return true by default when focus mode is enabled'
,
function
()
{
store
=
createStore
([
selection
],
[{
focus
:
{
user
:
{}
}
}]);
assert
.
equal
(
store
.
getState
().
focusMode
.
enabled
,
true
);
assert
.
equal
(
store
.
getState
().
focusMode
.
focused
,
true
);
assert
.
equal
(
store
.
focusModeFocused
(),
true
);
});
it
(
'should return false by default when focus mode is not enabled'
,
function
()
{
assert
.
equal
(
store
.
getState
().
focusMode
.
enabled
,
false
);
assert
.
equal
(
store
.
getState
().
focusMode
.
focused
,
true
);
assert
.
equal
(
store
.
focusModeFocused
(),
false
);
});
});
describe
(
'focusModeUserPrettyName()'
,
function
()
{
it
(
'should return false by default when focus mode is not enabled'
,
function
()
{
store
=
createStore
(
[
selection
],
[{
focus
:
{
user
:
{
displayName
:
'FakeDisplayName'
}
}
}]
);
assert
.
equal
(
store
.
focusModeUserPrettyName
(),
'FakeDisplayName'
);
});
it
(
'should the username when displayName is missing'
,
function
()
{
store
=
createStore
(
[
selection
],
[{
focus
:
{
user
:
{
username
:
'FakeUserName'
}
}
}]
);
assert
.
equal
(
store
.
focusModeUserPrettyName
(),
'FakeUserName'
);
});
it
(
'should an return empty string when user object has no names'
,
function
()
{
store
=
createStore
([
selection
],
[{
focus
:
{
user
:
{}
}
}]);
assert
.
equal
(
store
.
focusModeUserPrettyName
(),
''
);
});
it
(
'should an return empty string when there is no focus object'
,
function
()
{
assert
.
equal
(
store
.
focusModeUserPrettyName
(),
''
);
});
});
describe
(
'focusModeUsername()'
,
function
()
{
it
(
'should return the user name when present'
,
function
()
{
store
=
createStore
(
[
selection
],
[{
focus
:
{
user
:
{
username
:
'FakeUserName'
}
}
}]
);
assert
.
equal
(
store
.
focusModeUsername
(),
'FakeUserName'
);
});
it
(
'should return null when the username is not present'
,
function
()
{
store
=
createStore
([
selection
],
[{
focus
:
{
user
:
{}
}
}]);
assert
.
isNull
(
store
.
focusModeUsername
());
});
it
(
'should return null when the user object is not present'
,
function
()
{
assert
.
isNull
(
store
.
focusModeUsername
());
});
});
describe
(
'highlightAnnotations()'
,
function
()
{
it
(
'sets the highlighted annotations'
,
function
()
{
store
.
highlightAnnotations
([
'id1'
,
'id2'
]);
...
...
src/sidebar/templates/sidebar-content.html
View file @
b104d018
<focused-mode-header
ng-if=
"vm.showFocusedHeader()"
>
</focused-mode-header>
<selection-tabs
ng-if=
"vm.showSelectedTabs()"
is-loading=
"vm.isLoading()"
>
...
...
src/styles/sidebar/components/focused-mode-header.scss
0 → 100644
View file @
b104d018
.focused-mode-header
{
margin-bottom
:
10px
;
button
{
width
:
100%
;
height
:
24px
;
margin-right
:
10px
;
&
:focus
{
outline
:
none
;
}
}
}
src/styles/sidebar/sidebar.scss
View file @
b104d018
...
...
@@ -27,6 +27,7 @@ $base-line-height: 20px;
@import
'./components/annotation-thread'
;
@import
'./components/annotation-user'
;
@import
'./components/excerpt'
;
@import
'./components/focused-mode-header'
;
@import
'./components/group-list'
;
@import
'./components/group-list-item'
;
@import
'./components/help-panel'
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment