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
109c4ca0
Commit
109c4ca0
authored
May 05, 2022
by
Lyza Danger Gardner
Committed by
Lyza Gardner
May 12, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Extract `EmptyAnnotation` component
parent
6de3558a
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
258 additions
and
147 deletions
+258
-147
Annotation.js
src/sidebar/components/Annotation/Annotation.js
+38
-59
EmptyAnnotation.js
src/sidebar/components/Annotation/EmptyAnnotation.js
+45
-0
Annotation-test.js
src/sidebar/components/Annotation/test/Annotation-test.js
+4
-76
EmptyAnnotation-test.js
...idebar/components/Annotation/test/EmptyAnnotation-test.js
+108
-0
Thread.js
src/sidebar/components/Thread.js
+23
-11
Thread-test.js
src/sidebar/components/test/Thread-test.js
+40
-1
No files found.
src/sidebar/components/Annotation/Annotation.js
View file @
109c4ca0
...
...
@@ -14,8 +14,17 @@ import AnnotationReplyToggle from './AnnotationReplyToggle';
/**
* @typedef {import("../../../types/api").Annotation} Annotation
* @typedef {import("../../../types/api").SavedAnnotation} SavedAnnotation
* @typedef {import('../../../types/api').Group} Group
*/
/**
* @typedef AnnotationProps
* @prop {Annotation} annotation
* @prop {boolean} isReply
* @prop {VoidFunction} [onToggleReplies] - Callback to expand/collapse reply
* threads. The presence of a function indicates a toggle should be rendered.
* @prop {number} replyCount - Number of replies to this annotation's thread
* @prop {boolean} threadIsCollapsed - Is the thread to which this annotation belongs currently collapsed?
* @prop {import('../../services/annotations').AnnotationsService} annotationsService
*/
function
SavingMessage
()
{
...
...
@@ -42,20 +51,6 @@ function SavingMessage() {
<
/div
>
);
}
/**
* @typedef AnnotationProps
* @prop {Annotation} [annotation] - The annotation to render. If undefined,
* this Annotation will render as a "missing annotation" and will stand in
* as an Annotation for threads that lack an annotation.
* @prop {boolean} hasAppliedFilter - Is any filter applied currently?
* @prop {boolean} isReply
* @prop {VoidFunction} onToggleReplies - Callback to expand/collapse reply threads
* @prop {number} replyCount - Number of replies to this annotation's thread
* @prop {boolean} threadIsCollapsed - Is the thread to which this annotation belongs currently collapsed?
* @prop {import('../../services/annotations').AnnotationsService} annotationsService
*/
/**
* A single annotation.
*
...
...
@@ -63,74 +58,58 @@ function SavingMessage() {
*/
function
Annotation
({
annotation
,
hasAppliedFilter
,
isReply
,
onToggleReplies
,
replyCount
,
threadIsCollapsed
,
annotationsService
,
})
{
const
isCollapsedReply
=
isReply
&&
threadIsCollapsed
;
const
store
=
useSidebarStore
();
const
draft
=
annotation
&&
store
.
getDraft
(
annotation
);
const
annotationQuote
=
quote
(
annotation
);
const
draft
=
store
.
getDraft
(
annotation
);
const
userid
=
store
.
profile
().
userid
;
const
annotationQuote
=
annotation
?
quote
(
annotation
)
:
null
;
const
isFocused
=
annotation
&&
store
.
isAnnotationFocused
(
annotation
.
$tag
);
const
isSaving
=
annotation
&&
store
.
isSavingAnnotation
(
annotation
);
const
isEditing
=
annotation
&&
!!
draft
&&
!
isSaving
;
const
isFocused
=
store
.
isAnnotationFocused
(
annotation
.
$tag
);
const
isSaving
=
store
.
isSavingAnnotation
(
annotation
);
const
userid
=
store
.
profile
().
userid
;
const
showActions
=
annotation
&&
!
isSaving
&&
!
isEditing
&&
isSaved
(
annotation
);
const
showReplyToggle
=
!
isReply
&&
!
isEditing
&&
!
hasAppliedFilter
&&
replyCount
>
0
;
const
isEditing
=
!!
draft
&&
!
isSaving
;
const
isCollapsedReply
=
isReply
&&
threadIsCollapsed
;
const
showActions
=
!
isSaving
&&
!
isEditing
&&
isSaved
(
annotation
);
const
onReply
=
()
=>
{
if
(
annotation
&&
isSaved
(
annotation
)
&&
userid
)
{
if
(
isSaved
(
annotation
)
&&
userid
)
{
annotationsService
.
reply
(
annotation
,
userid
);
}
};
return
(
<
article
className
=
"space-y-4"
>
{
annotation
&&
(
<>
<
AnnotationHeader
annotation
=
{
annotation
}
isEditing
=
{
isEditing
}
replyCount
=
{
replyCount
}
threadIsCollapsed
=
{
threadIsCollapsed
}
/
>
{
annotationQuote
&&
(
<
AnnotationQuote
quote
=
{
annotationQuote
}
isFocused
=
{
isFocused
}
isOrphan
=
{
isOrphan
(
annotation
)}
/
>
)}
{
!
isCollapsedReply
&&
!
isEditing
&&
(
<
AnnotationBody
annotation
=
{
annotation
}
/
>
)}
<
AnnotationHeader
annotation
=
{
annotation
}
isEditing
=
{
isEditing
}
replyCount
=
{
replyCount
}
threadIsCollapsed
=
{
threadIsCollapsed
}
/
>
{
isEditing
&&
(
<
AnnotationEditor
annotation
=
{
annotation
}
draft
=
{
draft
}
/
>
)}
<
/
>
{
annotationQuote
&&
(
<
AnnotationQuote
quote
=
{
annotationQuote
}
isFocused
=
{
isFocused
}
isOrphan
=
{
isOrphan
(
annotation
)}
/
>
)}
{
!
annotation
&&
!
isCollapsedReply
&&
(
<
div
>
<
em
>
Message
not
available
.
<
/em
>
<
/div
>
{
!
isCollapsedReply
&&
!
isEditing
&&
(
<
AnnotationBody
annotation
=
{
annotation
}
/
>
)}
{
isEditing
&&
<
AnnotationEditor
annotation
=
{
annotation
}
draft
=
{
draft
}
/>
}
{
!
isCollapsedReply
&&
(
<
footer
className
=
"flex items-center"
>
{
showReplyToggle
&&
(
{
onToggleReplies
&&
(
<
AnnotationReplyToggle
onToggleReplies
=
{
onToggleReplies
}
replyCount
=
{
replyCount
}
...
...
src/sidebar/components/Annotation/EmptyAnnotation.js
0 → 100644
View file @
109c4ca0
import
AnnotationReplyToggle
from
'./AnnotationReplyToggle'
;
/**
* @typedef {import('./Annotation').AnnotationProps} AnnotationProps
* @typedef {Omit<AnnotationProps, 'annotation' | 'annotationsService'>} EmptyAnnotationProps
*/
/**
* Render an "annotation" when the annotation itself is missing. This can
* happen when an annotation is deleted by its author but there are still
* replies that pertain to it.
*
* @param {EmptyAnnotationProps} props
*/
export
default
function
EmptyAnnotation
({
isReply
,
replyCount
,
threadIsCollapsed
,
onToggleReplies
,
})
{
const
isCollapsedReply
=
isReply
&&
threadIsCollapsed
;
return
(
<
article
className
=
"space-y-4"
aria
-
label
=
{
`
${
isReply
?
'Reply'
:
'Annotation'
}
with unavailable content`
}
>
{
!
isCollapsedReply
&&
(
<
div
>
<
em
>
Message
not
available
.
<
/em
>
<
/div
>
)}
{
onToggleReplies
&&
(
<
footer
className
=
"flex items-center"
>
<
AnnotationReplyToggle
onToggleReplies
=
{
onToggleReplies
}
replyCount
=
{
replyCount
}
threadIsCollapsed
=
{
threadIsCollapsed
}
/
>
<
/footer
>
)}
<
/article
>
);
}
src/sidebar/components/Annotation/test/Annotation-test.js
View file @
109c4ca0
...
...
@@ -8,8 +8,6 @@ import { mockImportedComponents } from '../../../../test-util/mock-imported-comp
import
Annotation
,
{
$imports
}
from
'../Annotation'
;
describe
(
'Annotation'
,
()
=>
{
let
fakeOnToggleReplies
;
// Dependency Mocks
let
fakeMetadata
;
...
...
@@ -31,9 +29,7 @@ describe('Annotation', () => {
<
Annotation
annotation
=
{
fixtures
.
defaultAnnotation
()}
annotationsService
=
{
fakeAnnotationsService
}
hasAppliedFilter
=
{
false
}
isReply
=
{
false
}
onToggleReplies
=
{
fakeOnToggleReplies
}
replyCount
=
{
0
}
threadIsCollapsed
=
{
true
}
{...
props
}
...
...
@@ -42,8 +38,6 @@ describe('Annotation', () => {
};
beforeEach
(()
=>
{
fakeOnToggleReplies
=
sinon
.
stub
();
fakeAnnotationsService
=
{
reply
:
sinon
.
stub
(),
save
:
sinon
.
stub
().
resolves
(),
...
...
@@ -111,8 +105,10 @@ describe('Annotation', () => {
});
describe
(
'reply thread toggle'
,
()
=>
{
it
(
'should render a toggle button if the annotation has replies'
,
()
=>
{
it
(
'should render a toggle button if provided with a toggle callback'
,
()
=>
{
const
fakeOnToggleReplies
=
sinon
.
stub
();
const
wrapper
=
createComponent
({
onToggleReplies
:
fakeOnToggleReplies
,
replyCount
:
5
,
threadIsCollapsed
:
true
,
});
...
...
@@ -125,30 +121,8 @@ describe('Annotation', () => {
assert
.
equal
(
toggle
.
props
().
threadIsCollapsed
,
true
);
});
it
(
'should not render a reply toggle if the annotation has no replies'
,
()
=>
{
const
wrapper
=
createComponent
({
isReply
:
false
,
replyCount
:
0
,
threadIsCollapsed
:
true
,
});
assert
.
isFalse
(
wrapper
.
find
(
'AnnotationReplyToggle'
).
exists
());
});
it
(
'should not render a reply toggle if there are applied filters'
,
()
=>
{
const
wrapper
=
createComponent
({
hasAppliedFilter
:
true
,
isReply
:
false
,
replyCount
:
5
,
threadIsCollapsed
:
true
,
});
assert
.
isFalse
(
wrapper
.
find
(
'AnnotationReplyToggle'
).
exists
());
});
it
(
'should not render a reply toggle if the annotation itself is a reply'
,
()
=>
{
it
(
'should not render a reply toggle if no toggle callback provided'
,
()
=>
{
const
wrapper
=
createComponent
({
isReply
:
true
,
replyCount
:
5
,
threadIsCollapsed
:
true
,
});
...
...
@@ -222,52 +196,6 @@ describe('Annotation', () => {
assert
.
isTrue
(
wrapper
.
find
(
'footer'
).
exists
());
});
});
context
(
'missing annotation'
,
()
=>
{
it
(
'should render a message about annotation unavailability'
,
()
=>
{
const
wrapper
=
createComponent
({
annotation
:
undefined
});
assert
.
equal
(
wrapper
.
text
(),
'Message not available.'
);
});
it
(
'should not render a message if collapsed reply'
,
()
=>
{
const
wrapper
=
createComponent
({
annotation
:
undefined
,
isReply
:
true
,
threadIsCollapsed
:
true
,
});
assert
.
equal
(
wrapper
.
text
(),
''
);
});
it
(
'should render reply toggle controls if there are replies'
,
()
=>
{
const
wrapper
=
createComponent
({
annotation
:
undefined
,
replyCount
:
5
,
threadIsCollapsed
:
true
,
});
const
toggle
=
wrapper
.
find
(
'AnnotationReplyToggle'
);
assert
.
isTrue
(
toggle
.
exists
());
assert
.
equal
(
toggle
.
props
().
onToggleReplies
,
fakeOnToggleReplies
);
assert
.
equal
(
toggle
.
props
().
replyCount
,
5
);
assert
.
equal
(
toggle
.
props
().
threadIsCollapsed
,
true
);
});
it
(
'should not render reply toggle controls if collapsed reply'
,
()
=>
{
const
wrapper
=
createComponent
({
annotation
:
undefined
,
isReply
:
true
,
replyCount
:
5
,
threadIsCollapsed
:
true
,
});
const
toggle
=
wrapper
.
find
(
'AnnotationReplyToggle'
);
assert
.
isFalse
(
toggle
.
exists
());
});
});
});
it
(
...
...
src/sidebar/components/Annotation/test/EmptyAnnotation-test.js
0 → 100644
View file @
109c4ca0
import
{
mount
}
from
'enzyme'
;
import
{
checkAccessibility
}
from
'../../../../test-util/accessibility'
;
import
{
mockImportedComponents
}
from
'../../../../test-util/mock-imported-components'
;
import
EmptyAnnotation
,
{
$imports
}
from
'../EmptyAnnotation'
;
describe
(
'EmptyAnnotation'
,
()
=>
{
const
createComponent
=
props
=>
{
return
mount
(
<
EmptyAnnotation
isReply
=
{
false
}
replyCount
=
{
0
}
threadIsCollapsed
=
{
true
}
{...
props
}
/
>
);
};
beforeEach
(()
=>
{
$imports
.
$mock
(
mockImportedComponents
());
});
afterEach
(()
=>
{
$imports
.
$restore
();
});
describe
(
'reply thread toggle'
,
()
=>
{
it
(
'should render a toggle button if toggle callback provided'
,
()
=>
{
const
fakeOnToggleReplies
=
sinon
.
stub
();
const
wrapper
=
createComponent
({
onToggleReplies
:
fakeOnToggleReplies
,
replyCount
:
5
,
threadIsCollapsed
:
true
,
});
const
toggle
=
wrapper
.
find
(
'AnnotationReplyToggle'
);
assert
.
isTrue
(
toggle
.
exists
());
assert
.
equal
(
toggle
.
props
().
onToggleReplies
,
fakeOnToggleReplies
);
assert
.
equal
(
toggle
.
props
().
replyCount
,
5
);
assert
.
equal
(
toggle
.
props
().
threadIsCollapsed
,
true
);
});
it
(
'should not render a reply toggle if no callback provided'
,
()
=>
{
const
wrapper
=
createComponent
({
isReply
:
false
,
replyCount
:
5
,
threadIsCollapsed
:
true
,
});
assert
.
isFalse
(
wrapper
.
find
(
'AnnotationReplyToggle'
).
exists
());
});
});
describe
(
'labeling and description'
,
()
=>
{
it
(
'should render a label and message for top-level missing annotations'
,
()
=>
{
const
wrapper
=
createComponent
();
assert
.
equal
(
wrapper
.
find
(
'article'
).
props
()[
'aria-label'
],
'Annotation with unavailable content'
);
assert
.
equal
(
wrapper
.
text
(),
'Message not available.'
);
});
it
(
'should label the EmptyAnnotation as a reply if it is a reply'
,
()
=>
{
const
wrapper
=
createComponent
({
isReply
:
true
,
});
assert
.
equal
(
wrapper
.
find
(
'article'
).
props
()[
'aria-label'
],
'Reply with unavailable content'
);
});
it
(
'should not render a message if collapsed reply'
,
()
=>
{
const
wrapper
=
createComponent
({
isReply
:
true
,
threadIsCollapsed
:
true
,
});
assert
.
equal
(
wrapper
.
text
(),
''
);
});
});
it
(
'should pass a11y checks'
,
checkAccessibility
([
{
content
:
()
=>
createComponent
(),
},
{
name
:
'when a collapsed top-level thread'
,
content
:
()
=>
{
return
createComponent
({
isReply
:
false
,
threadIsCollapsed
:
true
});
},
},
{
name
:
'when a collapsed reply'
,
content
:
()
=>
{
return
createComponent
({
isReply
:
true
,
threadIsCollapsed
:
true
});
},
},
])
);
});
src/sidebar/components/Thread.js
View file @
109c4ca0
...
...
@@ -8,6 +8,7 @@ import { countHidden, countVisible } from '../helpers/thread';
import
Annotation
from
'./Annotation'
;
import
AnnotationHeader
from
'./Annotation/AnnotationHeader'
;
import
EmptyAnnotation
from
'./Annotation/EmptyAnnotation'
;
import
ModerationBanner
from
'./ModerationBanner'
;
/** @typedef {import('../helpers/build-thread').Thread} Thread */
...
...
@@ -107,29 +108,40 @@ function Thread({ thread, threadsService }) {
[
store
,
thread
.
id
,
thread
.
collapsed
]
);
const
showReplyToggle
=
!
isReply
&&
!
isEditing
&&
!
hasAppliedFilter
&&
thread
.
replyCount
>
0
;
// Memoize annotation content to avoid re-rendering an annotation when content
// in other annotations/threads change.
const
annotationContent
=
useMemo
(
()
=>
thread
.
visible
&&
(
<>
{
thread
.
annotation
&&
(
<
ModerationBanner
annotation
=
{
thread
.
annotation
}
/
>
{
thread
.
annotation
?
(
<>
<
ModerationBanner
annotation
=
{
thread
.
annotation
}
/
>
<
Annotation
annotation
=
{
thread
.
annotation
}
isReply
=
{
isReply
}
onToggleReplies
=
{
showReplyToggle
?
onToggleReplies
:
undefined
}
replyCount
=
{
thread
.
replyCount
}
threadIsCollapsed
=
{
thread
.
collapsed
}
/
>
<
/
>
)
:
(
<
EmptyAnnotation
isReply
=
{
isReply
}
onToggleReplies
=
{
showReplyToggle
?
onToggleReplies
:
undefined
}
replyCount
=
{
thread
.
replyCount
}
threadIsCollapsed
=
{
thread
.
collapsed
}
/
>
)}
<
Annotation
annotation
=
{
thread
.
annotation
}
hasAppliedFilter
=
{
hasAppliedFilter
}
isReply
=
{
isReply
}
onToggleReplies
=
{
onToggleReplies
}
replyCount
=
{
thread
.
replyCount
}
threadIsCollapsed
=
{
thread
.
collapsed
}
/
>
<
/
>
),
[
hasAppliedFilter
,
isReply
,
onToggleReplies
,
showReplyToggle
,
thread
.
annotation
,
thread
.
replyCount
,
thread
.
collapsed
,
...
...
src/sidebar/components/test/Thread-test.js
View file @
109c4ca0
...
...
@@ -158,6 +158,45 @@ describe('Thread', () => {
});
});
describe
(
'toggling replies for top-level threads'
,
()
=>
{
it
(
'provides an `onToggleReplies` callback for top-level threads with replies'
,
()
=>
{
const
threadWithChildren
=
buildThreadWithChildren
();
const
wrapper
=
createComponent
({
thread
:
threadWithChildren
});
assert
.
isFunction
(
wrapper
.
find
(
'Annotation'
).
props
().
onToggleReplies
);
});
it
(
'does not provide a toggle callback if thread is being edited'
,
()
=>
{
fakeStore
.
getDraft
.
returns
({});
const
threadWithChildren
=
buildThreadWithChildren
();
const
wrapper
=
createComponent
({
thread
:
threadWithChildren
});
assert
.
isUndefined
(
wrapper
.
find
(
'Annotation'
).
props
().
onToggleReplies
);
});
it
(
'does not provide a toggle callback if thread is a reply'
,
()
=>
{
const
threadWithChildren
=
buildThreadWithChildren
();
threadWithChildren
.
parent
=
1
;
const
wrapper
=
createComponent
({
thread
:
threadWithChildren
});
assert
.
isUndefined
(
wrapper
.
find
(
'Annotation'
).
props
().
onToggleReplies
);
});
it
(
'does not provide a toggle callback if there is an applied filter'
,
()
=>
{
fakeStore
.
hasAppliedFilter
.
returns
(
true
);
const
threadWithChildren
=
buildThreadWithChildren
();
const
wrapper
=
createComponent
({
thread
:
threadWithChildren
});
assert
.
isUndefined
(
wrapper
.
find
(
'Annotation'
).
props
().
onToggleReplies
);
});
it
(
'does not provide a toggle callback if there are no replies'
,
()
=>
{
const
thread
=
createThread
();
const
wrapper
=
createComponent
({
thread
});
assert
.
isUndefined
(
wrapper
.
find
(
'Annotation'
).
props
().
onToggleReplies
);
});
});
context
(
'collapsed thread with annotation and children'
,
()
=>
{
let
collapsedThread
;
...
...
@@ -193,7 +232,7 @@ describe('Thread', () => {
it
(
'renders an annotation component'
,
()
=>
{
const
wrapper
=
createComponent
({
thread
:
noAnnotationThread
});
const
annotation
=
wrapper
.
find
(
'Annotation'
);
const
annotation
=
wrapper
.
find
(
'
Empty
Annotation'
);
assert
.
isTrue
(
annotation
.
exists
());
});
...
...
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