diff options
Diffstat (limited to 'sdnr/wt/odlux/apps/helpApp/src')
-rw-r--r-- | sdnr/wt/odlux/apps/helpApp/src/actions/helpActions.ts | 61 | ||||
-rw-r--r-- | sdnr/wt/odlux/apps/helpApp/src/components/markdown.tsx | 60 | ||||
-rw-r--r-- | sdnr/wt/odlux/apps/helpApp/src/components/subMenuEntry.tsx | 37 | ||||
-rw-r--r-- | sdnr/wt/odlux/apps/helpApp/src/handlers/helpAppRootHandler.ts | 58 | ||||
-rw-r--r-- | sdnr/wt/odlux/apps/helpApp/src/index.html | 25 | ||||
-rw-r--r-- | sdnr/wt/odlux/apps/helpApp/src/models/tocNode.ts | 24 | ||||
-rw-r--r-- | sdnr/wt/odlux/apps/helpApp/src/plugin.tsx | 69 | ||||
-rw-r--r-- | sdnr/wt/odlux/apps/helpApp/src/services/helpService.ts | 47 | ||||
-rw-r--r-- | sdnr/wt/odlux/apps/helpApp/src/utilities/path.ts | 62 | ||||
-rw-r--r-- | sdnr/wt/odlux/apps/helpApp/src/views/helpApplication.tsx | 53 |
10 files changed, 496 insertions, 0 deletions
diff --git a/sdnr/wt/odlux/apps/helpApp/src/actions/helpActions.ts b/sdnr/wt/odlux/apps/helpApp/src/actions/helpActions.ts new file mode 100644 index 000000000..ba8969a2c --- /dev/null +++ b/sdnr/wt/odlux/apps/helpApp/src/actions/helpActions.ts @@ -0,0 +1,61 @@ +import { Action } from '../../../../framework/src/flux/action'; +import { Dispatch } from '../../../../framework/src/flux/store'; + +import { TocTreeNode } from '../models/tocNode'; +import helpService from '../services/helpService'; + +export class LoadTocAction extends Action { + constructor() { + super(); + + } +} + +export class TocLoadedAction extends Action { + constructor(public toc?: TocTreeNode[], error?: string) { + super(); + + } +} + +export const requestTocAsyncAction = async (dispatch: Dispatch) => { + dispatch(new LoadTocAction); + try { + const toc = await helpService.getTableOfContents(); + if (toc) { + dispatch(new TocLoadedAction(toc)); + } else { + dispatch(new TocLoadedAction(undefined, "Could not load TOC.")); + } + } catch (err) { + dispatch(new TocLoadedAction(undefined, err)); + } +} + +export class LoadDocumentAction extends Action { + constructor() { + super(); + + } +} + +export class DocumentLoadedAction extends Action { + constructor(public document?: string, public documentPath?: string, error?: string) { + super(); + + } +} + +export const requestDocumentAsyncActionCreator = (path: string) => async (dispatch: Dispatch) => { + dispatch(new LoadDocumentAction); + try { + const doc = await helpService.getDocument(path); + if (doc) { + dispatch(new DocumentLoadedAction(doc, path)); + } else { + dispatch(new DocumentLoadedAction(undefined, undefined, "Could not load document.")); + } + } catch (err) { + dispatch(new DocumentLoadedAction(undefined, undefined, err)); + } +}
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/helpApp/src/components/markdown.tsx b/sdnr/wt/odlux/apps/helpApp/src/components/markdown.tsx new file mode 100644 index 000000000..ea272bc4d --- /dev/null +++ b/sdnr/wt/odlux/apps/helpApp/src/components/markdown.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; + +import * as marked from 'marked'; +import * as hljs from 'highlight.js'; + +type MarkdownComponentProps = { + text: string; + className?: string; + markedOptions?: marked.MarkedOptions; + style?: React.CSSProperties +} + +const defaultRenderer = new marked.Renderer(); +defaultRenderer.link = (href, title, text) => ( + `<a target="_blank" rel="noopener noreferrer" href="${ href }" title="${ title }">${ text }</a>` +); + + +class MarkdownComponent extends React.Component<MarkdownComponentProps> { + constructor(props: MarkdownComponentProps) { + super(props); + + const markedOptions: marked.MarkedOptions = { + gfm: true, + tables: true, + breaks: false, + pedantic: false, + sanitize: true, + smartLists: true, + smartypants: false, + langPrefix: 'hljs ', + ...(this.props.markedOptions || {}), + highlight: (code, lang) => { + if (!!(lang && hljs.getLanguage(lang))) { + return hljs.highlight(lang, code).value; + } + return code; + } + }; + + marked.setOptions(markedOptions); + } + render() { + const { text, className, style } = this.props; + + + const html = (marked(text || '', { renderer: this.props.markedOptions && this.props.markedOptions.renderer || defaultRenderer })); + + return ( + <div + dangerouslySetInnerHTML={ { __html: html } } + className={ className } + style={ style } + /> + ); + } +} + +export const Markdown = MarkdownComponent; + diff --git a/sdnr/wt/odlux/apps/helpApp/src/components/subMenuEntry.tsx b/sdnr/wt/odlux/apps/helpApp/src/components/subMenuEntry.tsx new file mode 100644 index 000000000..72bb39e39 --- /dev/null +++ b/sdnr/wt/odlux/apps/helpApp/src/components/subMenuEntry.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; + +import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore"; +import connect, { Connect, IDispatcher } from '../../../../framework/src/flux/connect'; +import { TreeView, TreeViewCtorType } from '../../../../framework/src/components/material-ui/treeView'; + +import { ListItemText } from '@material-ui/core'; + +import { NavigateToApplication } from '../../../../framework/src/actions/navigationActions'; + +import { TocTreeNode } from '../models/tocNode'; + +const TocTree = TreeView as any as TreeViewCtorType<TocTreeNode>; + +const mapProps = (state: IApplicationStoreState) => ({ + helpToc: state.helpApp.toc, + helpBusy: state.helpApp.busy +}); + +const mapDisp = (dispatcher: IDispatcher) => ({ + requestDocument: (node: TocTreeNode) => dispatcher.dispatch(new NavigateToApplication("helpApp", node.uri)) +}); + +const SubMenuEntryComponent: React.SFC<Connect<typeof mapProps, typeof mapDisp>> = (props) => { + return props.helpToc + ? ( + <TocTree items={ props.helpToc } contentProperty={ "label" } childrenProperty={ "nodes" } depthOffset={ 1 } + useFolderIcons={ false } enableSearchBar={ false } onItemClick={ props.requestDocument } /> + ) + : ( + <ListItemText >Loading ...</ListItemText> + ) +}; + +export const SubMenuEntry = connect(mapProps, mapDisp)(SubMenuEntryComponent); +export default SubMenuEntry; + diff --git a/sdnr/wt/odlux/apps/helpApp/src/handlers/helpAppRootHandler.ts b/sdnr/wt/odlux/apps/helpApp/src/handlers/helpAppRootHandler.ts new file mode 100644 index 000000000..efdc6e83d --- /dev/null +++ b/sdnr/wt/odlux/apps/helpApp/src/handlers/helpAppRootHandler.ts @@ -0,0 +1,58 @@ +// main state handler + +import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; +import { TocTreeNode } from 'models/tocNode'; +import { IActionHandler } from '../../../../framework/src/flux/action'; +import { LoadTocAction, TocLoadedAction, LoadDocumentAction, DocumentLoadedAction } from '../actions/helpActions'; + +export interface IHelpAppStoreState { + busy: boolean; + toc: TocTreeNode[] | undefined; + content: string | undefined; + currentPath: string | undefined; +} + +declare module '../../../../framework/src/store/applicationStore' { + interface IApplicationStoreState { + helpApp: IHelpAppStoreState + } +} + +const helpAppStoreStateInit: IHelpAppStoreState = { + busy: false, + toc: undefined, + content: undefined, + currentPath: undefined +}; + +export const helpAppRootHandler: IActionHandler<IHelpAppStoreState> = (state = helpAppStoreStateInit, action) => { + if (action instanceof LoadTocAction) { + state = { + ...state, + busy: true + }; + } else if (action instanceof TocLoadedAction) { + state = { + ...state, + busy: false, + toc: action.toc + }; + } else if (action instanceof LoadDocumentAction) { + state = { + ...state, + busy: true + }; + } else if (action instanceof DocumentLoadedAction) { + state = { + ...state, + busy: false, + content: action.document, + currentPath: action.documentPath + }; + } + + return state; +} + + +export default helpAppRootHandler; diff --git a/sdnr/wt/odlux/apps/helpApp/src/index.html b/sdnr/wt/odlux/apps/helpApp/src/index.html new file mode 100644 index 000000000..2d20410e0 --- /dev/null +++ b/sdnr/wt/odlux/apps/helpApp/src/index.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta http-equiv="X-UA-Compatible" content="ie=edge"> + <!-- <link rel="stylesheet" href="./vendor.css"> --> + <title>Minimal App</title> +</head> + +<body> + <div id="app"></div> + <script type="text/javascript" src="./require.js"></script> + <script type="text/javascript" src="./config.js"></script> + <script> + // run the application + require(["app", "helpApp"], function (app, helpApp) { + helpApp.register(); + app("./app.tsx") + }); + </script> +</body> + +</html>
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/helpApp/src/models/tocNode.ts b/sdnr/wt/odlux/apps/helpApp/src/models/tocNode.ts new file mode 100644 index 000000000..ae73ec43d --- /dev/null +++ b/sdnr/wt/odlux/apps/helpApp/src/models/tocNode.ts @@ -0,0 +1,24 @@ +export type VersionInfo = { + label: string, + path: string, + date: string +} + +export type TocNode = { + label: string; + versions: { + [versionKey: string]: VersionInfo, + current: VersionInfo + }; + nodes?: TocNodeCollection; +} + +export type TocNodeCollection = { [tocNodeKey: string]: TocNode }; + + +export type TocTreeNode = { + label: string; + uri: string; + nodes?: TocTreeNode[]; + disabled?: boolean; +} diff --git a/sdnr/wt/odlux/apps/helpApp/src/plugin.tsx b/sdnr/wt/odlux/apps/helpApp/src/plugin.tsx new file mode 100644 index 000000000..d1cf80804 --- /dev/null +++ b/sdnr/wt/odlux/apps/helpApp/src/plugin.tsx @@ -0,0 +1,69 @@ +// app configuration and main entry point for the app + +import * as React from "react"; +import { withRouter, RouteComponentProps, Route, Switch, Redirect } from 'react-router-dom'; + +import { faFirstAid } from '@fortawesome/free-solid-svg-icons'; // select app icon + +import applicationManager from '../../../framework/src/services/applicationManager'; +import { IApplicationStoreState } from "../../../framework/src/store/applicationStore"; +import connect, { Connect, IDispatcher } from '../../../framework/src/flux/connect'; + +import { requestTocAsyncAction, requestDocumentAsyncActionCreator } from "./actions/helpActions"; +import { helpAppRootHandler } from './handlers/helpAppRootHandler'; + +import { HelpApplication } from './views/helpApplication'; +import { SubMenuEntry } from "./components/subMenuEntry"; + +import '!style-loader!css-loader!highlight.js/styles/default.css'; + +const mapProps = (state: IApplicationStoreState) => ({ + +}); + +const mapDisp = (dispatcher: IDispatcher) => ({ + requestDocument: (path: string) => { + dispatcher.dispatch(requestDocumentAsyncActionCreator(path)); + } +}); + +let currentHelpPath: string | undefined = undefined; + +const HelpApplicationRouteAdapter = connect(mapProps, mapDisp)((props: RouteComponentProps<{ '0'?: string }> & Connect<typeof mapProps, typeof mapDisp>) => { + if (currentHelpPath !== props.match.params["0"]) { + // route parameter has changed + currentHelpPath = props.match.params["0"] || undefined; + // Hint: This timeout is need, since it is not recommended to change the state while rendering is in progress ! + window.setTimeout(() => { + if (currentHelpPath) { + props.requestDocument(currentHelpPath); + } + }); + } + return ( + <HelpApplication /> + ) +}); + +const App = withRouter((props: RouteComponentProps) => ( + <Switch> + <Route path={ `${ props.match.path }/*` } component={ HelpApplicationRouteAdapter } /> + <Redirect to={ `${ props.match.path }` } /> + </Switch> +)); + +export async function register() { + const applicationApi = applicationManager.registerApplication({ + name: "helpApp", + icon: faFirstAid, + rootComponent: App, + rootActionHandler: helpAppRootHandler, + menuEntry: "Help", + subMenuEntry: SubMenuEntry + }); + + // start the initial toc request after the application store is initalized + const store = await applicationApi.applicationStoreInitialized; + store.dispatch(requestTocAsyncAction); + +}
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/helpApp/src/services/helpService.ts b/sdnr/wt/odlux/apps/helpApp/src/services/helpService.ts new file mode 100644 index 000000000..480cdd04e --- /dev/null +++ b/sdnr/wt/odlux/apps/helpApp/src/services/helpService.ts @@ -0,0 +1,47 @@ +import { requestRest } from '../../../../framework/src/services/restService'; +import { TocTreeNode, TocNodeCollection } from '../models/tocNode'; + +class HelpService { + + private tocNodeCollection: TocTreeNode[] | null = null; + private documents: { [path: string]: string | null } = {}; + + public async getDocument(path: string): Promise<string | null> { + // check if the result is allready in the cache + if (this.documents[path]) return Promise.resolve(this.documents[path]); + + // request the document + const result = await requestRest<string>(`/help/${ path }`.replace(/\/{2,}/i, '/')); + if (result) { + this.documents[path] = result; + } + return this.documents[path] || null; + } + + public async getTableOfContents(): Promise<TocTreeNode[] | null> { + // check if the result is allready in the cache + if (this.tocNodeCollection) return Promise.resolve(this.tocNodeCollection); + + // request the table of contents + const result = await requestRest<TocNodeCollection>('/help/?meta'); + if (result !== false) { + const mapNodesCollection = (col: TocNodeCollection): TocTreeNode[] => { + return Object.keys(col).reduce <TocTreeNode[]>((acc, key) => { + const current = col[key]; + acc.push({ + label: current.label, + uri: current.versions.current.path, + nodes: current.nodes && mapNodesCollection(current.nodes) || undefined + }); + return acc; + }, []); + } + + this.tocNodeCollection = result && mapNodesCollection(result); + } + return this.tocNodeCollection || null; + } +} + +export const helpService = new HelpService(); +export default helpService;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/helpApp/src/utilities/path.ts b/sdnr/wt/odlux/apps/helpApp/src/utilities/path.ts new file mode 100644 index 000000000..3cc58a202 --- /dev/null +++ b/sdnr/wt/odlux/apps/helpApp/src/utilities/path.ts @@ -0,0 +1,62 @@ +export const resolvePath = (...paths: string[]): string => { + function resolve(pathA: string, pathB: string) { + // ‘a’ => ['a'] + // 'a/b' => ['a', 'b'] + // '/a/b' => ['', 'a', 'b'] + // '/a/b/' => ['', 'a', 'b', ''] + const pathBParts = pathB.split('/'); + if (pathBParts[0] === '') { + return pathBParts.join('/'); + } + const pathAParts = pathA.split('/'); + const aLastIndex = pathAParts.length - 1; + if (pathAParts[aLastIndex] !== '') { + pathAParts[aLastIndex] = ''; + } + + let part: string; + let i = 0; + while (typeof (part = pathBParts[i]) === 'string') { + switch (part) { + case '..': + pathAParts.pop(); + pathAParts.pop(); + pathAParts.push(''); + break; + case '.': + pathAParts.pop(); + pathAParts.push(''); + break; + default: + pathAParts.pop(); + pathAParts.push(part); + pathAParts.push(''); + break; + } + i++; + } + if (pathBParts[pathBParts.length - 1] !== '') pathAParts.pop(); + return pathAParts.join('/'); + } + + let i = 0; + let path: string; + let r = location.pathname; + + const urlRegex = /^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i; + const multiSlashReg = /\/\/+/g; + + while (typeof (path = paths[i]) === 'string') { + debugger; + const matches = path && path.match(urlRegex); + if (matches || !i) { + r = path; + } else { + path = path.replace(multiSlashReg, '/'); + r = resolve(r, path); + } + i++; + } + + return r; +};
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/helpApp/src/views/helpApplication.tsx b/sdnr/wt/odlux/apps/helpApp/src/views/helpApplication.tsx new file mode 100644 index 000000000..dedb93607 --- /dev/null +++ b/sdnr/wt/odlux/apps/helpApp/src/views/helpApplication.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import * as marked from 'marked'; + +import { resolvePath } from '../utilities/path'; + +import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; +import connect, { Connect } from '../../../../framework/src/flux/connect'; + +import { Markdown } from "../components/markdown"; + +import '!style-loader!css-loader!github-markdown-css/github-markdown.css' + +const mapProps = (state: IApplicationStoreState) => ({ + content: state.helpApp.content, + currentPath: state.helpApp.currentPath +}); + +type HelpApplicationComponentProps = Connect<typeof mapProps>; + +class HelpApplicationComponent extends React.Component<HelpApplicationComponentProps> { + + /** + * Initializes a new instance. + */ + constructor(props: HelpApplicationComponentProps) { + super(props); + + this.renderer = new marked.Renderer(); + + this.renderer.link = (href: string, title: string, text: string) => { + // check if href is rel or abs + const absUrlMatch = href.trim().match(/^https?:\/\//i); + return `<a href="${ absUrlMatch ? href : resolvePath('#/helpApp/', this.props.currentPath || '/', href) }" title="${ title }" >${ text }</a>` + }; + + this.renderer.image = (href: string, title: string) => { + return `<img src="${ resolvePath('/help/', this.props.currentPath || '/', href) }" alt="${ title }" />` + }; + + } + + render(): JSX.Element { + return this.props.content ? ( + <Markdown text={ this.props.content } markedOptions={ { renderer: this.renderer } } className="markdown-body" + style={{ maxWidth: "960px", margin: "1.5em auto" }} /> + ) : (<h2>Loading ...</h2>) + } + + private renderer: marked.Renderer; +} + +export const HelpApplication = connect(mapProps)(HelpApplicationComponent); +export default HelpApplication;
\ No newline at end of file |