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
b0dfce31
Unverified
Commit
b0dfce31
authored
Jul 22, 2019
by
Hannah Stepanek
Committed by
GitHub
Jul 22, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1254 from hypothesis/convert-search-status-bar
Convert search status bar to preact
parents
30d4004e
cdd54488
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
268 additions
and
222 deletions
+268
-222
search-status-bar.js
src/sidebar/components/search-status-bar.js
+101
-43
search-status-bar-test.js
src/sidebar/components/test/search-status-bar-test.js
+161
-142
index.js
src/sidebar/index.js
+4
-1
search-status-bar.html
src/sidebar/templates/search-status-bar.html
+0
-35
sidebar-content.html
src/sidebar/templates/sidebar-content.html
+2
-1
No files found.
src/sidebar/components/search-status-bar.js
View file @
b0dfce31
'use strict'
;
const
memoize
=
require
(
'../util/memoize'
);
const
{
Fragment
,
createElement
}
=
require
(
'preact'
);
const
propTypes
=
require
(
'prop-types'
);
const
{
useMemo
}
=
require
(
'preact/hooks'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
const
uiConstants
=
require
(
'../ui-constants'
);
const
useStore
=
require
(
'../store/use-store'
);
// @ngInject
function
SearchStatusBarController
(
store
,
rootThread
)
{
this
.
TAB_ANNOTATIONS
=
uiConstants
.
TAB_ANNOTATIONS
;
this
.
TAB_NOTES
=
uiConstants
.
TAB_NOTES
;
this
.
TAB_ORPHANS
=
uiConstants
.
TAB_ORPHANS
;
const
thread
=
()
=>
{
return
rootThread
.
thread
(
store
.
getState
());
};
const
visibleCount
=
memoize
(
thread
=>
{
return
thread
.
children
.
reduce
(
const
countVisibleAnns
=
annThread
=>
{
return
annThread
.
children
.
reduce
(
function
(
count
,
child
)
{
return
count
+
visibleCount
(
child
);
return
count
+
countVisibleAnns
(
child
);
},
t
hread
.
visible
?
1
:
0
annT
hread
.
visible
?
1
:
0
);
});
};
/**
* A bar where the user can clear a selection or search and see whether
* any search results were found.
* */
function
SearchStatusBar
({
selectedTab
,
totalAnnotations
,
totalNotes
,
rootThread
,
})
{
const
storeState
=
useStore
(
store
=>
store
.
getState
());
const
clearSelection
=
useStore
(
store
=>
store
.
clearSelection
);
const
filterQuery
=
storeState
.
filterQuery
;
const
filterActive
=
!!
storeState
.
filterQuery
;
this
.
filterMatchCount
=
function
()
{
return
visibleCount
(
thread
());
const
thread
=
rootThread
.
thread
(
storeState
);
const
visibleCount
=
useMemo
(()
=>
{
return
countVisibleAnns
(
thread
);
},
[
thread
]);
const
filterResults
=
()
=>
{
const
resultsCount
=
visibleCount
;
switch
(
resultsCount
)
{
case
0
:
return
'No results for "'
+
filterQuery
+
'"'
;
case
1
:
return
'1 search result'
;
default
:
return
resultsCount
+
' search results'
;
}
};
this
.
areAllAnnotationsVisible
=
function
()
{
if
(
store
.
getState
()
.
directLinkedGroupFetchFailed
)
{
const
areNotAllAnnotationsVisible
=
()
=>
{
if
(
store
State
.
directLinkedGroupFetchFailed
)
{
return
true
;
}
const
selection
=
store
.
getState
()
.
selectedAnnotationMap
;
const
selection
=
store
State
.
selectedAnnotationMap
;
if
(
!
selection
)
{
return
false
;
}
return
Object
.
keys
(
selection
).
length
>
0
;
};
this
.
filterQuery
=
function
()
{
return
store
.
getState
().
filterQuery
;
};
this
.
filterActive
=
function
()
{
return
!!
store
.
getState
().
filterQuery
;
};
this
.
onClearSelection
=
function
()
{
store
.
clearSelection
();
};
return
(
<
div
>
{
filterActive
&&
(
<
div
className
=
"search-status-bar"
>
<
button
className
=
"primary-action-btn primary-action-btn--short"
onClick
=
{
clearSelection
}
title
=
"Clear the search filter and show all annotations"
>
<
i
className
=
"primary-action-btn__icon h-icon-close"
/>
Clear
search
<
/button
>
<
span
>
{
filterResults
()}
<
/span
>
<
/div
>
)}
{
!
filterActive
&&
areNotAllAnnotationsVisible
()
&&
(
<
div
className
=
"search-status-bar"
>
<
button
className
=
"primary-action-btn primary-action-btn--short"
onClick
=
{
clearSelection
}
title
=
"Clear the selection and show all annotations"
>
{
selectedTab
===
uiConstants
.
TAB_ORPHANS
&&
(
<
Fragment
>
Show
all
annotations
and
notes
<
/Fragment
>
)}
{
selectedTab
===
uiConstants
.
TAB_ANNOTATIONS
&&
(
<
Fragment
>
Show
all
annotations
{
totalAnnotations
>
1
&&
<
span
>
({
totalAnnotations
})
<
/span>
}
<
/Fragment
>
)}
{
selectedTab
===
uiConstants
.
TAB_NOTES
&&
(
<
Fragment
>
Show
all
notes
{
totalNotes
>
1
&&
<
span
>
({
totalNotes
})
<
/span>
}
<
/Fragment
>
)}
<
/button
>
<
/div
>
)}
<
/div
>
);
}
module
.
export
s
=
{
controller
:
SearchStatusBarController
,
controllerAs
:
'vm'
,
bindings
:
{
selectedTab
:
'<'
,
totalAnnotations
:
'<'
,
totalNotes
:
'<'
,
}
,
template
:
require
(
'../templates/search-status-bar.html'
)
,
SearchStatusBar
.
propType
s
=
{
selectedTab
:
propTypes
.
oneOf
([
uiConstants
.
TAB_ANNOTATIONS
,
uiConstants
.
TAB_ORPHANS
,
uiConstants
.
TAB_NOTES
,
]).
isRequired
,
totalAnnotations
:
propTypes
.
number
.
isRequired
,
totalNotes
:
propTypes
.
number
.
isRequired
,
rootThread
:
propTypes
.
object
.
isRequired
,
};
SearchStatusBar
.
injectedProps
=
[
'rootThread'
];
module
.
exports
=
withServices
(
SearchStatusBar
);
src/sidebar/components/test/search-status-bar-test.js
View file @
b0dfce31
'use strict'
;
const
angular
=
require
(
'angular'
);
const
{
shallow
}
=
require
(
'enzyme'
);
const
{
createElement
}
=
require
(
'preact'
);
const
util
=
require
(
'../../directive/test/util'
);
describe
(
'searchStatusBar'
,
()
=>
{
before
(()
=>
{
angular
.
module
(
'app'
,
[])
.
component
(
'searchStatusBar'
,
require
(
'../search-status-bar'
));
});
const
SearchStatusBar
=
require
(
'../search-status-bar'
);
describe
(
'SearchStatusBar'
,
()
=>
{
let
fakeRootThread
;
let
fakeStore
;
function
createComponent
(
props
)
{
return
shallow
(
<
SearchStatusBar
rootThread
=
{
fakeRootThread
}
{...
props
}
/
>
).
dive
();
// dive() needed because this component uses `withServices`
}
beforeEach
(()
=>
{
fakeRootThread
=
{
thread
:
sinon
.
stub
(),
thread
:
sinon
.
stub
()
.
returns
({
children
:
[]
})
,
};
fakeStore
=
{
getState
:
sinon
.
stub
(),
...
...
@@ -26,48 +27,20 @@ describe('searchStatusBar', () => {
clearDirectLinkedIds
:
sinon
.
stub
(),
clearSelection
:
sinon
.
stub
(),
};
angular
.
mock
.
module
(
'app'
,
{
store
:
fakeStore
,
rootThread
:
fakeRootThread
,
});
});
describe
(
'filterQuery'
,
()
=>
{
[
'tag:foo'
,
null
].
forEach
(
filterQuery
=>
{
it
(
'returns the `filterQuery`'
,
()
=>
{
fakeStore
.
getState
.
returns
({
filterQuery
});
const
elem
=
util
.
createDirective
(
document
,
'searchStatusBar'
,
{});
const
ctrl
=
elem
.
ctrl
;
assert
.
equal
(
ctrl
.
filterQuery
(),
filterQuery
);
});
SearchStatusBar
.
$imports
.
$mock
({
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
});
});
describe
(
'filterActive'
,
()
=>
{
it
(
'returns true if there is a `filterQuery`'
,
()
=>
{
fakeStore
.
getState
.
returns
({
filterQuery
:
'tag:foo'
});
const
elem
=
util
.
createDirective
(
document
,
'searchStatusBar'
,
{});
const
ctrl
=
elem
.
ctrl
;
assert
.
isTrue
(
ctrl
.
filterActive
());
afterEach
(()
=>
{
SearchStatusBar
.
$imports
.
$restore
();
});
it
(
'returns false if `filterQuery` is null'
,
()
=>
{
fakeStore
.
getState
.
returns
({
filterQuery
:
null
});
const
elem
=
util
.
createDirective
(
document
,
'searchStatusBar'
,
{});
const
ctrl
=
elem
.
ctrl
;
assert
.
isFalse
(
ctrl
.
filterActive
());
});
});
describe
(
'filterMatchCount'
,
()
=>
{
it
(
'returns the total number of visible annotations or replies'
,
()
=>
{
fakeRootThread
.
thread
.
returns
({
[
{
description
:
'shows correct text if 2 annotations match the search filter'
,
children
:
[
{
id
:
'1'
,
...
...
@@ -80,120 +53,166 @@ describe('searchStatusBar', () => {
children
:
[],
},
],
expectedText
:
'2 search results'
,
},
{
description
:
'shows correct text if 1 annotation matches the search filter'
,
children
:
[
{
id
:
'1'
,
visible
:
true
,
children
:
[{
id
:
'3'
,
visible
:
false
,
children
:
[]
}],
},
{
id
:
'2'
,
visible
:
false
,
children
:
[],
},
],
expectedText
:
'1 search result'
,
},
{
description
:
'shows correct text if no annotation matches the search filter'
,
children
:
[
{
id
:
'1'
,
visible
:
false
,
children
:
[{
id
:
'3'
,
visible
:
false
,
children
:
[]
}],
},
{
id
:
'2'
,
visible
:
false
,
children
:
[],
},
],
expectedText
:
'No results for "tag:foo"'
,
},
].
forEach
(
test
=>
{
it
(
test
.
description
,
()
=>
{
fakeRootThread
.
thread
.
returns
({
children
:
test
.
children
,
});
fakeStore
.
getState
.
returns
({
filterQuery
:
'tag:foo'
,
});
const
elem
=
util
.
createDirective
(
document
,
'searchStatusBar'
,
{});
const
ctrl
=
elem
.
ctrl
;
assert
.
equal
(
ctrl
.
filterMatchCount
(),
2
);
});
const
wrapper
=
createComponent
({
selectedTab
:
'annotation'
,
totalAnnotations
:
3
,
totalNotes
:
0
,
});
describe
(
'areAllAnnotationsVisible'
,
()
=>
{
it
(
'returns true if the direct-linked group fetch failed'
,
()
=>
{
fakeStore
.
getState
.
returns
({
directLinkedGroupFetchFailed
:
true
});
const
elem
=
util
.
createDirective
(
document
,
'searchStatusBar'
,
{});
const
ctrl
=
elem
.
ctrl
;
const
buttonText
=
wrapper
.
find
(
'button'
).
text
();
assert
.
equal
(
buttonText
,
'Clear search'
);
assert
.
isTrue
(
ctrl
.
areAllAnnotationsVisible
());
const
searchResultsText
=
wrapper
.
find
(
'span'
).
text
();
assert
.
equal
(
searchResultsText
,
test
.
expectedText
);
});
});
it
(
'returns true if there are annotations selected
'
,
()
=>
{
it
(
'displays "Show all annotations" button when a direct-linked group fetch fails
'
,
()
=>
{
fakeStore
.
getState
.
returns
({
directLinkedGroupFetchFailed
:
false
,
selectedAnnotationMap
:
{
ann
:
true
},
filterQuery
:
null
,
directLinkedGroupFetchFailed
:
true
,
selectedAnnotationMap
:
{
annId
:
true
},
});
const
elem
=
util
.
createDirective
(
document
,
'searchStatusBar'
,
{});
const
ctrl
=
elem
.
ctrl
;
const
wrapper
=
createComponent
({
selectedTab
:
'annotation'
,
totalAnnotations
:
1
,
totalNotes
:
0
,
});
assert
.
isTrue
(
ctrl
.
areAllAnnotationsVisible
());
const
buttonText
=
wrapper
.
find
(
'button'
).
text
();
assert
.
equal
(
buttonText
,
'Show all annotations'
);
});
it
(
'returns false if there are no annotations selected
'
,
()
=>
{
it
(
'displays "Show all annotations" button when there are selected annotations
'
,
()
=>
{
fakeStore
.
getState
.
returns
({
filterQuery
:
null
,
directLinkedGroupFetchFailed
:
false
,
selectedAnnotationMap
:
{
},
selectedAnnotationMap
:
{
annId
:
true
},
});
const
elem
=
util
.
createDirective
(
document
,
'searchStatusBar'
,
{});
const
ctrl
=
elem
.
ctrl
;
const
wrapper
=
createComponent
({
selectedTab
:
'annotation'
,
totalAnnotations
:
1
,
totalNotes
:
0
,
});
assert
.
isFalse
(
ctrl
.
areAllAnnotationsVisible
());
const
buttonText
=
wrapper
.
find
(
'button'
).
text
();
assert
.
equal
(
buttonText
,
'Show all annotations'
);
});
it
(
'returns false if the `selectedAnnotationMap` is null'
,
()
=>
{
[
null
,
{}].
forEach
(
selectedAnnotationMap
=>
{
it
(
'does not display "Show all annotations" button when there are no selected annotations'
,
()
=>
{
fakeStore
.
getState
.
returns
({
filterQuery
:
null
,
directLinkedGroupFetchFailed
:
false
,
selectedAnnotationMap
:
null
,
selectedAnnotationMap
:
selectedAnnotationMap
,
});
const
wrapper
=
createComponent
({
selectedTab
:
'annotation'
,
totalAnnotations
:
1
,
totalNotes
:
0
,
});
const
elem
=
util
.
createDirective
(
document
,
'searchStatusBar'
,
{});
const
ctrl
=
elem
.
ctrl
;
assert
.
isFalse
(
ctrl
.
areAllAnnotationsVisible
());
const
buttons
=
wrapper
.
find
(
'button'
);
assert
.
equal
(
buttons
.
length
,
0
);
});
});
context
(
'when there is a filter'
,
()
=>
{
it
(
'should display the filter count'
,
()
=>
{
fakeRootThread
.
thread
.
returns
({
children
:
[
[
{
id
:
'1'
,
visible
:
true
,
children
:
[{
id
:
'3'
,
visible
:
true
,
children
:
[]
}],
description
:
'displays "Show all annotations and notes" button when the orphans tab is selected'
,
selectedTab
:
'orphan'
,
totalAnnotations
:
1
,
totalNotes
:
1
,
expectedText
:
'Show all annotations and notes'
,
},
{
id
:
'2'
,
visible
:
false
,
children
:
[],
description
:
'displays "Show all notes" button when the notes tab is selected'
,
selectedTab
:
'note'
,
totalAnnotations
:
1
,
totalNotes
:
1
,
expectedText
:
'Show all notes'
,
},
],
});
fakeStore
.
getState
.
returns
({
filterQuery
:
'tag:foo'
,
});
const
elem
=
util
.
createDirective
(
document
,
'searchStatusBar'
,
{});
assert
.
include
(
elem
[
0
].
textContent
,
'2 search results'
);
});
});
context
(
'when there is a selection'
,
()
=>
{
it
(
'should display the "Show all annotations (2)" message when there are 2 annotations'
,
()
=>
{
const
msg
=
'Show all annotations'
;
const
msgCount
=
'(2)'
;
fakeStore
.
getState
.
returns
({
selectedAnnotationMap
:
{
ann1
:
true
},
});
const
elem
=
util
.
createDirective
(
document
,
'searchStatusBar'
,
{
{
description
:
'displays "Show all notes (2)" button when the notes tab is selected and there are two notes'
,
selectedTab
:
'note'
,
totalAnnotations
:
2
,
totalNotes
:
2
,
expectedText
:
'Show all notes (2)'
,
},
{
description
:
'displays "Show all annotations (2)" button when the notes tab is selected and there are two annotations'
,
selectedTab
:
'annotation'
,
});
const
clearBtn
=
elem
[
0
].
querySelector
(
'button'
);
assert
.
include
(
clearBtn
.
textContent
,
msg
);
assert
.
include
(
clearBtn
.
textContent
,
msgCount
);
});
it
(
'should display the "Show all notes (3)" message when there are 3 notes'
,
()
=>
{
const
msg
=
'Show all notes'
;
const
msgCount
=
'(3)'
;
totalAnnotations
:
2
,
totalNotes
:
2
,
expectedText
:
'Show all annotations (2)'
,
},
].
forEach
(
test
=>
{
it
(
test
.
description
,
()
=>
{
fakeStore
.
getState
.
returns
({
selectedAnnotationMap
:
{
ann1
:
true
},
filterQuery
:
null
,
directLinkedGroupFetchFailed
:
false
,
selectedAnnotationMap
:
{
annId
:
true
},
});
const
elem
=
util
.
createDirective
(
document
,
'searchStatusBar'
,
{
totalNotes
:
3
,
selectedTab
:
'note'
,
const
wrapper
=
createComponent
({
selectedTab
:
test
.
selectedTab
,
totalAnnotations
:
test
.
totalAnnotations
,
totalNotes
:
test
.
totalNotes
,
});
const
clearBtn
=
elem
[
0
].
querySelector
(
'button'
);
assert
.
include
(
clearBtn
.
textContent
,
msg
);
assert
.
include
(
clearBtn
.
textContent
,
msgCoun
t
);
const
buttonText
=
wrapper
.
find
(
'button'
).
text
(
);
assert
.
equal
(
buttonText
,
test
.
expectedTex
t
);
});
});
});
src/sidebar/index.js
View file @
b0dfce31
...
...
@@ -181,7 +181,10 @@ function startAngularApp(config) {
wrapReactComponent
(
require
(
'./components/moderation-banner'
))
)
.
component
(
'newNoteBtn'
,
require
(
'./components/new-note-btn'
))
.
component
(
'searchStatusBar'
,
require
(
'./components/search-status-bar'
))
.
component
(
'searchStatusBar'
,
wrapReactComponent
(
require
(
'./components/search-status-bar'
))
)
.
component
(
'selectionTabs'
,
require
(
'./components/selection-tabs'
))
.
component
(
'sidebarContent'
,
require
(
'./components/sidebar-content'
))
.
component
(
...
...
src/sidebar/templates/search-status-bar.html
deleted
100644 → 0
View file @
30d4004e
<div
class=
"search-status-bar"
ng-if=
"vm.filterActive()"
>
<button
class=
"primary-action-btn primary-action-btn--short"
ng-click=
"vm.onClearSelection()"
title=
"Clear the search filter and show all annotations"
>
<i
class=
"primary-action-btn__icon h-icon-close"
></i>
Clear search
</button>
<span
ng-pluralize
count=
"vm.filterMatchCount()"
when=
"{'0': 'No results for “{{vm.filterQuery()}}”',
'one': '1 search result',
'other': '{} search results'}"
></span>
</div>
<div
class=
"search-status-bar"
ng-if=
"!vm.filterActive() && vm.areAllAnnotationsVisible()"
>
<button
class=
"primary-action-btn primary-action-btn--short"
ng-click=
"vm.onClearSelection()"
title=
"Clear the selection and show all annotations"
>
<span
ng-if=
"!vm.selectedTab || vm.selectedTab === vm.TAB_ORPHANS"
>
Show all annotations and notes
</span>
<span
ng-if=
"vm.selectedTab === vm.TAB_ANNOTATIONS"
>
Show all annotations
<span
ng-if=
"vm.totalAnnotations > 1"
>
({{vm.totalAnnotations}})
</span>
</span>
<span
ng-if=
"vm.selectedTab === vm.TAB_NOTES"
>
Show all notes
<span
ng-if=
"vm.totalNotes > 1"
>
({{vm.totalNotes}})
</span>
</span>
</button>
</div>
src/sidebar/templates/sidebar-content.html
View file @
b0dfce31
...
...
@@ -9,7 +9,8 @@
</selection-tabs>
<search-status-bar
ng-show=
"!vm.isLoading()"
class=
"search-status-bar"
ng-if=
"!vm.isLoading()"
selected-tab=
"vm.selectedTab"
total-annotations=
"vm.totalAnnotations"
total-notes=
"vm.totalNotes"
>
...
...
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