feat: support documentation

This commit is contained in:
Donus(ADA) 2023-10-08 15:53:16 +07:00
parent 250227a134
commit 01c298c58e
17 changed files with 2337 additions and 6686 deletions

8669
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,7 @@
"file-dialog": "^0.0.8",
"file-saver": "^2.0.5",
"formik": "^2.2.9",
"github-markdown-css": "^5.2.0",
"graphiql": "^1.5.9",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
@ -36,7 +37,7 @@
"immer": "^9.0.15",
"know-your-http-well": "^0.5.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"markdown-it": "^13.0.2",
"mousetrap": "^1.6.5",
"nanoid": "3.3.4",
"next": "12.3.3",

View File

@ -0,0 +1,5 @@
const Editor = (props) => {
return <textarea {...props} />;
};
export default Editor;

View File

@ -0,0 +1,28 @@
import StyledMarkdownBodyWrapper from './StyledMarkdownBodyWrapper';
const MarkdownBody = (props) => {
const handleOnDoubleClick = () => {
switch (event.detail) {
case 2: {
props.OnDoubleClick();
break;
}
case 1:
default: {
break;
}
}
};
return (
<StyledMarkdownBodyWrapper theme={props.theme}>
<div
className="markdown-body"
dangerouslySetInnerHTML={{ __html: props.children }}
onClick={handleOnDoubleClick}
/>
</StyledMarkdownBodyWrapper>
);
};
export default MarkdownBody;

View File

@ -0,0 +1,49 @@
import styled from 'styled-components';
const StyledContentWrapper = styled.div`
height: calc(100vh - 100px);
margin-right: 5px;
margin-left: 5px;
background-color: ${(props) => props.theme.rightPane.bg};
.text-end {
text-align: end;
}
::-webkit-scrollbar {
width: 0px;
}
::-webkit-scrollbar-button {
display: none;
}
textarea {
height: inherit;
background: ${(props) => props.theme.bg};
color: ${(props) => props.theme.text};
border: solid 1px ${(props) => props.theme.modal.input.border};
padding: 1em;
resize: none;
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
outline: ${(props) => props.theme.modal.input.focusBorder} !important;
}
::-webkit-scrollbar {
width: 0px;
}
::-webkit-scrollbar-button {
display: none;
}
}
`;
export default StyledContentWrapper;

View File

