summaryrefslogtreecommitdiffstats
path: root/sdnr/wt/odlux/apps/helpApp/src
diff options
context:
space:
mode:
Diffstat (limited to 'sdnr/wt/odlux/apps/helpApp/src')
-rw-r--r--sdnr/wt/odlux/apps/helpApp/src/actions/helpActions.ts61
-rw-r--r--sdnr/wt/odlux/apps/helpApp/src/components/markdown.tsx60
-rw-r--r--sdnr/wt/odlux/apps/helpApp/src/components/subMenuEntry.tsx37
-rw-r--r--sdnr/wt/odlux/apps/helpApp/src/handlers/helpAppRootHandler.ts58
-rw-r--r--sdnr/wt/odlux/apps/helpApp/src/index.html25
-rw-r--r--sdnr/wt/odlux/apps/helpApp/src/models/tocNode.ts24
-rw-r--r--sdnr/wt/odlux/apps/helpApp/src/plugin.tsx69
-rw-r--r--sdnr/wt/odlux/apps/helpApp/src/services/helpService.ts47
-rw-r--r--sdnr/wt/odlux/apps/helpApp/src/utilities/path.ts62
-rw-r--r--sdnr/wt/odlux/apps/helpApp/src/views/helpApplication.tsx53
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