Unverified Commit 7a0bc167 authored by Andrey Azov's avatar Andrey Azov Committed by GitHub
Browse files

Update help (#478)

parent 5fb91328
Pipeline #143807 passed with stages
in 7 minutes and 16 seconds
......@@ -33,6 +33,7 @@ const SpeciesPage = lazy(() => import('./species/SpeciesPage'));
const CustomDownload = lazy(() => import('./custom-download/CustomDownload'));
const Browser = lazy(() => import('./browser/Browser'));
const EntityViewer = lazy(() => import('./entity-viewer/EntityViewer'));
const About = lazy(() => import('./about/About'));
type AppProps = {
changeCurrentApp: (name: string) => void;
......@@ -67,6 +68,7 @@ const App = (props: AppProps) => {
component={EntityViewer}
/>
<Route path={`/genome-browser/:genomeId?`} component={Browser} />
<Route path={`/about`} component={About} />
<Route>
<Redirect to={{ ...location, state: { is404: true } }} />
</Route>
......
@import 'src/styles/common';
@import 'src/shared/components/help-article/helpArticleConstants';
$article-right-padding: 1.5rem;
.appBar {
display: flex;
align-items: center;
height: 80px;
padding: 0 44px;
}
.topMenuBar {
display: flex;
align-items: center;
height: 40px;
padding: 0 44px;
background: $light-grey;
box-shadow: 0 2px 3px $grey;
}
.main {
height: 100%;
display: grid;
grid-template-columns: [article] calc(440px + #{$article-right-padding}) [aside] minmax(200px, 300px);
grid-column-gap: 10rem;
padding-left: 80px;
padding-top: 44px;
article {
grid-column: article;
}
aside {
display: flex;
flex-direction: column;
grid-column: aside;
}
.asideTitle {
margin-bottom: $aside-title-margin-bottom;
}
}
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { ReactNode } from 'react';
import { useLocation } from 'react-router';
import useApiService from 'src/shared/hooks/useApiService';
import { TextArticle } from 'src/shared/components/help-article';
import {
TopMenu,
SideMenu
} from 'src/content/app/about/components/about-menu/AboutMenu';
import { Menu as MenuType } from 'src/shared/types/help-and-docs/menu';
import { TextArticle as TextArticleType } from 'src/shared/types/help-and-docs/article';
import styles from './About.scss';
const About = () => {
const location = useLocation();
const { data: menu } = useApiService<MenuType>({
endpoint: `/api/docs/menus?name=about`
});
const { data: article } = useApiService<TextArticleType>({
endpoint: `/api/docs/article?url=${encodeURIComponent(location.pathname)}`
});
return (
<>
<div>
<AppBar />
<TopMenuBar>
{menu && <TopMenu menu={menu} currentUrl={location.pathname} />}
</TopMenuBar>
</div>
<Main>
{article && <TextArticle article={article} />}
<aside>
{menu && (
<>
<div className={styles.asideTitle}>More about...</div>
<SideMenu menu={menu} currentUrl={location.pathname} />
</>
)}
</aside>
</Main>
</>
);
};
const AppBar = () => {
return <div className={styles.appBar}>About Ensembl</div>;
};
const TopMenuBar = (props: { children: ReactNode }) => {
return <div className={styles.topMenuBar}>{props.children}</div>;
};
const Main = (props: { children: ReactNode }) => {
return <main className={styles.main}>{props.children}</main>;
};
export default About;
@import 'src/styles/common';
.topMenuLink + .topMenuLink {
margin-left: 2rem;
}
.sideMenuLink {
display: inline-block;
padding: 0.3rem;
}
.activeLink {
color: $black;
}
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { Menu as MenuType } from 'src/shared/types/help-and-docs/menu';
import { MenuItem } from 'src/shared/types/help-and-docs/menu';
import styles from './AboutMenu.scss';
type Props = {
menu: MenuType;
currentUrl: string;
};
export const TopMenu = (props: Props) => {
return (
<>
{props.menu.items.map((item, index) => {
const className = classNames(styles.topMenuLink, {
[styles.activeLink]: hasItemWithUrl(item, props.currentUrl)
});
return (
<Link className={className} to={item.url as string} key={index}>
{item.name}
</Link>
);
})}
</>
);
};
export const SideMenu = (props: Props) => {
const { menu, currentUrl } = props;
const menuItem = menu.items.find(
(menuItem) =>
menuItem.type === 'collection' &&
menuItem.items.find((item) => item.url === currentUrl)
);
if (!menuItem || menuItem.type !== 'collection') {
// this shouldn't happen
return null;
}
return (
<>
{menuItem.items.map((item, index) => {
const linkClasses = classNames(styles.sideMenuLink, {
[styles.activeLink]: item.url === currentUrl
});
return (
<Link className={linkClasses} to={item.url as string} key={index}>
{item.name}
</Link>
);
})}
</>
);
};
const hasItemWithUrl = (menuItem: MenuItem, url: string): boolean => {
if (menuItem.url === url) {
return true;
} else if (menuItem.type === 'collection' && menuItem.items.length) {
return menuItem.items.some((item) => hasItemWithUrl(item, url));
}
return false;
};
......@@ -66,11 +66,20 @@
align-items: baseline;
margin-top: 4px;
font-size: 12px;
color: $medium-dark-grey;
color: $light-blue;
}
.logotypeAbout {
height: 10px;
fill: $medium-dark-grey;
fill: $light-blue;
margin-left: 0.5rem;
}
// For disabling temporarily the about Ensembl link in production
.aboutEnsemblDisabled {
color: $medium-dark-grey;
.logotypeAbout {
fill: $medium-dark-grey;
}
}
......@@ -17,6 +17,8 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { isEnvironment, Environment } from 'src/shared/helpers/environment';
import LaunchbarContainer from './launchbar/LaunchbarContainer';
import Account from './account/Account';
......@@ -59,12 +61,23 @@ export const Topbar = () => (
</div>
</div>
</div>
<div className={styles.aboutEnsembl}>
<AboutEnsembl />
</div>
);
// Temporarily disable the link to About Ensembl in production
const AboutEnsembl = () =>
isEnvironment([Environment.DEVELOPMENT, Environment.INTERNAL]) ? (
<Link to="/about" className={styles.aboutEnsembl}>
About
<Logotype className={styles.logotypeAbout} />
</Link>
) : (
<div className={styles.aboutEnsemblDisabled}>
About
<Logotype className={styles.logotypeAbout} />
</div>
</div>
);
);
export const Header = () => (
<header>
......
@import 'src/styles/common';
@import 'helpArticleConstants';
.textArticle {
h1 {
font-size: 15px;
line-height: 18px;
font-weight: $normal;
margin: 0 0 #{$heading-margin-bottom} -#{$heading-outdent};
}
h2 {
font-size: 13px;
margin-top: calc(2 * #{$line-height});
margin-bottom: $line-height;
}
p {
line-height: $line-height;
margin: $line-height 0;
}
img {
max-width: 100%;
}
// Notice that the external-link svg has the same shape as in the static/img/shared folder
// But the version copied here has the fill color set inline, because it is being accessed as an external image via url
a[href^=http] {
&::before {
content: url('./external-link.svg');
display: inline-block;
margin: 0 0.125rem;
position: relative;
height: 10px;
width: 10px;
}
}
}
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useRef, useEffect, RefObject } from 'react';
import { useDispatch } from 'react-redux';
import { push } from 'connected-react-router';
import { TextArticle as TextArticleType } from 'src/shared/types/help-and-docs/article';
import styles from './HelpArticle.scss';
type Props = {
article: TextArticleType;
className?: string;
};
const TextArticle = (props: Props) => {
const articleRef = useRef<HTMLElement | null>(null);
useRoutingRules(articleRef);
return (
<article
ref={articleRef}
className={styles.textArticle}
dangerouslySetInnerHTML={{ __html: props.article.body }}
/>
);
};
const useRoutingRules = <T extends HTMLElement>(ref: RefObject<T>) => {
const dispatch = useDispatch();
const onClick = (event: MouseEvent) => {
event.preventDefault();
const target = event.target as HTMLElement;
if (!target?.matches('a')) {
return;
}
const href = target.getAttribute('href') as string;
if (href.startsWith('/')) {
// This is a link to another page within the site;
// Use React Router to navigate;
dispatch(push(href));
} else {
// A href containing an absolute urls, with a protocol and a hostname
// is treated as a link to an external resource; open it in a new tab
window.open(href);
}
};
useEffect(() => {
ref?.current?.addEventListener('click', onClick);
return () => ref?.current?.removeEventListener('click', onClick);
});
};
export default TextArticle;
$heading-outdent: 16px;
$heading-margin-bottom: 27px;
$aside-title-margin-bottom: $heading-margin-bottom;
$line-height: 17px;
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="user" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
<path fill="#ff9900" fill-rule="evenodd" clip-rule="evenodd" d="M22,5.2c0,0-0.1,0.1-0.1,0.1l-8.5,8.7c-1,1-1,2.5,0,3.5l1.2,1.2c1,1,2.5,1,3.5,0l8.5-8.7c0,0,0.1-0.1,0.1-0.1
l2.6,2.7c1,1,1.7,0.6,1.7-0.7V1.8C31,1.4,30.6,1,30.2,1h-9.8c-1.4,0-1.7,0.8-0.7,1.8C19.7,2.8,22,5.2,22,5.2z M6,1C3.2,1,1,3.2,1,6
v20c0,2.8,2.2,5,5,5h20c2.8,0,5-2.2,5-5V13.1v7.1L26,16v7.5c0,1.4-1.1,2.5-2.5,2.5h-15C7.1,26,6,24.9,6,23.5v-15C6,7.1,7.1,6,8.5,6
H16l-4.2-5h7.1C18.9,1,6,1,6,1z"/>
</svg>
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { default as TextArticle } from './TextArticle';
......@@ -61,6 +61,15 @@ $article-right-padding: 1.5rem; // area over which Macs (which hide their scroll
}
}
.videoLoadingIndicator {
position: absolute;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.aside {
grid-column: aside;
padding-left: $aside-left-padding;
......@@ -111,3 +120,29 @@ $article-right-padding: 1.5rem; // area over which Macs (which hide their scroll
fill: white;
}
}
.historyButtons {
position: absolute;
top: 16px;
line-height: 0;
right: 70px;
}
.historyButton {
width: 18px;
height: 18px;
border-radius: 100%;
&:first-child {
margin-right: 1.4rem;
}
&Active {
fill: $blue;
cursor: pointer;
}
&Inactive {
fill: $grey;
}
}
......@@ -14,91 +14,154 @@
* limitations under the License.
*/
import React, { useState, useRef, ReactNode } from 'react';
import React, {
useState,
useEffect,
useRef,
ReactNode,
MutableRefObject
} from 'react';
import classNames from 'classnames';
import useHelpArticle, {
emptyRelatedItems,
CurrentArticle,
CurrentVideo,
CurrentItem,
RelatedItems as RelatedItemsType,
ArticleReference,
VideoReference
} from './useHelpArticle';
import useResizeObserver from 'src/shared/hooks/useResizeObserver.ts';
import HelpPopupHistory from './helpPopupHistory';
import useHelpArticle, { Article as ArticleType } from './useHelpArticle';
import useResizeObserver from 'src/shared/hooks/useResizeObserver';
import { CircleLoader } from 'src/shared/components/loader/Loader';
import { ReactComponent as VideoIcon } from 'static/img/shared/video.svg';
import { ReactComponent as BackIcon } from 'static/img/browser/navigate-left.svg';
import { ReactComponent as ForwardIcon } from 'static/img/browser/navigate-right.svg';
import { LoadingState } from 'src/shared/types/loading-state';
import {
RelatedArticle,
HelpVideo,
SlugReference,
PathReference
TextArticle,
VideoArticle,
SlugReference
} from './types';
import styles from './HelpPopupBody.scss';
type Props = SlugReference | PathReference;
type Props = SlugReference;
const HelpPopupBody = (props: Props) => {
const [currentReference, setCurrentReference] = useState<
ArticleReference | VideoReference
>(createArticleReference(props));
const { currentHelpItem, relatedHelpItems } = useHelpArticle(
currentReference
const [currentReference, setCurrentReference] = useState<SlugReference>(
props
);
const { article, loadingState } = useHelpArticle(currentReference);
const historyRef = useRef<HelpPopupHistory | null>(null);
useEffect(() => {
historyRef.current = new HelpPopupHistory(currentReference);
}, []);
const onRelatedItemClick = (reference: ArticleReference | VideoReference) => {
const onRelatedItemClick = (reference: SlugReference) => {
historyRef.current?.add(reference);
setCurrentReference(reference);
};
if (currentHelpItem?.type === 'article') {
const onHistoryBack = () => {
const prevReference = historyRef.current?.getPrevious();
if (prevReference) {
setCurrentReference(prevReference);
}
};
const onHistoryForward = () => {
const nextReference = historyRef.current?.getNext();
if (nextReference) {
setCurrentReference(nextReference);
}
};