Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC for docs filtering feature #1816

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/theme/DocItem/TagsWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from "react";

function TagsWrapper({props, activeIDs}) {
const {children, ...rest} = props; // do we need 'rest' for anything?

const removeEmptyH6 = (arr) => {
return arr.filter(i => !(i.props && i.props.originalType === 'h6' && i.props.children === ''));
}

if (!activeIDs) {
const newProps = {...props, children: removeEmptyH6(children)};
return <React.Fragment {...newProps}/>; // page does not contain any tags or nothing is selected or everything is selected.
}

const isNextChildLower = (nextItem, currentHeader) => {
const headers = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
if (!headers.includes(nextItem.toLowerCase())) {
return true; // next item is not a header
} else if (headers.indexOf(currentHeader.toLowerCase()) < headers.indexOf(nextItem.toLowerCase())) {
return true; // next item is header but it is lower
} else {
return false;
}
}

const addHighterHeaders = (acc, headerLevel, index, activeChildren) => {
if (headerLevel < 3) {
return acc.reverse();
}
const highterHeader = children.slice(0, index).filter(i => i.props.originalType === `h${headerLevel - 1}`).pop();
const isHeaderAlreadyDisplayed = !!(highterHeader && activeChildren && activeChildren.filter(c => c.props.id === highterHeader.props.id).length);
const accumulatedHeaders = highterHeader && !isHeaderAlreadyDisplayed ? [...acc, highterHeader] : acc;
return addHighterHeaders(accumulatedHeaders, headerLevel - 1, index);
}

const activeChildren = children.reduce((acc, child, index) => {
// Works only with headers that aren't hidden under <details>
// Could be modified to walk recursively through child.props.children if it exists, but i doubt that we need it.

if (acc.showChild && (!child.props || isNextChildLower(child.props.originalType, acc.currentHeaderType))) {
acc.showChild = true;
} else if (child.props && activeIDs.includes(child.props.id)) {
acc.showChild = true;
acc.currentHeaderType = child.props.originalType;
const highterHeaders = addHighterHeaders([], parseInt(child.props.originalType.slice(1, 2), 10), index, acc.children);
acc.children = [...acc.children, ...highterHeaders];
} else {
acc.showChild = false;
// Note: Manipulating children style is possible like that by adding the child.props.style, i.e. to gray out some of them instead of filtering.
// newChild = {...child, props: {...child.props, style: {opacity: '0.3'}}}
}
return acc.showChild ? {...acc, children: [...acc.children, child]} : acc;

}, {showChild: false, currentHeaderType: '', children: []});

activeChildren.children.push(<p style={{textAlign: 'center', color: '#606770', fontSize: '90%'}}>Not found what you was looking for? Check the active filters</p>);

return <React.Fragment children={removeEmptyH6(activeChildren.children)}/>
};

export default TagsWrapper;
58 changes: 56 additions & 2 deletions src/theme/DocItem/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import TOC from "@theme/TOC";
import clsx from "clsx";
import styles from "./styles.module.css";
import { useActivePlugin, useVersions } from "@theme/hooks/useDocs";
import TagsWrapper from "./TagsWrapper";

//Components
import DocsInfo from "./DocsInfo";
Expand Down Expand Up @@ -57,6 +58,57 @@ function DocItem(props) {
);
});

const getActiveIDs = (selectedComponents) => {
if (!metadata.frontMatter.meta || !metadata.frontMatter.meta[0].tags || !Object.keys(metadata.frontMatter.meta[0].tags).length) {
return undefined; // No tags defined for that page
}
if (!selectedComponents || !(selectedComponents.tags instanceof Object)) {
return undefined; // No selectedComponents or object has wrong structure
}
const numberOfSelectedComponents = Object.values(selectedComponents.tags).filter(i => i.value).length;
const activeDownloadType = selectedComponents.downloadType && selectedComponents.downloadType.toLowerCase() !== 'all';
const activeESM = selectedComponents.esm && selectedComponents.esm.toLowerCase() !== 'all';
if (!numberOfSelectedComponents && !activeDownloadType && !activeESM) {
return undefined; // Nothing is selected
}
const activeTags = Object.keys(selectedComponents.tags).filter(tag => selectedComponents.tags[tag].value).map(tag => tag.toLowerCase());
activeDownloadType && activeTags.push(selectedComponents.downloadType.toLowerCase());
activeESM && activeTags.push(selectedComponents.esm.toLowerCase());
const tagsDictionary = metadata.frontMatter.meta[0].tags;
const lowerCaseTagsDict = Object.keys(tagsDictionary).reduce((acc, i) => ({...acc, [i.toLowerCase()]: tagsDictionary[i]}), {});
const activeHeaders = activeTags.reduce((acc, tag) => lowerCaseTagsDict[tag] ? [...acc, ...lowerCaseTagsDict[tag]] : acc, []);
return activeHeaders.map(i => i.toLowerCase().replaceAll(' ', '-').replaceAll(/[^\w-]/ig, ''));
}

const getTOC = (activeIDs, toc) => {
if (!activeIDs) {
return toc;
}
const filterChildren = (arr) => {
return arr.reduce((acc, i) => {
if (activeIDs.includes(i.id)) {
acc.push(i);
} else if (i.children.length) {
const c = filterChildren(i.children);
if (c.length) {
acc.push({...i, children: c});
}
}
return acc;
}, []);
}
return filterChildren(toc);
}

