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
89bc4044
Commit
89bc4044
authored
Nov 04, 2019
by
Lyza Danger Gardner
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Reimplement Help/Tutorial panel
parent
7953c6a4
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
561 additions
and
238 deletions
+561
-238
help-panel.js
src/sidebar/components/help-panel.js
+148
-31
hypothesis-app.js
src/sidebar/components/hypothesis-app.js
+20
-17
help-panel-test.js
src/sidebar/components/test/help-panel-test.js
+181
-29
hypothesis-app-test.js
src/sidebar/components/test/hypothesis-app-test.js
+36
-77
top-bar-test.js
src/sidebar/components/test/top-bar-test.js
+49
-17
top-bar.js
src/sidebar/components/top-bar.js
+49
-23
index.js
src/sidebar/index.js
+2
-0
hypothesis-app.html
src/sidebar/templates/hypothesis-app.html
+1
-6
ui-constants.js
src/sidebar/ui-constants.js
+1
-0
help-panel.scss
src/styles/sidebar/components/help-panel.scss
+62
-36
sidebar-panel.scss
src/styles/sidebar/components/sidebar-panel.scss
+12
-2
No files found.
src/sidebar/components/help-panel.js
View file @
89bc4044
'use strict'
;
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
{
useCallback
,
useMemo
,
useState
}
=
require
(
'preact/hooks'
);
const
propTypes
=
require
(
'prop-types'
);
const
uiConstants
=
require
(
'../ui-constants'
);
const
useStore
=
require
(
'../store/use-store'
);
const
VersionData
=
require
(
'../util/version-data'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
const
SidebarPanel
=
require
(
'./sidebar-panel'
);
const
SvgIcon
=
require
(
'./svg-icon'
);
const
Tutorial
=
require
(
'./tutorial'
);
const
VersionInfo
=
require
(
'./version-info'
);
/**
/**
* @ngdoc directive
* External link "tabs" inside of the help panel.
* @name helpPanel
* @description Displays product version and environment info
*/
*/
// @ngInject
function
HelpPanelTab
({
linkText
,
url
})
{
module
.
exports
=
{
return
(
controllerAs
:
'vm'
,
<
div
className
=
"help-panel-tabs__tab"
>
// @ngInject
<
a
controller
:
function
(
$scope
,
$window
,
store
,
serviceUrl
)
{
href
=
{
url
}
this
.
userAgent
=
$window
.
navigator
.
userAgent
;
className
=
"help-panel-tabs__link"
this
.
version
=
'__VERSION__'
;
// replaced by versionify
target
=
"_blank"
this
.
dateTime
=
new
Date
();
rel
=
"noopener noreferrer"
this
.
serviceUrl
=
serviceUrl
;
>
{
linkText
}{
' '
}
$scope
.
$watch
(
<
SvgIcon
function
()
{
name
=
"external"
return
store
.
frames
();
className
=
"help-panel-tabs__icon"
},
inline
=
{
true
}
function
(
frames
)
{
/
>
if
(
frames
.
length
===
0
)
{
<
/a
>
return
;
<
/div
>
}
);
this
.
url
=
frames
[
0
].
uri
;
}
this
.
documentFingerprint
=
frames
[
0
].
metadata
.
documentFingerprint
;
}.
bind
(
this
)
HelpPanelTab
.
propTypes
=
{
);
/* What the tab's link should say */
},
linkText
:
propTypes
.
string
.
isRequired
,
template
:
require
(
'../templates/help-panel.html'
),
/* Where the tab's link should go */
bindings
:
{
url
:
propTypes
.
string
.
isRequired
,
auth
:
'<'
,
};
onClose
:
'&'
,
},
/**
* A help sidebar panel with two sub-panels: tutorial and version info.
*/
function
HelpPanel
({
auth
,
session
})
{
const
mainFrame
=
useStore
(
store
=>
store
.
mainFrame
());
// Should this panel be auto-opened at app launch? Note that the actual
// auto-open triggering of this panel is owned by the `hypothesis-app` component.
// This reference is such that we know whether we should "dismiss" the tutorial
// (permanently for this user) when it is closed.
const
hasAutoDisplayPreference
=
useStore
(
store
=>
!!
store
.
getState
().
session
.
preferences
.
show_sidebar_tutorial
);
// The "Tutorial" (getting started) subpanel is the default panel shown
const
[
activeSubPanel
,
setActiveSubPanel
]
=
useState
(
'tutorial'
);
// Build version details about this session/app
const
versionData
=
useMemo
(()
=>
{
const
userInfo
=
auth
||
{};
const
documentInfo
=
mainFrame
||
{};
return
new
VersionData
(
userInfo
,
documentInfo
);
},
[
auth
,
mainFrame
]);
// The support ticket URL encodes some version info in it to pre-fill in the
// create-new-ticket form
const
supportTicketURL
=
`https://web.hypothes.is/get-help/?sys_info=
${
versionData
.
asEncodedURLString
()}
`
;
const
subPanelTitles
=
{
tutorial
:
'Getting started'
,
versionInfo
:
'About this version'
,
};
const
openSubPanel
=
(
e
,
panelName
)
=>
{
e
.
preventDefault
();
setActiveSubPanel
(
panelName
);
};
const
dismissFn
=
session
.
dismissSidebarTutorial
;
// Reference for useCallback dependency
const
onActiveChanged
=
useCallback
(
active
=>
{
if
(
!
active
&&
hasAutoDisplayPreference
)
{
// If the tutorial is currently being auto-displayed, update the user
// preference to disable the auto-display from happening on subsequent
// app launches
dismissFn
();
}
},
[
dismissFn
,
hasAutoDisplayPreference
]
);
return
(
<
SidebarPanel
title
=
"Need some help?"
panelName
=
{
uiConstants
.
PANEL_HELP
}
onActiveChanged
=
{
onActiveChanged
}
>
<
h3
className
=
"help-panel__sub-panel-title"
>
{
subPanelTitles
[
activeSubPanel
]}
<
/h3
>
<
div
className
=
"help-panel__content"
>
{
activeSubPanel
===
'tutorial'
&&
<
Tutorial
/>
}
{
activeSubPanel
===
'versionInfo'
&&
(
<
VersionInfo
versionData
=
{
versionData
}
/
>
)}
<
div
className
=
"help-panel__footer"
>
{
activeSubPanel
===
'versionInfo'
&&
(
<
a
href
=
"#"
className
=
"help-panel__sub-panel-link help-panel__sub-panel-link--left"
onClick
=
{
e
=>
openSubPanel
(
e
,
'tutorial'
)}
>
<
SvgIcon
name
=
"arrow-left"
className
=
"help-panel__icon"
/>
<
div
>
Getting
started
<
/div
>
<
/a
>
)}
{
activeSubPanel
===
'tutorial'
&&
(
<
a
href
=
"#"
className
=
"help-panel__sub-panel-link help-panel__sub-panel-link--right"
onClick
=
{
e
=>
openSubPanel
(
e
,
'versionInfo'
)}
>
<
div
>
About
this
version
<
/div
>
<
SvgIcon
name
=
"arrow-right"
className
=
"help-panel__icon"
/>
<
/a
>
)}
<
/div
>
<
/div
>
<
div
className
=
"help-panel-tabs"
>
<
HelpPanelTab
linkText
=
"Help topics"
url
=
"https://web.hypothes.is/help/"
/>
<
HelpPanelTab
linkText
=
"New support ticket"
url
=
{
supportTicketURL
}
/
>
<
/div
>
<
/SidebarPanel
>
);
}
HelpPanel
.
propTypes
=
{
/* Object with auth and user information */
auth
:
propTypes
.
object
.
isRequired
,
session
:
propTypes
.
object
.
isRequired
,
};
};
HelpPanel
.
injectedProps
=
[
'session'
];
module
.
exports
=
withServices
(
HelpPanel
);
src/sidebar/components/hypothesis-app.js
View file @
89bc4044
...
@@ -4,6 +4,9 @@ const events = require('../events');
...
@@ -4,6 +4,9 @@ const events = require('../events');
const
{
parseAccountID
}
=
require
(
'../util/account-id'
);
const
{
parseAccountID
}
=
require
(
'../util/account-id'
);
const
serviceConfig
=
require
(
'../service-config'
);
const
serviceConfig
=
require
(
'../service-config'
);
const
bridgeEvents
=
require
(
'../../shared/bridge-events'
);
const
bridgeEvents
=
require
(
'../../shared/bridge-events'
);
const
uiConstants
=
require
(
'../ui-constants'
);
const
isSidebar
=
require
(
'../util/is-sidebar'
);
const
{
shouldAutoDisplayTutorial
}
=
require
(
'../util/session-util'
);
/**
/**
* Return the user's authentication status from their profile.
* Return the user's authentication status from their profile.
...
@@ -56,23 +59,34 @@ function HypothesisAppController(
...
@@ -56,23 +59,34 @@ function HypothesisAppController(
// used by templates to show an intermediate or loading state.
// used by templates to show an intermediate or loading state.
this
.
auth
=
{
status
:
'unknown'
};
this
.
auth
=
{
status
:
'unknown'
};
// App dialogs
this
.
helpPanel
=
{
visible
:
false
};
// Check to see if we're in the sidebar, or on a standalone page such as
// Check to see if we're in the sidebar, or on a standalone page such as
// the stream page or an individual annotation page.
// the stream page or an individual annotation page.
this
.
isSidebar
=
$window
.
top
!==
$window
;
this
.
isSidebar
=
isSidebar
()
;
if
(
this
.
isSidebar
)
{
if
(
this
.
isSidebar
)
{
frameSync
.
connect
();
frameSync
.
connect
();
}
}
// Reload the view when the user switches accounts
// Reload the view when the user switches accounts
this
.
onUserChange
=
profile
=>
{
self
.
auth
=
authStateFromProfile
(
profile
);
if
(
shouldAutoDisplayTutorial
(
this
.
isSidebar
,
store
.
getState
().
session
,
settings
)
)
{
// Auto-open the tutorial (help) panel
store
.
openSidebarPanel
(
uiConstants
.
PANEL_HELP
);
}
};
$scope
.
$on
(
events
.
USER_CHANGED
,
function
(
event
,
data
)
{
$scope
.
$on
(
events
.
USER_CHANGED
,
function
(
event
,
data
)
{
self
.
auth
=
authStateFromProfil
e
(
data
.
profile
);
self
.
onUserChang
e
(
data
.
profile
);
});
});
session
.
load
().
then
(
profile
=>
{
session
.
load
().
then
(
profile
=>
{
self
.
auth
=
authStateFromProfil
e
(
profile
);
self
.
onUserChang
e
(
profile
);
});
});
/**
/**
...
@@ -110,17 +124,6 @@ function HypothesisAppController(
...
@@ -110,17 +124,6 @@ function HypothesisAppController(
$window
.
open
(
serviceUrl
(
'signup'
));
$window
.
open
(
serviceUrl
(
'signup'
));
};
};
this
.
showHelpPanel
=
function
()
{
const
service
=
serviceConfig
(
settings
)
||
{};
if
(
service
.
onHelpRequestProvided
)
{
// Let the host page handle the help request.
bridge
.
call
(
bridgeEvents
.
HELP_REQUESTED
);
return
;
}
this
.
helpPanel
.
visible
=
true
;
};
// Prompt to discard any unsaved drafts.
// Prompt to discard any unsaved drafts.
const
promptToLogout
=
function
()
{
const
promptToLogout
=
function
()
{
// TODO - Replace this with a UI which doesn't look terrible.
// TODO - Replace this with a UI which doesn't look terrible.
...
...
src/sidebar/components/test/help-panel-test.js
View file @
89bc4044
'use strict'
;
'use strict'
;
const
angular
=
require
(
'angular'
);
const
{
mount
}
=
require
(
'enzyme'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
act
}
=
require
(
'preact/test-utils'
);
describe
(
'helpPanel'
,
function
()
{
const
HelpPanel
=
require
(
'../help-panel'
);
const
mockImportedComponents
=
require
(
'./mock-imported-components'
);
describe
(
'HelpPanel'
,
function
()
{
let
fakeAuth
;
let
fakeSessionService
;
let
fakeStore
;
let
fakeStore
;
let
$componentController
;
let
fakeVersionData
;
let
$rootScope
;
let
fakeVersionDataObject
;
function
createComponent
(
props
)
{
return
mount
(
<
HelpPanel
auth
=
{
fakeAuth
}
session
=
{
fakeSessionService
}
{...
props
}
/
>
);
}
beforeEach
(
function
()
{
beforeEach
(()
=>
{
fakeAuth
=
{};
fakeSessionService
=
{
dismissSidebarTutorial
:
sinon
.
stub
()
};
fakeStore
=
{
fakeStore
=
{
frames
:
sinon
.
stub
().
returns
([]),
getState
:
sinon
.
stub
()
.
returns
({
session
:
{
preferences
:
{
show_sidebar_tutorial
:
true
}
}
}),
mainFrame
:
sinon
.
stub
().
returns
(
null
),
};
fakeVersionDataObject
=
{
asEncodedURLString
:
sinon
.
stub
().
returns
(
'fakeURLString'
),
};
};
fakeVersionData
=
sinon
.
stub
().
returns
(
fakeVersionDataObject
);
HelpPanel
.
$imports
.
$mock
(
mockImportedComponents
());
HelpPanel
.
$imports
.
$mock
({
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
'../util/version-data'
:
fakeVersionData
,
});
});
afterEach
(()
=>
{
HelpPanel
.
$imports
.
$restore
();
});
context
(
'when viewing tutorial sub-panel'
,
()
=>
{
it
(
'should show tutorial by default'
,
()
=>
{
const
wrapper
=
createComponent
();
const
subHeader
=
wrapper
.
find
(
'.help-panel__sub-panel-title'
);
assert
.
equal
(
subHeader
.
text
(),
'Getting started'
);
assert
.
isTrue
(
wrapper
.
find
(
'Tutorial'
).
exists
());
assert
.
isFalse
(
wrapper
.
find
(
'VersionInfo'
).
exists
());
});
it
(
'should show navigation link to versionInfo sub-panel'
,
()
=>
{
const
wrapper
=
createComponent
();
const
link
=
wrapper
.
find
(
'.help-panel__sub-panel-link'
);
assert
.
equal
(
link
.
text
(),
'About this version'
);
});
it
(
'should switch to versionInfo sub-panel when footer link clicked'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'.help-panel__sub-panel-link'
).
simulate
(
'click'
);
assert
.
equal
(
wrapper
.
find
(
'.help-panel__sub-panel-title'
).
text
(),
'About this version'
);
assert
.
isTrue
(
wrapper
.
find
(
'VersionInfo'
).
exists
());
assert
.
equal
(
wrapper
.
find
(
'VersionInfo'
).
prop
(
'versionData'
),
fakeVersionDataObject
);
assert
.
isFalse
(
wrapper
.
find
(
'Tutorial'
).
exists
());
});
});
context
(
'when viewing versionInfo sub-panel'
,
()
=>
{
it
(
'should show navigation link back to tutorial sub-panel'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'.help-panel__sub-panel-link'
).
simulate
(
'click'
);
const
link
=
wrapper
.
find
(
'.help-panel__sub-panel-link'
);
angular
.
module
(
'h'
,
[]).
component
(
'helpPanel'
,
require
(
'../help-panel'
));
assert
.
isTrue
(
wrapper
.
find
(
'VersionInfo'
).
exists
());
assert
.
isFalse
(
wrapper
.
find
(
'Tutorial'
).
exists
());
assert
.
equal
(
link
.
text
(),
'Getting started'
);
});
it
(
'should switch to tutorial sub-panel when link clicked'
,
()
=>
{
const
wrapper
=
createComponent
();
// Click to get to version-info sub-panel...
wrapper
.
find
(
'.help-panel__sub-panel-link'
).
simulate
(
'click'
);
const
link
=
wrapper
.
find
(
'.help-panel__sub-panel-link'
);
// Click again to get back to tutorial sub-panel
link
.
simulate
(
'click'
);
assert
.
isFalse
(
wrapper
.
find
(
'VersionInfo'
).
exists
());
assert
.
isTrue
(
wrapper
.
find
(
'Tutorial'
).
exists
());
});
});
describe
(
'`HelpPanelTab`s'
,
()
=>
{
it
(
'should render static link to knowledge base'
,
()
=>
{
const
wrapper
=
createComponent
();
angular
.
mock
.
module
(
'h'
,
{
assert
.
isTrue
(
store
:
fakeStore
,
wrapper
serviceUrl
:
sinon
.
stub
(),
.
find
(
'HelpPanelTab'
)
.
filter
({
linkText
:
'Help topics'
})
.
exists
()
);
});
});
angular
.
mock
.
inject
(
function
(
_$componentController_
,
_$rootScope_
)
{
it
(
'should render dynamic link to create a new help ticket'
,
()
=>
{
$componentController
=
_$componentController_
;
const
wrapper
=
createComponent
();
$rootScope
=
_$rootScope_
;
const
helpTab
=
wrapper
.
find
(
'HelpPanelTab'
)
.
filter
({
linkText
:
'New support ticket'
});
assert
.
isTrue
(
helpTab
.
exists
());
assert
.
include
(
helpTab
.
prop
(
'url'
),
'fakeURLString'
);
});
});
});
});
it
(
'displays the URL and fingerprint of the first connected frame'
,
function
()
{
context
(
'dismissing the tutorial and clearing profile setting'
,
()
=>
{
fakeStore
.
frames
.
returns
([
context
(
'profile preference to auto-show tutorial is truthy'
,
()
=>
{
{
beforeEach
(()
=>
{
uri
:
'https://publisher.org/article.pdf'
,
fakeStore
.
getState
.
returns
({
metadata
:
{
session
:
{
preferences
:
{
show_sidebar_tutorial
:
true
}
},
documentFingerprint
:
'12345'
,
});
},
});
},
]);
it
(
'should not dismiss the panel when it is initially opened'
,
()
=>
{
const
wrapper
=
createComponent
();
const
$scope
=
$rootScope
.
$new
();
const
onActiveChanged
=
wrapper
const
ctrl
=
$componentController
(
'helpPanel'
,
{
$scope
:
$scope
});
.
find
(
'SidebarPanel'
)
$scope
.
$digest
();
.
prop
(
'onActiveChanged'
);
assert
.
equal
(
ctrl
.
url
,
'https://publisher.org/article.pdf'
);
act
(()
=>
{
assert
.
equal
(
ctrl
.
documentFingerprint
,
'12345'
);
// "Activate" the panel (simulate the `SidebarPanel` communicating
// an active state via callback prop)
onActiveChanged
(
true
);
});
assert
.
notOk
(
fakeSessionService
.
dismissSidebarTutorial
.
callCount
);
});
it
(
'should invoke dismiss service method when panel is first closed'
,
()
=>
{
const
wrapper
=
createComponent
();
const
onActiveChanged
=
wrapper
.
find
(
'SidebarPanel'
)
.
prop
(
'onActiveChanged'
);
act
(()
=>
{
// "Activate" the panel (simulate the `SidebarPanel` communicating
// an active state via callback prop)
onActiveChanged
(
true
);
// Now "close" the panel
onActiveChanged
(
false
);
});
assert
.
calledOnce
(
fakeSessionService
.
dismissSidebarTutorial
);
});
});
context
(
'profile preference to auto-show tutorial is falsy'
,
()
=>
{
beforeEach
(()
=>
{
fakeStore
.
getState
.
returns
({
session
:
{
preferences
:
{
show_sidebar_tutorial
:
false
}
},
});
});
it
(
'should not invoke dismiss service method when panel is closed'
,
()
=>
{
const
wrapper
=
createComponent
();
const
onActiveChanged
=
wrapper
.
find
(
'SidebarPanel'
)
.
prop
(
'onActiveChanged'
);
act
(()
=>
{
// "Activate" the panel (simulate the `SidebarPanel` communicating
// an active state via callback prop)
onActiveChanged
(
true
);
// Now "close" the panel
onActiveChanged
(
false
);
});
assert
.
notOk
(
fakeSessionService
.
dismissSidebarTutorial
.
callCount
);
});
});
});
});
});
});
src/sidebar/components/test/hypothesis-app-test.js
View file @
89bc4044
...
@@ -18,9 +18,11 @@ describe('sidebar.components.hypothesis-app', function() {
...
@@ -18,9 +18,11 @@ describe('sidebar.components.hypothesis-app', function() {
let
fakeFeatures
=
null
;
let
fakeFeatures
=
null
;
let
fakeFlash
=
null
;
let
fakeFlash
=
null
;
let
fakeFrameSync
=
null
;
let
fakeFrameSync
=
null
;
let
fakeIsSidebar
=
null
;
let
fakeParams
=
null
;
let
fakeParams
=
null
;
let
fakeServiceConfig
=
null
;
let
fakeServiceConfig
=
null
;
let
fakeSession
=
null
;
let
fakeSession
=
null
;
let
fakeShouldAutoDisplayTutorial
=
null
;
let
fakeGroups
=
null
;
let
fakeGroups
=
null
;
let
fakeRoute
=
null
;
let
fakeRoute
=
null
;
let
fakeServiceUrl
=
null
;
let
fakeServiceUrl
=
null
;
...
@@ -40,10 +42,16 @@ describe('sidebar.components.hypothesis-app', function() {
...
@@ -40,10 +42,16 @@ describe('sidebar.components.hypothesis-app', function() {
});
});
beforeEach
(
function
()
{
beforeEach
(
function
()
{
fakeIsSidebar
=
sandbox
.
stub
().
returns
(
true
);
fakeServiceConfig
=
sandbox
.
stub
();
fakeServiceConfig
=
sandbox
.
stub
();
fakeShouldAutoDisplayTutorial
=
sinon
.
stub
().
returns
(
false
);
hypothesisApp
.
$imports
.
$mock
({
hypothesisApp
.
$imports
.
$mock
({
'../util/is-sidebar'
:
fakeIsSidebar
,
'../service-config'
:
fakeServiceConfig
,
'../service-config'
:
fakeServiceConfig
,
'../util/session-util'
:
{
shouldAutoDisplayTutorial
:
fakeShouldAutoDisplayTutorial
,
},
});
});
angular
.
module
(
'h'
,
[]).
component
(
'hypothesisApp'
,
hypothesisApp
);
angular
.
module
(
'h'
,
[]).
component
(
'hypothesisApp'
,
hypothesisApp
);
...
@@ -60,8 +68,15 @@ describe('sidebar.components.hypothesis-app', function() {
...
@@ -60,8 +68,15 @@ describe('sidebar.components.hypothesis-app', function() {
fakeStore
=
{
fakeStore
=
{
tool
:
'comment'
,
tool
:
'comment'
,
clearSelectedAnnotations
:
sandbox
.
spy
(),
clearSelectedAnnotations
:
sandbox
.
spy
(),
getState
:
sinon
.
stub
(),
getState
:
sinon
.
stub
().
returns
({
session
:
{
preferences
:
{
show_sidebar_tutorial
:
false
,
},
},
}),
clearGroups
:
sinon
.
stub
(),
clearGroups
:
sinon
.
stub
(),
openSidebarPanel
:
sinon
.
stub
(),
// draft store
// draft store
countDrafts
:
sandbox
.
stub
().
returns
(
0
),
countDrafts
:
sandbox
.
stub
().
returns
(
0
),
discardAllDrafts
:
sandbox
.
stub
(),
discardAllDrafts
:
sandbox
.
stub
(),
...
@@ -143,32 +158,36 @@ describe('sidebar.components.hypothesis-app', function() {
...
@@ -143,32 +158,36 @@ describe('sidebar.components.hypothesis-app', function() {
sandbox
.
restore
();
sandbox
.
restore
();
});
});
describe
(
'#isSidebar'
,
function
()
{
it
(
'is false if the window is the top window'
,
function
()
{
fakeWindow
.
top
=
fakeWindow
;
const
ctrl
=
createController
();
assert
.
isFalse
(
ctrl
.
isSidebar
);
});
it
(
'is true if the window is not the top window'
,
function
()
{
fakeWindow
.
top
=
{};
const
ctrl
=
createController
();
assert
.
isTrue
(
ctrl
.
isSidebar
);
});
});
it
(
'connects to host frame in the sidebar app'
,
function
()
{
it
(
'connects to host frame in the sidebar app'
,
function
()
{
fake
Window
.
top
=
{}
;
fake
IsSidebar
.
returns
(
true
)
;
createController
();
createController
();
assert
.
called
(
fakeFrameSync
.
connect
);
assert
.
called
(
fakeFrameSync
.
connect
);
});
});
it
(
'does not connect to the host frame in the stream'
,
function
()
{
it
(
'does not connect to the host frame in the stream'
,
function
()
{
fake
Window
.
top
=
fakeWindow
;
fake
IsSidebar
.
returns
(
false
)
;
createController
();
createController
();
assert
.
notCalled
(
fakeFrameSync
.
connect
);
assert
.
notCalled
(
fakeFrameSync
.
connect
);
});
});
describe
(
'auto-opening tutorial'
,
()
=>
{
it
(
'should open tutorial on profile load when criteria are met'
,
()
=>
{
fakeShouldAutoDisplayTutorial
.
returns
(
true
);
createController
();
return
fakeSession
.
load
().
then
(()
=>
{
assert
.
calledOnce
(
fakeStore
.
openSidebarPanel
);
});
});
it
(
'should not open tutorial on profile load when criteria are not met'
,
()
=>
{
fakeShouldAutoDisplayTutorial
.
returns
(
false
);
createController
();
return
fakeSession
.
load
().
then
(()
=>
{
assert
.
equal
(
fakeStore
.
openSidebarPanel
.
callCount
,
0
);
});
});
});
it
(
'auth.status is "unknown" on startup'
,
function
()
{
it
(
'auth.status is "unknown" on startup'
,
function
()
{
const
ctrl
=
createController
();
const
ctrl
=
createController
();
assert
.
equal
(
ctrl
.
auth
.
status
,
'unknown'
);
assert
.
equal
(
ctrl
.
auth
.
status
,
'unknown'
);
...
@@ -290,66 +309,6 @@ describe('sidebar.components.hypothesis-app', function() {
...
@@ -290,66 +309,6 @@ describe('sidebar.components.hypothesis-app', function() {
});
});
});
});
describe
(
'#showHelpPanel'
,
function
()
{
context
(
'when using a third-party service'
,
function
()
{
context
(
"when there's no onHelpRequest callback function"
,
function
()
{
beforeEach
(
'configure a service with no onHelpRequest'
,
function
()
{
fakeServiceConfig
.
returns
({});
});
it
(
'does not send an event'
,
function
()
{
createController
().
showHelpPanel
();
assert
.
notCalled
(
fakeBridge
.
call
);
});
it
(
'shows the help panel'
,
function
()
{
const
ctrl
=
createController
();
ctrl
.
showHelpPanel
();
assert
.
isTrue
(
ctrl
.
helpPanel
.
visible
);
});
});
context
(
"when there's an onHelpRequest callback function"
,
function
()
{
beforeEach
(
'provide an onHelpRequest callback'
,
function
()
{
fakeServiceConfig
.
returns
({
onHelpRequestProvided
:
true
});
});
it
(
'sends the HELP_REQUESTED event'
,
function
()
{
createController
().
showHelpPanel
();
assert
.
calledWith
(
fakeBridge
.
call
,
bridgeEvents
.
HELP_REQUESTED
);
});
it
(
'does not show the help panel'
,
function
()
{
const
ctrl
=
createController
();
ctrl
.
showHelpPanel
();
assert
.
isFalse
(
ctrl
.
helpPanel
.
visible
);
});
});
});
context
(
'when not using a third-party service'
,
function
()
{
it
(
'does not send an event'
,
function
()
{
createController
().
showHelpPanel
();
assert
.
notCalled
(
fakeBridge
.
call
);
});
it
(
'shows the help panel'
,
function
()
{
const
ctrl
=
createController
();
ctrl
.
showHelpPanel
();
assert
.
isTrue
(
ctrl
.
helpPanel
.
visible
);
});
});
});
describe
(
'#login()'
,
function
()
{
describe
(
'#login()'
,
function
()
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
fakeAuth
.
login
=
sinon
.
stub
().
returns
(
Promise
.
resolve
());
fakeAuth
.
login
=
sinon
.
stub
().
returns
(
Promise
.
resolve
());
...
...
src/sidebar/components/test/top-bar-test.js
View file @
89bc4044
...
@@ -4,15 +4,18 @@ const { createElement } = require('preact');
...
@@ -4,15 +4,18 @@ const { createElement } = require('preact');
const
{
mount
}
=
require
(
'enzyme'
);
const
{
mount
}
=
require
(
'enzyme'
);
const
uiConstants
=
require
(
'../../ui-constants'
);
const
uiConstants
=
require
(
'../../ui-constants'
);
const
bridgeEvents
=
require
(
'../../../shared/bridge-events'
);
const
TopBar
=
require
(
'../top-bar'
);
const
TopBar
=
require
(
'../top-bar'
);
const
mockImportedComponents
=
require
(
'./mock-imported-components'
);
const
mockImportedComponents
=
require
(
'./mock-imported-components'
);
describe
(
'TopBar'
,
()
=>
{
describe
(
'TopBar'
,
()
=>
{
const
fakeSettings
=
{};
const
fakeSettings
=
{};
let
fakeBridge
;
let
fakeStore
;
let
fakeStore
;
let
fakeStreamer
;
let
fakeStreamer
;
let
fakeIsThirdPartyService
;
let
fakeIsThirdPartyService
;
let
fakeServiceConfig
;
beforeEach
(()
=>
{
beforeEach
(()
=>
{
fakeIsThirdPartyService
=
sinon
.
stub
().
returns
(
false
);
fakeIsThirdPartyService
=
sinon
.
stub
().
returns
(
false
);
...
@@ -29,6 +32,12 @@ describe('TopBar', () => {
...
@@ -29,6 +32,12 @@ describe('TopBar', () => {
toggleSidebarPanel
:
sinon
.
stub
(),
toggleSidebarPanel
:
sinon
.
stub
(),
};
};
fakeBridge
=
{
call
:
sinon
.
stub
(),
};
fakeServiceConfig
=
sinon
.
stub
().
returns
({});
fakeStreamer
=
{
fakeStreamer
=
{
applyPendingUpdates
:
sinon
.
stub
(),
applyPendingUpdates
:
sinon
.
stub
(),
};
};
...
@@ -37,6 +46,7 @@ describe('TopBar', () => {
...
@@ -37,6 +46,7 @@ describe('TopBar', () => {
TopBar
.
$imports
.
$mock
({
TopBar
.
$imports
.
$mock
({
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
'../util/is-third-party-service'
:
fakeIsThirdPartyService
,
'../util/is-third-party-service'
:
fakeIsThirdPartyService
,
'../service-config'
:
fakeServiceConfig
,
});
});
});
});
...
@@ -57,6 +67,7 @@ describe('TopBar', () => {
...
@@ -57,6 +67,7 @@ describe('TopBar', () => {
return
mount
(
return
mount
(
<
TopBar
<
TopBar
auth
=
{
auth
}
auth
=
{
auth
}
bridge
=
{
fakeBridge
}
isSidebar
=
{
true
}
isSidebar
=
{
true
}
settings
=
{
fakeSettings
}
settings
=
{
fakeSettings
}
streamer
=
{
fakeStreamer
}
streamer
=
{
fakeStreamer
}
...
@@ -86,14 +97,41 @@ describe('TopBar', () => {
...
@@ -86,14 +97,41 @@ describe('TopBar', () => {
assert
.
called
(
fakeStreamer
.
applyPendingUpdates
);
assert
.
called
(
fakeStreamer
.
applyPendingUpdates
);
});
});
it
(
'shows Help Panel when help icon is clicked'
,
()
=>
{
describe
(
'`HelpButton` and help requests'
,
()
=>
{
const
onShowHelpPanel
=
sinon
.
stub
();
context
(
'no help service handler configured in services (default)'
,
()
=>
{
const
wrapper
=
createTopBar
({
it
(
'toggles Help Panel on click'
,
()
=>
{
onShowHelpPanel
:
onShowHelpPanel
,
const
wrapper
=
createTopBar
();
const
help
=
helpBtn
(
wrapper
);
help
.
simulate
(
'click'
);
assert
.
calledWith
(
fakeStore
.
toggleSidebarPanel
,
uiConstants
.
PANEL_HELP
);
});
it
(
'displays a help icon active state when help panel active'
,
()
=>
{
// state returning active sidebar panel as `PANEL_HELP` triggers active class
fakeStore
.
getState
=
sinon
.
stub
().
returns
({
sidebarPanels
:
{
activePanelName
:
uiConstants
.
PANEL_HELP
,
},
});
const
wrapper
=
createTopBar
();
const
help
=
helpBtn
(
wrapper
);
wrapper
.
update
();
assert
.
isTrue
(
help
.
hasClass
(
'top-bar__btn--active'
));
assert
.
isOk
(
help
.
prop
(
'aria-expanded'
));
});
context
(
'help service handler configured in services'
,
()
=>
{
it
(
'fires a bridge event if help clicked and service is configured'
,
()
=>
{
fakeServiceConfig
.
returns
({
onHelpRequestProvided
:
true
});
const
wrapper
=
createTopBar
();
const
help
=
helpBtn
(
wrapper
);
help
.
simulate
(
'click'
);
assert
.
equal
(
fakeStore
.
toggleSidebarPanel
.
callCount
,
0
);
assert
.
calledWith
(
fakeBridge
.
call
,
bridgeEvents
.
HELP_REQUESTED
);
});
});
});
});
const
help
=
helpBtn
(
wrapper
);
help
.
simulate
(
'click'
);
assert
.
called
(
onShowHelpPanel
);
});
});
describe
(
'login/account actions'
,
()
=>
{
describe
(
'login/account actions'
,
()
=>
{
...
@@ -170,7 +208,10 @@ describe('TopBar', () => {
...
@@ -170,7 +208,10 @@ describe('TopBar', () => {
it
(
'toggles the share annotations panel when "Share" is clicked'
,
()
=>
{
it
(
'toggles the share annotations panel when "Share" is clicked'
,
()
=>
{
const
wrapper
=
createTopBar
();
const
wrapper
=
createTopBar
();
wrapper
.
find
(
'[title="Share annotations on this page"]'
).
simulate
(
'click'
);
wrapper
.
find
(
'[title="Share annotations on this page"]'
).
simulate
(
'click'
);
assert
.
called
(
fakeStore
.
toggleSidebarPanel
);
assert
.
calledWith
(
fakeStore
.
toggleSidebarPanel
,
uiConstants
.
PANEL_SHARE_ANNOTATIONS
);
});
});
it
(
'adds an active-state class to the "Share" icon when the panel is open'
,
()
=>
{
it
(
'adds an active-state class to the "Share" icon when the panel is open'
,
()
=>
{
...
@@ -217,14 +258,5 @@ describe('TopBar', () => {
...
@@ -217,14 +258,5 @@ describe('TopBar', () => {
assert
.
isFalse
(
wrapper
.
exists
(
'SortMenu'
));
assert
.
isFalse
(
wrapper
.
exists
(
'SortMenu'
));
assert
.
isFalse
(
wrapper
.
exists
(
'button[title="Share this page"]'
));
assert
.
isFalse
(
wrapper
.
exists
(
'button[title="Share this page"]'
));
});
});
it
(
'does show the Help menu and user menu'
,
()
=>
{
const
wrapper
=
createTopBar
({
isSidebar
:
false
,
auth
:
{
status
:
'logged-in'
},
});
assert
.
isTrue
(
wrapper
.
exists
(
'button[title="Help"]'
));
assert
.
isTrue
(
wrapper
.
exists
(
'UserMenu'
));
});
});
});
});
});
src/sidebar/components/top-bar.js
View file @
89bc4044
...
@@ -4,9 +4,11 @@ const { Fragment, createElement } = require('preact');
...
@@ -4,9 +4,11 @@ const { Fragment, createElement } = require('preact');
const
classnames
=
require
(
'classnames'
);
const
classnames
=
require
(
'classnames'
);
const
propTypes
=
require
(
'prop-types'
);
const
propTypes
=
require
(
'prop-types'
);
const
bridgeEvents
=
require
(
'../../shared/bridge-events'
);
const
useStore
=
require
(
'../store/use-store'
);
const
useStore
=
require
(
'../store/use-store'
);
const
{
applyTheme
}
=
require
(
'../util/theme'
);
const
{
applyTheme
}
=
require
(
'../util/theme'
);
const
isThirdPartyService
=
require
(
'../util/is-third-party-service'
);
const
isThirdPartyService
=
require
(
'../util/is-third-party-service'
);
const
serviceConfig
=
require
(
'../service-config'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
const
uiConstants
=
require
(
'../ui-constants'
);
const
uiConstants
=
require
(
'../ui-constants'
);
...
@@ -17,16 +19,44 @@ const SortMenu = require('./sort-menu');
...
@@ -17,16 +19,44 @@ const SortMenu = require('./sort-menu');
const
SvgIcon
=
require
(
'./svg-icon'
);
const
SvgIcon
=
require
(
'./svg-icon'
);
const
UserMenu
=
require
(
'./user-menu'
);
const
UserMenu
=
require
(
'./user-menu'
);
/**
* Button for opening/closing the help panel
*/
function
HelpButton
({
onClick
})
{
const
isActive
=
useStore
(
store
=>
store
.
getState
().
sidebarPanels
.
activePanelName
===
uiConstants
.
PANEL_HELP
);
return
(
<
button
className
=
{
classnames
(
'top-bar__btn top-bar__help-btn'
,
{
'top-bar__btn--active'
:
isActive
,
})}
onClick
=
{
onClick
}
title
=
"Help"
aria
-
expanded
=
{
isActive
}
aria
-
pressed
=
{
isActive
}
>
<
SvgIcon
name
=
"help"
className
=
"top-bar__help-icon"
/>
<
/button
>
);
}
HelpButton
.
propTypes
=
{
/* callback */
onClick
:
propTypes
.
func
.
isRequired
,
};
/**
/**
* The toolbar which appears at the top of the sidebar providing actions
* The toolbar which appears at the top of the sidebar providing actions
* to switch groups, view account information, sort/filter annotations etc.
* to switch groups, view account information, sort/filter annotations etc.
*/
*/
function
TopBar
({
function
TopBar
({
auth
,
auth
,
bridge
,
isSidebar
,
isSidebar
,
onLogin
,
onLogin
,
onLogout
,
onLogout
,
onShowHelpPanel
,
onSignUp
,
onSignUp
,
settings
,
settings
,
streamer
,
streamer
,
...
@@ -51,6 +81,19 @@ function TopBar({
...
@@ -51,6 +81,19 @@ function TopBar({
togglePanelFn
(
uiConstants
.
PANEL_SHARE_ANNOTATIONS
);
togglePanelFn
(
uiConstants
.
PANEL_SHARE_ANNOTATIONS
);
};
};
/**
* Open the help panel, or, if a service callback is configured to handle
* help requests, fire a relevant event instead
*/
const
requestHelp
=
()
=>
{
const
service
=
serviceConfig
(
settings
)
||
{};
if
(
service
.
onHelpRequestProvided
)
{
bridge
.
call
(
bridgeEvents
.
HELP_REQUESTED
);
}
else
{
togglePanelFn
(
uiConstants
.
PANEL_HELP
);
}
};
const
loginControl
=
(
const
loginControl
=
(
<
Fragment
>
<
Fragment
>
{
auth
.
status
===
'unknown'
&&
(
{
auth
.
status
===
'unknown'
&&
(
...
@@ -82,14 +125,7 @@ function TopBar({
...
@@ -82,14 +125,7 @@ function TopBar({
<
div
className
=
"top-bar__inner content"
>
<
div
className
=
"top-bar__inner content"
>
<
StreamSearchInput
/>
<
StreamSearchInput
/>
<
div
className
=
"top-bar__expander"
/>
<
div
className
=
"top-bar__expander"
/>
<
button
<
HelpButton
onClick
=
{
requestHelp
}
/
>
className
=
"top-bar__btn top-bar__help-btn"
onClick
=
{
onShowHelpPanel
}
title
=
"Help"
aria
-
label
=
"Help"
>
<
SvgIcon
name
=
"help"
className
=
"top-bar__help-icon"
/>
<
/button
>
{
loginControl
}
{
loginControl
}
<
/div
>
<
/div
>
)}
)}
...
@@ -124,14 +160,7 @@ function TopBar({
...
@@ -124,14 +160,7 @@ function TopBar({
<
SvgIcon
name
=
"share"
/>
<
SvgIcon
name
=
"share"
/>
<
/button
>
<
/button
>
)}
)}
<
button
<
HelpButton
onClick
=
{
requestHelp
}
/
>
className
=
"top-bar__btn top-bar__help-btn"
onClick
=
{
onShowHelpPanel
}
title
=
"Help"
aria
-
label
=
"Help"
>
<
SvgIcon
name
=
"help"
className
=
"top-bar__help-icon"
/>
<
/button
>
{
loginControl
}
{
loginControl
}
<
/div
>
<
/div
>
)}
)}
...
@@ -152,16 +181,13 @@ TopBar.propTypes = {
...
@@ -152,16 +181,13 @@ TopBar.propTypes = {
username
:
propTypes
.
string
,
username
:
propTypes
.
string
,
}),
}),
bridge
:
propTypes
.
object
.
isRequired
,
/**
/**
* Flag indicating whether the app is the sidebar or a top-level page.
* Flag indicating whether the app is the sidebar or a top-level page.
*/
*/
isSidebar
:
propTypes
.
bool
,
isSidebar
:
propTypes
.
bool
,
/**
* Callback invoked when user clicks "Help" button.
*/
onShowHelpPanel
:
propTypes
.
func
,
/**
/**
* Callback invoked when user clicks "Login" button.
* Callback invoked when user clicks "Login" button.
*/
*/
...
@@ -178,6 +204,6 @@ TopBar.propTypes = {
...
@@ -178,6 +204,6 @@ TopBar.propTypes = {
streamer
:
propTypes
.
object
,
streamer
:
propTypes
.
object
,
};
};
TopBar
.
injectedProps
=
[
'settings'
,
'streamer'
];
TopBar
.
injectedProps
=
[
'
bridge'
,
'
settings'
,
'streamer'
];
module
.
exports
=
withServices
(
TopBar
);
module
.
exports
=
withServices
(
TopBar
);
src/sidebar/index.js
View file @
89bc4044
...
@@ -155,6 +155,8 @@ function startAngularApp(config) {
...
@@ -155,6 +155,8 @@ function startAngularApp(config) {
.
component
(
.
component
(
'helpLink'
,
'helpLink'
,
wrapReactComponent
(
require
(
'./components/help-link'
))
wrapReactComponent
(
require
(
'./components/help-link'
))
'helpPanel'
,
wrapReactComponent
(
require
(
'./components/help-panel'
))
)
)
.
component
(
'helpPanel'
,
require
(
'./components/help-panel'
))
.
component
(
'helpPanel'
,
require
(
'./components/help-panel'
))
.
component
(
.
component
(
...
...
src/sidebar/templates/hypothesis-app.html
View file @
89bc4044
...
@@ -4,16 +4,11 @@
...
@@ -4,16 +4,11 @@
on-login=
"vm.login()"
on-login=
"vm.login()"
on-sign-up=
"vm.signUp()"
on-sign-up=
"vm.signUp()"
on-logout=
"vm.logout()"
on-logout=
"vm.logout()"
on-show-help-panel=
"vm.showHelpPanel()"
is-sidebar=
"::vm.isSidebar"
>
is-sidebar=
"::vm.isSidebar"
>
</top-bar>
</top-bar>
<div
class=
"content"
>
<div
class=
"content"
>
<sidebar-tutorial
ng-if=
"vm.isSidebar"
></sidebar-tutorial>
<help-panel
auth=
"vm.auth"
></help-panel>
<help-panel
ng-if=
"vm.helpPanel.visible"
on-close=
"vm.helpPanel.visible = false"
auth=
"vm.auth"
>
</help-panel>
<share-annotations-panel></share-annotations-panel>
<share-annotations-panel></share-annotations-panel>
<main
ng-view=
""
></main>
<main
ng-view=
""
></main>
</div>
</div>
...
...
src/sidebar/ui-constants.js
View file @
89bc4044
...
@@ -5,6 +5,7 @@
...
@@ -5,6 +5,7 @@
*/
*/
module
.
exports
=
{
module
.
exports
=
{
PANEL_HELP
:
'help'
,
PANEL_SHARE_ANNOTATIONS
:
'shareGroupAnnotations'
,
PANEL_SHARE_ANNOTATIONS
:
'shareGroupAnnotations'
,
TAB_ANNOTATIONS
:
'annotation'
,
TAB_ANNOTATIONS
:
'annotation'
,
TAB_NOTES
:
'note'
,
TAB_NOTES
:
'note'
,
...
...
src/styles/sidebar/components/help-panel.scss
View file @
89bc4044
.help-panel
{
.help-panel
{
@include
font-normal
;
&
__sub-panel-title
{
background
:
$grey-3
;
margin
:
0
;
margin-bottom
:
0
.72em
;
padding
:
0
.5em
;
padding
:
$layout-h-margin
;
text-align
:
center
;
border-radius
:
2px
;
font-size
:
1
.25em
;
}
font-weight
:
600
;
}
.help-panel-title
{
&
__content
{
color
:
$grey-6
;
padding
:
0
.5em
;
font-weight
:
bold
;
border-top
:
1px
solid
$grey-3
;
// Margin between top of the dialog and
border-bottom
:
1px
solid
$grey-3
;
// top of x-height of title should be ~15px.
line-height
:
$normal-line-height
;
margin-top
:
-5px
;
font-size
:
$normal-font-size
;
}
}
.help-panel-content
{
&
__icon
{
// Margin between bottom of ascent of title and
width
:
12px
;
// top of x-height of content should be 20px.
height
:
12px
;
margin-top
:
11px
;
}
}
.help-panel-content__key
{
&
__footer
{
width
:
100px
;
padding
:
0
.5em
0
;
float
:
left
;
display
:
flex
;
color
:
$grey-4
;
align-items
:
center
;
}
}
.help-panel-content__val
{
&
__sub-panel-link
{
word-wrap
:
break-word
;
display
:
flex
;
margin-left
:
100px
;
align-items
:
center
;
}
color
:
$brand
;
.help-panel-content
{
&
--right
{
margin-top
:
10px
;
margin-left
:
auto
;
margin-bottom
:
15px
;
}
}
&
-icon
{
margin
:
5px
;
width
:
12px
;
height
:
12px
;
}
}
&
-tabs
{
display
:
flex
;
align-items
:
center
;
&
__tab
{
flex
:
1
1
0px
;
margin-top
:
0
.5em
;
border-right
:
1px
solid
$grey-3
;
text-align
:
center
;
font-size
:
1
.25em
;
color
:
$grey-5
;
&
:last-of-type
{
border-right
:
none
;
}
}
&
__link
{
color
:
$grey-5
;
}
.help-panel-content__link
{
&
__icon
{
color
:
$grey-6
;
width
:
12px
;
text-decoration
:
underline
;
height
:
12px
;
&
:hover
{
}
text-decoration
:
underline
;
}
}
}
}
src/styles/sidebar/components/sidebar-panel.scss
View file @
89bc4044
...
@@ -15,6 +15,16 @@
...
@@ -15,6 +15,16 @@
border-bottom-style
:
solid
;
border-bottom-style
:
solid
;
}
}
&
__subheader
{
width
:
100%
;
text-align
:
center
;
padding
:
1em
0
.5em
;
border-bottom
:
1px
solid
$grey-3
;
font-size
:
1
.25em
;
font-weight
:
500
;
color
:
$grey-5
;
}
&
__title
{
&
__title
{
color
:
$brand
;
color
:
$brand
;
font-size
:
$body2-font-size
;
font-size
:
$body2-font-size
;
...
@@ -44,7 +54,7 @@
...
@@ -44,7 +54,7 @@
}
}
&
__content
{
&
__content
{
padding
:
0
.75
em
;
margin
:
1
em
;
padding
-top
:
0
;
margin
-top
:
0
;
}
}
}
}
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