@ -0,0 +1,80 @@
import styled from 'styled-components';
const StyledMarkdownBodyWrapper = styled.div`
height: inherit;
.markdown-body {
overflow-y: auto;
background-color: ${(props) => props.theme.rightPane.bg};
border: 1px solid ${(props) => props.theme.rightPane.border};
color: ${(props) => props.theme.text};
box-sizing: border-box;
height: 100%;
margin: 0 auto;
padding: 1em;
font-size: 0.875rem;
}
.markdown-body h2 {
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: 0.3em;
font-size: 1.125em;
border-bottom: 1px solid var(--color-border-muted);
}
.markdown-body h3 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: 1em;
}
.markdown-body h4 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: 0.875em;
}
.markdown-body h5 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: 0.85em;
}
.markdown-body h6 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: 0.8em;
color: var(--color-fg-muted);
}
.markdown-body h1 {
margin: 0.67em 0;
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: 0.3em;
font-size: 1.375em;
border-bottom: 1px solid var(--color-border-muted);
}
.markdown-body hr {
box-sizing: content-box;
overflow: hidden;
background: transparent;
border-bottom: 1px solid var(--color-border-muted);
height: 1px;
padding: 0;
margin: 24px 0;
background-color: var(--color-border-default);
border: 0;
}
.markdown-body ul {
list-style-type: disc;
}
.markdown-body ol {
list-style-type: decimal;
}
@media (max-width: 767px) {
.markdown-body {
padding: 15px;
}
}
`;
export default StyledMarkdownBodyWrapper;

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
border-left: 1px solid ${(props) => props.theme.rightPane.border};
padding-top: 2em;
height: 100%;
`;
export default StyledWrapper;

View File

@ -0,0 +1,104 @@
import { IconX } from '@tabler/icons';
import 'github-markdown-css/github-markdown.css';
import MarkdownIt from 'markdown-it';
import { updateRequestDocs } from 'providers/ReduxStore/slices/collections/index';
import { closeDocs } from 'providers/ReduxStore/slices/docs';
import { useTheme } from 'providers/Theme/index';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { findCollectionByUid, findItemInCollection } from 'utils/collections/index';
import Editor from './DocumentEditor';
import MarkdownBody from './MarkdownBody';
import StyledContentWrapper from './StyledContentWrapper';
import StyledWrapper from './StyledWrapper';
const md = new MarkdownIt();
const getItem = (collections, collectionUid, itemUid) => {
const collection = findCollectionByUid(collections, collectionUid);
if (!collection) {
return new Error('Collection not found');
}
const item = findItemInCollection(collection, itemUid);
if (!item) {
return new Error('Item not found');
}
return item;
};
const Documentation = () => {
const dispatch = useDispatch();
const themeContext = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const collections = useSelector((state) => state.collections);
const isShowDocs = useSelector((state) => state.docs.isShow);
const tab = tabs.find((tab) => tab.uid === activeTabUid);
const item = getItem(collections.collections, tab?.collectionUid, tab?.uid);
const [isEditing, setIsEditing] = useState(false);
const draftDocs = item.draft?.request?.docs;
const savedDocs = item?.request?.docs || '';
const docs = draftDocs !== undefined ? draftDocs : savedDocs;
const toggleViewMode = () => {
setIsEditing((prev) => !prev);
};
const setCloseDocs = () => {
dispatch(closeDocs());
};
const handleChange = (e) => {
dispatch(
updateRequestDocs({
itemUid: tab.uid,
collectionUid: tab.collectionUid,
docs: e.target.value
})
);
};
const htmlFromMarkdown = md.render(docs);
if (!isShowDocs) {
return null;
}
if (!tab) {
return null;
}
return (
<StyledWrapper className="h-screen" theme={themeContext.theme}>
<button className="btn absolute top-2 right-2" onClick={setCloseDocs}>
<IconX />
</button>
<div
className="inline-block m-1 mt-5 mb-0"
style={{ backgroundColor: themeContext.theme.rightPane.bg, width: '-webkit-fill-available' }}
>
<h3 className="mb-2 ml-4 font-bold float-left"> Documentation </h3>
<button className="text-end float-right mr-6 text-blue-400" onClick={toggleViewMode}>
{isEditing ? 'Preview' : 'Edit'}
</button>
</div>
<StyledContentWrapper theme={themeContext.theme}>
{isEditing ? (
<Editor className="w-full h-full" onChange={handleChange} value={docs} />
) : (
<MarkdownBody OnDoubleClick={toggleViewMode} theme={themeContext.theme}>
{htmlFromMarkdown}
</MarkdownBody>
)}
</StyledContentWrapper>
</StyledWrapper>
);
};
export default Documentation;

View File

@ -9,6 +9,7 @@ import StyledWrapper from './StyledWrapper';
import 'codemirror/theme/material.css';
import 'codemirror/theme/monokai.css';
import 'codemirror/addon/scroll/simplescrollbars.css';
import Documentation from 'components/Documentation';
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
if (!SERVER_RENDERED) {
@ -43,6 +44,7 @@ export default function Main() {
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isDragging = useSelector((state) => state.app.isDragging);
const showHomePage = useSelector((state) => state.app.showHomePage);
const showDocs = useSelector((state) => state.docs.isShow);
// Todo: write a better logging flow that can be used to log by turning on debug flag
// Enable for debugging.
@ -56,7 +58,7 @@ export default function Main() {
<div>
<StyledWrapper className={className}>
<Sidebar />
<section className="flex flex-grow flex-col overflow-auto">
<section className="flex flex-grow flex-col overflow-hidden">
{showHomePage ? (
<Welcome />
) : (
@ -66,6 +68,11 @@ export default function Main() {
</>
)}
</section>
{showDocs && (
<section className="flex flex-col w-1/4">
<Documentation />
</section>
)}
</StyledWrapper>
</div>
);

View File

@ -2,12 +2,14 @@ import { configureStore } from '@reduxjs/toolkit';
import appReducer from './slices/app';
import collectionsReducer from './slices/collections';
import tabsReducer from './slices/tabs';
import docsReducer from './slices/docs';
export const store = configureStore({
reducer: {
app: appReducer,
collections: collectionsReducer,
tabs: tabsReducer
tabs: tabsReducer,
docs: docsReducer
}
});

View File

@ -1329,6 +1329,20 @@ export const collectionsSlice = createSlice({
if (collection) {
collection.runnerResult = null;
}
},
updateRequestDocs: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
if (!item.draft) {
item.draft = cloneDeep(item);
}
item.draft.request.docs = action.payload.docs;
}
}
}
}
});
@ -1405,7 +1419,8 @@ export const {
resetRunResults,
runRequestEvent,
runFolderEvent,
resetCollectionRunner
resetCollectionRunner,
updateRequestDocs
} = collectionsSlice.actions;
export default collectionsSlice.reducer;

View File

@ -0,0 +1,22 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
isShow: false
};
export const docsSlice = createSlice({
name: 'docs',
initialState,
reducers: {
showDocs: (state) => {
state.isShow = true;
},
closeDocs: (state) => {
state.isShow = false;
}
}
});
export const { closeDocs, showDocs } = docsSlice.actions;
export default docsSlice.reducer;

View File

@ -223,6 +223,7 @@ const darkTheme = {
table: {
border: '#333',
thead: {
bg: '#4d4d4d',
color: 'rgb(204, 204, 204)'
},
striped: '#2A2D2F',
@ -233,6 +234,11 @@ const darkTheme = {
plainGrid: {
hoverBg: '#3D3D3D'
},
rightPane: {
bg: '#1e1e1e',
border: '#4f4f4f'
}
};

View File

@ -227,6 +227,7 @@ const lightTheme = {
table: {
border: '#efefef',
thead: {
bg: '#f4f4f4',
color: '#616161'
},
striped: '#f3f3f3',
@ -237,6 +238,11 @@ const lightTheme = {
plainGrid: {
hoverBg: '#f4f4f4'
},
rightPane: {
bg: '#fff',
border: '#e1e1e1'
}
};

View File

@ -384,7 +384,8 @@ export const transformRequestToSaveToFilesystem = (item) => {
script: _item.request.script,
vars: _item.request.vars,
assertions: _item.request.assertions,
tests: _item.request.tests
tests: _item.request.tests,
docs: _item.request.docs
}
};

View File

@ -116,7 +116,8 @@ const bruToJson = (bru) => {
script: _.get(json, 'script', {}),
vars: _.get(json, 'vars', {}),
assertions: _.get(json, 'assertions', []),
tests: _.get(json, 'tests', '')
tests: _.get(json, 'tests', ''),
docs: _.get(json, 'docs', '')
}
};
@ -169,7 +170,8 @@ const jsonToBru = (json) => {
res: _.get(json, 'request.vars.res', [])
},
assertions: _.get(json, 'request.assertions', []),
tests: _.get(json, 'request.tests', '')
tests: _.get(json, 'request.tests', ''),
docs: _.get(json, 'request.docs', '')
};
return jsonToBruV2(bruJson);

View File

@ -115,7 +115,8 @@ const requestSchema = Yup.object({
.strict()
.nullable(),
assertions: Yup.array().of(keyValueSchema).nullable(),
tests: Yup.string().nullable()
tests: Yup.string().nullable(),
docs: Yup.string().nullable()
})
.noUnknown(true)
.strict();