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
855d3695
Unverified
Commit
855d3695
authored
Nov 20, 2019
by
Robert Knight
Committed by
GitHub
Nov 20, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1475 from hypothesis/convert-excerpt
Convert excerpt to Preact (2/n)
parents
804c0177
533fbaf0
Changes
14
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
541 additions
and
638 deletions
+541
-638
annotation-body.js
src/sidebar/components/annotation-body.js
+102
-0
annotation-quote.js
src/sidebar/components/annotation-quote.js
+54
-0
annotation.js
src/sidebar/components/annotation.js
+0
-4
excerpt.js
src/sidebar/components/excerpt.js
+177
-165
annotation-body-test.js
src/sidebar/components/test/annotation-body-test.js
+33
-0
annotation-quote-test.js
src/sidebar/components/test/annotation-quote-test.js
+29
-0
annotation-test.js
src/sidebar/components/test/annotation-test.js
+21
-30
excerpt-test.js
src/sidebar/components/test/excerpt-test.js
+102
-176
index.js
src/sidebar/index.js
+8
-10
annotation.html
src/sidebar/templates/annotation.html
+14
-37
excerpt.html
src/sidebar/templates/excerpt.html
+0
-21
excerpt-overflow-monitor.js
src/sidebar/util/excerpt-overflow-monitor.js
+0
-109
excerpt-overflow-monitor-test.js
src/sidebar/util/test/excerpt-overflow-monitor-test.js
+0
-86
excerpt.scss
src/styles/sidebar/components/excerpt.scss
+1
-0
No files found.
src/sidebar/components/annotation-body.js
0 → 100644
View file @
855d3695
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
propTypes
=
require
(
'prop-types'
);
const
Excerpt
=
require
(
'./excerpt'
);
const
MarkdownEditor
=
require
(
'./markdown-editor'
);
const
MarkdownView
=
require
(
'./markdown-view'
);
/**
* Display the rendered content of an annotation.
*/
function
AnnotationBody
({
collapse
,
isEditing
,
isHiddenByModerator
,
hasContent
,
onCollapsibleChanged
,
onEditText
,
onToggleCollapsed
,
text
,
})
{
return
(
<
section
className
=
"annotation-body"
>
{
!
isEditing
&&
(
<
Excerpt
collapse
=
{
collapse
}
collapsedHeight
=
{
400
}
inlineControls
=
{
false
}
onCollapsibleChanged
=
{
onCollapsibleChanged
}
onToggleCollapsed
=
{
collapsed
=>
onToggleCollapsed
({
collapsed
})}
overflowThreshold
=
{
20
}
>
<
MarkdownView
markdown
=
{
text
}
textClass
=
{{
'annotation-body is-hidden'
:
isHiddenByModerator
,
'has-content'
:
hasContent
,
}}
/
>
<
/Excerpt
>
)}
{
isEditing
&&
<
MarkdownEditor
text
=
{
text
}
onEditText
=
{
onEditText
}
/>
}
<
/section
>
);
}
AnnotationBody
.
propTypes
=
{
/**
* Whether to limit the height of the annotation body.
*
* If this is true and the intrinsic height exceeds a fixed threshold, the
* body is truncated. See `onCollapsibleChanged` and `onToggleCollapsed`.
*/
collapse
:
propTypes
.
bool
,
/**
* Whether to show moderated content, if `isHiddenByModerator` is true or
* a placeholder otherwise.
*
* This will be `true` if the current user is a moderator of the annotation's
* group. For non-moderators the content is not exposed via the API.
*/
hasContent
:
propTypes
.
bool
,
/**
* Whether to display the body in edit mode (if true) or view mode.
*/
isEditing
:
propTypes
.
bool
,
/**
* `true` if the contents of this annotation body have been redacted by
* a moderator.
*/
isHiddenByModerator
:
propTypes
.
bool
,
/**
* Callback invoked when the height of the rendered annotation body increases
* above or falls below the threshold at which the `collapse` prop will affect
* it.
*/
onCollapsibleChanged
:
propTypes
.
func
,
/**
* Callback invoked when the user edits the content of the annotation body.
*/
onEditText
:
propTypes
.
func
,
/**
* Callback invoked when the user clicks a shaded area at the bottom of a
* truncated body to indicate that they want to see the rest of the content.
*/
onToggleCollapsed
:
propTypes
.
func
,
/**
* The markdown annotation body, which is either rendered as HTML (if `isEditing`
* is false) or displayed in a text area otherwise.
*/
text
:
propTypes
.
string
,
};
module
.
exports
=
AnnotationBody
;
src/sidebar/components/annotation-quote.js
0 → 100644
View file @
855d3695
'use strict'
;
const
classnames
=
require
(
'classnames'
);
const
{
createElement
}
=
require
(
'preact'
);
const
propTypes
=
require
(
'prop-types'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
const
{
applyTheme
}
=
require
(
'../util/theme'
);
const
Excerpt
=
require
(
'./excerpt'
);
/**
* Display the selected text from the document associated with an annotation.
*/
function
AnnotationQuote
({
isOrphan
,
quote
,
settings
=
{}
})
{
return
(
<
section
className
=
{
classnames
(
'annotation-quote-list'
,
isOrphan
&&
'is-orphan'
)}
>
<
Excerpt
collapsedHeight
=
{
35
}
inlineControls
=
{
true
}
overflowThreshold
=
{
20
}
>
<
blockquote
className
=
"annotation-quote"
style
=
{
applyTheme
([
'selectionFontFamily'
],
settings
)}
>
{
quote
}
<
/blockquote
>
<
/Excerpt
>
<
/section
>
);
}
AnnotationQuote
.
propTypes
=
{
/**
* If `true`, display an indicator that the annotated text was not found in
* the current version of the document.
*/
isOrphan
:
propTypes
.
bool
,
/**
* The text that the annotation refers to. This is rendered as plain text
* (ie. HTML tags are rendered literally).
*/
quote
:
propTypes
.
string
,
// Used for theming.
settings
:
propTypes
.
object
,
};
AnnotationQuote
.
injectedProps
=
[
'settings'
];
module
.
exports
=
withServices
(
AnnotationQuote
);
src/sidebar/components/annotation.js
View file @
855d3695
...
...
@@ -562,10 +562,6 @@ function AnnotationController(
return
;
}
self
.
canCollapseBody
=
canCollapse
;
// This event handler is called from outside the digest cycle, so
// explicitly trigger a digest.
$scope
.
$digest
();
};
this
.
setText
=
function
(
text
)
{
...
...
src/sidebar/components/excerpt.js
View file @
855d3695
This diff is collapsed.
Click to expand it.
src/sidebar/components/test/annotation-body-test.js
0 → 100644
View file @
855d3695
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
{
mount
}
=
require
(
'enzyme'
);
const
AnnotationBody
=
require
(
'../annotation-body'
);
const
mockImportedComponents
=
require
(
'./mock-imported-components'
);
describe
(
'AnnotationBody'
,
()
=>
{
function
createBody
(
props
=
{})
{
return
mount
(
<
AnnotationBody
text
=
"test comment"
{...
props
}
/>
)
;
}
beforeEach
(()
=>
{
AnnotationBody
.
$imports
.
$mock
(
mockImportedComponents
());
});
afterEach
(()
=>
{
AnnotationBody
.
$imports
.
$restore
();
});
it
(
'displays the body if `isEditing` is false'
,
()
=>
{
const
wrapper
=
createBody
({
isEditing
:
false
});
assert
.
isFalse
(
wrapper
.
exists
(
'MarkdownEditor'
));
assert
.
isTrue
(
wrapper
.
exists
(
'MarkdownView'
));
});
it
(
'displays an editor if `isEditing` is true'
,
()
=>
{
const
wrapper
=
createBody
({
isEditing
:
true
});
assert
.
isTrue
(
wrapper
.
exists
(
'MarkdownEditor'
));
assert
.
isFalse
(
wrapper
.
exists
(
'MarkdownView'
));
});
});
src/sidebar/components/test/annotation-quote-test.js
0 → 100644
View file @
855d3695
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
{
mount
}
=
require
(
'enzyme'
);
const
AnnotationQuote
=
require
(
'../annotation-quote'
);
const
mockImportedComponents
=
require
(
'./mock-imported-components'
);
describe
(
'AnnotationQuote'
,
()
=>
{
function
createQuote
(
props
)
{
return
mount
(
<
AnnotationQuote
quote
=
"test quote"
settings
=
{{}}
{...
props
}
/
>
);
}
beforeEach
(()
=>
{
AnnotationQuote
.
$imports
.
$mock
(
mockImportedComponents
());
});
afterEach
(()
=>
{
AnnotationQuote
.
$imports
.
$restore
();
});
it
(
'renders the quote'
,
()
=>
{
const
wrapper
=
createQuote
();
const
quote
=
wrapper
.
find
(
'blockquote'
);
assert
.
equal
(
quote
.
text
(),
'test quote'
);
});
});
src/sidebar/components/test/annotation-test.js
View file @
855d3695
...
...
@@ -156,16 +156,22 @@ describe('annotation', function() {
onClick
:
'&'
,
},
})
.
component
(
'
markdownEditor
'
,
{
.
component
(
'
annotationBody
'
,
{
bindings
:
{
text
:
'<'
,
collapse
:
'<'
,
hasContent
:
'<'
,
isEditing
:
'<'
,
isHiddenByModerator
:
'<'
,
onCollapsibleChanged
:
'&'
,
onEditText
:
'&'
,
onToggleCollapsed
:
'&'
,
text
:
'<'
,
},
})
.
component
(
'
markdownView
'
,
{
.
component
(
'
annotationQuote
'
,
{
bindings
:
{
markdow
n
:
'<'
,
textClass
:
'<'
,
isOrpha
n
:
'<'
,
quote
:
'<'
,
},
});
});
...
...
@@ -1256,18 +1262,6 @@ describe('annotation', function() {
});
});
it
(
'renders quotes as plain text'
,
function
()
{
const
ann
=
fixtures
.
defaultAnnotation
();
ann
.
target
[
0
].
selector
=
[
{
type
:
'TextQuoteSelector'
,
exact
:
'<<-&->>'
,
},
];
const
el
=
createDirective
(
ann
).
element
;
assert
.
equal
(
el
[
0
].
querySelector
(
'blockquote'
).
textContent
,
'<<-&->>'
);
});
[
{
context
:
'for moderators'
,
...
...
@@ -1275,10 +1269,8 @@ describe('annotation', function() {
// Content still present.
text
:
'Some offensive content'
,
}),
textClass
:
{
'annotation-body is-hidden'
:
true
,
'has-content'
:
true
,
},
isHiddenByModerator
:
true
,
hasContent
:
true
,
},
{
context
:
'for non-moderators'
,
...
...
@@ -1287,18 +1279,17 @@ describe('annotation', function() {
tags
:
[],
text
:
''
,
}),
textClass
:
{
'annotation-body is-hidden'
:
true
,
'has-content'
:
false
,
},
isHiddenByModerator
:
true
,
hasContent
:
false
,
},
].
forEach
(
testCase
=>
{
it
(
`
renders hidden annotations with a custom text class (
${
testCase
.
context
}
)`
,
()
=>
{
const
el
=
createDirective
(
testCase
.
ann
).
element
;
].
forEach
(
({
ann
,
context
,
isHiddenByModerator
,
hasContent
})
=>
{
it
(
`
passes moderation status to annotation body (
${
context
}
)`
,
()
=>
{
const
el
=
createDirective
(
ann
).
element
;
assert
.
match
(
el
.
find
(
'
markdown-view'
).
controller
(
'markdownView
'
),
el
.
find
(
'
annotation-body'
).
controller
(
'annotationBody
'
),
sinon
.
match
({
textClass
:
testCase
.
textClass
,
isHiddenByModerator
,
hasContent
,
})
);
});
...
...
src/sidebar/components/test/excerpt-test.js
View file @
855d3695
This diff is collapsed.
Click to expand it.
src/sidebar/index.js
View file @
855d3695
...
...
@@ -130,6 +130,10 @@ function startAngularApp(config) {
// UI components
.
component
(
'annotation'
,
require
(
'./components/annotation'
))
.
component
(
'annotationBody'
,
wrapReactComponent
(
require
(
'./components/annotation-body'
))
)
.
component
(
'annotationHeader'
,
wrapReactComponent
(
require
(
'./components/annotation-header'
))
...
...
@@ -142,6 +146,10 @@ function startAngularApp(config) {
'annotationPublishControl'
,
wrapReactComponent
(
require
(
'./components/annotation-publish-control'
))
)
.
component
(
'annotationQuote'
,
wrapReactComponent
(
require
(
'./components/annotation-quote'
))
)
.
component
(
'annotationShareDialog'
,
require
(
'./components/annotation-share-dialog'
)
...
...
@@ -151,7 +159,6 @@ function startAngularApp(config) {
'annotationViewerContent'
,
require
(
'./components/annotation-viewer-content'
)
)
.
component
(
'excerpt'
,
require
(
'./components/excerpt'
))
.
component
(
'helpPanel'
,
wrapReactComponent
(
require
(
'./components/help-panel'
))
...
...
@@ -160,14 +167,6 @@ function startAngularApp(config) {
'loggedOutMessage'
,
wrapReactComponent
(
require
(
'./components/logged-out-message'
))
)
.
component
(
'markdownEditor'
,
wrapReactComponent
(
require
(
'./components/markdown-editor'
))
)
.
component
(
'markdownView'
,
wrapReactComponent
(
require
(
'./components/markdown-view'
))
)
.
component
(
'moderationBanner'
,
wrapReactComponent
(
require
(
'./components/moderation-banner'
))
...
...
@@ -232,7 +231,6 @@ function startAngularApp(config) {
// Utilities
.
value
(
'Discovery'
,
require
(
'../shared/discovery'
))
.
value
(
'ExcerptOverflowMonitor'
,
require
(
'./util/excerpt-overflow-monitor'
))
.
value
(
'OAuthClient'
,
require
(
'./util/oauth-client'
))
.
value
(
'VirtualThreadList'
,
require
(
'./virtual-thread-list'
))
.
value
(
'isSidebar'
,
isSidebar
)
...
...
src/sidebar/templates/annotation.html
View file @
855d3695
...
...
@@ -13,45 +13,22 @@
show-document-info=
"vm.showDocumentInfo"
>
</annotation-header>
<
!-- Excerpts -->
<section
class=
"annotation-quote-list
"
ng-class=
"{'is-orphan' : vm.isOrphan()}
"
<
annotation-quote
quote=
"vm.quote()
"
is-orphan=
"vm.isOrphan()
"
ng-if=
"vm.quote()"
>
<excerpt
collapsed-height=
"35"
inline-controls=
"true"
overflow-hysteresis=
"20"
content-data=
"selector.exact"
>
<blockquote
class=
"annotation-quote"
h-branding=
"selectionFontFamily"
ng-bind=
"vm.quote()"
></blockquote>
</excerpt>
</section>
</annotation-quote>
<!-- / Excerpts -->
<!-- Body -->
<section
name=
"text"
class=
"annotation-body"
>
<excerpt
inline-controls=
"false"
on-collapsible-changed=
"vm.setBodyCollapsible(collapsible)"
collapse=
"vm.collapseBody"
collapsed-height=
"400"
overflow-hysteresis=
"20"
content-data=
"vm.state().text"
ng-if=
"!vm.editing()"
>
<markdown-view
markdown=
"vm.state().text"
text-class=
"{'annotation-body is-hidden':vm.isHiddenByModerator(),
'has-content':vm.hasContent()}"
>
</markdown-view>
</excerpt>
<markdown-editor
text=
"vm.state().text"
on-edit-text=
"vm.setText(text)"
ng-if=
"vm.editing()"
>
</markdown-editor>
</section>
<!-- / Body -->
<annotation-body
collapse=
"vm.collapseBody"
has-content=
"vm.hasContent()"
is-editing=
"vm.editing()"
is-hidden-by-moderator=
"vm.isHiddenByModerator()"
on-collapsible-changed=
"vm.setBodyCollapsible(collapsible)"
on-edit-text=
"vm.setText(text)"
on-toggle-collapsed=
"vm.collapseBody = collapsed"
text=
"vm.state().text"
>
</annotation-body>
<!-- Tags -->
<div
class=
"annotation-body form-field"
ng-if=
"vm.editing()"
>
...
...
src/sidebar/templates/excerpt.html
deleted
100644 → 0
View file @
804c0177
<div
class=
"excerpt__container"
>
<div
class=
"excerpt"
ng-style=
"vm.contentStyle()"
>
<div
ng-transclude
></div>
<div
ng-click=
"vm.expand()"
ng-class=
"vm.bottomShadowStyles()"
title=
"Show the full excerpt"
></div>
<div
class=
"excerpt__inline-controls"
ng-show=
"vm.showInlineControls()"
>
<span
class=
"excerpt__toggle-link"
ng-show=
"vm.isExpandable()"
>
…
<a
ng-click=
"vm.toggle($event)"
title=
"Show the full excerpt"
h-branding=
"accentColor, selectionFontFamily"
>
More
</a>
</span>
<span
class=
"excerpt__toggle-link"
ng-show=
"vm.isCollapsible()"
>
<a
ng-click=
"vm.toggle($event)"
title=
"Show the first few lines only"
h-branding=
"accentColor, selectionFontFamily"
>
Less
</a>
</span>
</div>
</div>
</div>
src/sidebar/util/excerpt-overflow-monitor.js
deleted
100644 → 0
View file @
804c0177
'use strict'
;
function
toPx
(
val
)
{
return
val
.
toString
()
+
'px'
;
}
/**
* Interface used by ExcerptOverflowMonitor to retrieve the state of the
* <excerpt> and report when the state changes.
*
* interface Excerpt {
* getState(): State;
* contentHeight(): number | undefined;
* onOverflowChanged(): void;
* }
*/
/**
* A helper for the <excerpt> component which handles determinination of the
* overflow state and content styling given the current state of the component
* and the height of its contents.
*
* When the state of the excerpt or its content changes, the component should
* call check() to schedule an async update of the overflow state.
*
* @param {Excerpt} excerpt - Interface used to query the current state of the
* excerpt and notify it when the overflow state changes.
* @param {(callback) => number} requestAnimationFrame -
* Function called to schedule an async recalculation of the overflow
* state.
*/
function
ExcerptOverflowMonitor
(
excerpt
,
requestAnimationFrame
)
{
let
pendingUpdate
=
false
;
// Last-calculated overflow state
let
prevOverflowing
;
function
update
()
{
const
state
=
excerpt
.
getState
();
if
(
!
pendingUpdate
)
{
return
;
}
pendingUpdate
=
false
;
const
hysteresisPx
=
state
.
overflowHysteresis
||
0
;
const
overflowing
=
excerpt
.
contentHeight
()
>
state
.
collapsedHeight
+
hysteresisPx
;
if
(
overflowing
===
prevOverflowing
)
{
return
;
}
prevOverflowing
=
overflowing
;
excerpt
.
onOverflowChanged
(
overflowing
);
}
/**
* Schedule a deferred check of whether the content is collapsed.
*/
function
check
()
{
if
(
pendingUpdate
)
{
return
;
}
pendingUpdate
=
true
;
requestAnimationFrame
(
update
);
}
/**
* Returns an object mapping CSS properties to values that should be applied
* to an excerpt's content element in order to truncate it based on the
* current overflow state.
*/
function
contentStyle
()
{
const
state
=
excerpt
.
getState
();
let
maxHeight
=
''
;
if
(
prevOverflowing
)
{
if
(
state
.
collapse
)
{
maxHeight
=
toPx
(
state
.
collapsedHeight
);
}
else
if
(
state
.
animate
)
{
// Animating the height change requires that the final
// height be specified exactly, rather than relying on
// auto height
maxHeight
=
toPx
(
excerpt
.
contentHeight
());
}
}
else
if
(
typeof
prevOverflowing
===
'undefined'
&&
state
.
collapse
)
{
// If the excerpt is collapsed but the overflowing state has not yet
// been computed then the exact max height is unknown, but it will be
// in the range [state.collapsedHeight, state.collapsedHeight +
// state.overflowHysteresis]
//
// Here we guess that the final content height is most likely to be
// either less than `collapsedHeight` or more than `collapsedHeight` +
// `overflowHysteresis`, in which case it will be truncated to
// `collapsedHeight`.
maxHeight
=
toPx
(
state
.
collapsedHeight
);
}
return
{
'max-height'
:
maxHeight
,
};
}
this
.
contentStyle
=
contentStyle
;
this
.
check
=
check
;
}
module
.
exports
=
ExcerptOverflowMonitor
;
src/sidebar/util/test/excerpt-overflow-monitor-test.js
deleted
100644 → 0
View file @
804c0177
'use strict'
;
const
ExcerptOverflowMonitor
=
require
(
'../excerpt-overflow-monitor'
);
describe
(
'ExcerptOverflowMonitor'
,
function
()
{
let
contentHeight
;
let
ctrl
;
let
monitor
;
let
state
;
beforeEach
(
function
()
{
contentHeight
=
0
;
state
=
{
animate
:
true
,
collapsedHeight
:
100
,
collapse
:
true
,
overflowHysteresis
:
20
,
};
ctrl
=
{
getState
:
function
()
{
return
state
;
},
contentHeight
:
function
()
{
return
contentHeight
;
},
onOverflowChanged
:
sinon
.
stub
(),
};
monitor
=
new
ExcerptOverflowMonitor
(
ctrl
,
function
(
callback
)
{
callback
();
});
});
describe
(
'overflow state'
,
function
()
{
it
(
'overflows if height > collaped height + hysteresis'
,
function
()
{
contentHeight
=
200
;
monitor
.
check
();
assert
.
calledWith
(
ctrl
.
onOverflowChanged
,
true
);
});
it
(
'does not overflow if height < collapsed height'
,
function
()
{
contentHeight
=
80
;
monitor
.
check
();
assert
.
calledWith
(
ctrl
.
onOverflowChanged
,
false
);
});
it
(
'does not overflow if height is in [collapsed height, collapsed height + hysteresis]'
,
function
()
{
contentHeight
=
110
;
monitor
.
check
();
assert
.
calledWith
(
ctrl
.
onOverflowChanged
,
false
);
});
});
context
(
'#contentStyle'
,
function
()
{
it
(
'sets max-height if collapsed and overflowing'
,
function
()
{
contentHeight
=
200
;
monitor
.
check
();
assert
.
deepEqual
(
monitor
.
contentStyle
(),
{
'max-height'
:
'100px'
});
});
it
(
'sets max height to empty if not overflowing'
,
function
()
{
contentHeight
=
80
;
monitor
.
check
();
assert
.
deepEqual
(
monitor
.
contentStyle
(),
{
'max-height'
:
''
});
});
it
(
'sets max-height if overflow state is unknown'
,
function
()
{
// Before the initial overflow check, the state is unknown
assert
.
deepEqual
(
monitor
.
contentStyle
(),
{
'max-height'
:
'100px'
});
});
});
context
(
'#check'
,
function
()
{
it
(
'calls onOverflowChanged() if state changed'
,
function
()
{
contentHeight
=
200
;
monitor
.
check
();
ctrl
.
onOverflowChanged
=
sinon
.
stub
();
contentHeight
=
250
;
monitor
.
check
();
assert
.
notCalled
(
ctrl
.
onOverflowChanged
);
});
});
});
src/styles/sidebar/components/excerpt.scss
View file @
855d3695
...
...
@@ -5,6 +5,7 @@
.excerpt
{
transition
:
max-height
$expand-duration
ease-in
;
overflow
:
hidden
;
position
:
relative
;
}
// a container which wraps the <excerpt> and contains the excerpt
...
...
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