const [activeIDs, setActiveIDs] = useState(getActiveIDs(JSON.parse(window.sessionStorage.getItem('ZoweDocs::selectedComponents') || "{}")));

window.onstorage = (e) => {
const newComponentsSelection = window.sessionStorage.getItem('ZoweDocs::selectedComponents');
if (newComponentsSelection) {
setActiveIDs(getActiveIDs(JSON.parse(newComponentsSelection)));
}
};

return (
<>
<Head>
Expand Down Expand Up @@ -108,7 +160,9 @@ function DocItem(props) {
title={title}
/>
)}
<MDXProvider components={MDXComponents}>
<MDXProvider components={{...MDXComponents,
wrapper: props => <TagsWrapper props={props} activeIDs={activeIDs}/>
}}>
<div className="markdown">
<DocContent />
</div>
Expand All @@ -125,7 +179,7 @@ function DocItem(props) {
</div>
{!hideTableOfContents && DocContent.toc && (
<div className="col col--3">
<TOC toc={DocContent.toc} />
<TOC toc={getTOC(activeIDs, DocContent.toc)} />
</div>
)}
</div>
Expand Down
111 changes: 111 additions & 0 deletions src/theme/DocSidebar/ComponentSelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { useState } from "react";
import styles from './styles.module.css';

// Notes:
// DocSideBarItem can take optional props - customProps: item={{...item, customProps: {testprop: true}}}
// Rewrite in TS?

// FIXME: Cross dependency of Components and ESMs/Download types. Selecting of ESM should not affect components if no other ESMs are specified for page / topic?
const ESMs = ['All', 'RACF', 'TopSecret', 'ACF2'];
const downloadTypes = ['All', 'SMPE', 'Pax', 'Container'];
const makeNewComponentsObject = () => {
const item = {
tags: {
cli: {label: "CLI", value: false},
desktop: {label: "Desktop", value: false},
explorer: {label: "Explorer", value: false},
apiml: {label: "Mediation Layer", value: false},
zss: {label: "ZSS", value: false}
},
esm: ESMs[0],
downloadType: downloadTypes[0],
}
window.sessionStorage.setItem("ZoweDocs::selectedComponents", JSON.stringify(item));
return item;
}

const ComponentSelector = () => {
const currentComponents = window.sessionStorage.getItem("ZoweDocs::selectedComponents");
const [components, setComponents] = useState(currentComponents ? JSON.parse(currentComponents) : makeNewComponentsObject());
const [showTagsSelector, toggleTagsSelector] = useState(false);

const setStorage = item => {
window.sessionStorage.setItem("ZoweDocs::selectedComponents", item);
window.dispatchEvent( new Event('storage') );
setComponents(JSON.parse(window.sessionStorage.getItem("ZoweDocs::selectedComponents")));
}

const setTags = tag => {
const tags = components.tags;
tags[tag].value = !tags[tag].value;
const item = JSON.stringify({...components, tags});
setStorage(item);
}

const setESM = type => {
const item = JSON.stringify({...components, esm: type});
setStorage(item);
}

const setDownloadType = type => {
const item = JSON.stringify({...components, downloadType: type});
setStorage(item);
}

return (
<div className={styles.tagsSelectorContainer}>
<div className={styles.tagsSeparator}/>
<div className={styles.collapsibleTagsSelector} onClick={() => toggleTagsSelector(!showTagsSelector)}>
<p className={styles.tagsSelectorLabel}>Select tags or components</p>
<a className={`${styles.arrowIcon} menu__link--sublist`} style={{transform: showTagsSelector ? 'rotate(0deg)' : 'rotate(-90deg)'}}/>
</div>

<div className={styles.tagsSelectorContent} style={{height: showTagsSelector ? '420px' : '0px'}}>
<label className={styles.tagSectionLabel} for={'components'}>Components</label>
<div id="components" className={styles.tagsSection}>
{Object.keys(components.tags).map(i => (
<div key={i} className={styles.tagOption}>
<input id={i} type="checkbox" checked={components.tags[i].value} onChange={() => setTags(i)}/>
<label style={{paddingLeft: "4px"}} for={i}>{components.tags[i].label}</label>
</div>
))}
</div>

<label className={styles.tagSectionLabel} for={'esm'}>ESMs</label>
<div id="esm" className={styles.tagsSection}>
{ESMs.map(esm => <div className={styles.tagOption}>
<input
id={esm}
className={styles.tagRadioInput}
onChange={() => setESM(esm)}
type="radio"
name="esm-types"
value={esm}
checked={esm === components.esm}
/>
<label for={esm}>{esm}</label>
</div>)}
</div>

<label className={styles.tagSectionLabel} for={'downloadType'}>Download Types</label>
<div id="downloadType" className={styles.tagsSection}>
{downloadTypes.map(type => <div className={styles.tagOption}>
<input
id={type}
className={styles.tagRadioInput}
onChange={() => setDownloadType(type)}
type="radio"
name="download-types"
value={type}
checked={type === components.downloadType}
/>
<label for={type}>{type}</label>
</div>)}
</div>

</div>
</div>
);
};

export default ComponentSelector;
Loading