Skip to content
Snippets Groups Projects
Commit 6c10a9e4 authored by matthiasf's avatar matthiasf
Browse files

add menu provider

parent 5645c7ed
No related branches found
No related tags found
2 merge requests!1162Draft: Ui integration,!1128UI: Implement yang model view
......@@ -4,8 +4,14 @@
"form": {
"submit": "Submit",
"empty_field": "This field can´t be empty"
},
"toast": {
"copied": "Copied to clipboard"
}
},
"json_viewer": {
"copy": "Copy"
},
"login": {
"form": {
"failed": "The username or password is invalid",
......
import { MenuProvider } from '@provider/menu/menu.provider'
import i18next from 'i18next'
import React from 'react'
import ReactDOM from 'react-dom/client'
......@@ -25,8 +26,10 @@ ReactDOM.createRoot(document.getElementById("root")).render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<I18nextProvider i18n={i18next}>
{installToastify()}
<RouterProvider router={router} />
<MenuProvider>
{installToastify()}
<RouterProvider router={router} />
</MenuProvider>
</I18nextProvider>
</PersistGate>
</Provider>
......
......@@ -11,7 +11,7 @@
color: lighten(map-get($map: $theme-colors, $key: "black"), 20%) !important;
background-color: white !important;
border: 0;
padding: 0.1em 0 !important;
padding: 0.2em 0 !important;
}
& > td:nth-child(2) {
......@@ -21,6 +21,19 @@
&:hover > td {
background-color: map-get($theme-colors, "primary::hover") !important;
}
&:hover .icons {
color: map-get($theme-colors, "black") !important;
opacity: 100%;
transition: gap 0.3s;
gap: 0.7em;
}
& > .text-end {
vertical-align: middle;
padding-top: 0 !important;
padding-right: 5px !important;
}
}
.list-item-td.object {
......@@ -28,10 +41,6 @@
color: map-get($map: $theme-colors, $key: "black") !important;
}
&:not(:first-child) > td {
padding-top: 0.5em !important;
}
&:hover {
cursor: pointer;
}
......@@ -45,3 +54,11 @@
td .icon {
font-size: 0.8em;
}
.icons {
color: lighten(map-get($map: $theme-colors, $key: "dark"), 20%);
gap: 0.5em;
opacity: 0%;
min-width: 3em;
}
import { faAlignRight } from "@fortawesome/free-solid-svg-icons"
import { faAlignRight, faCopy, faPenToSquare, faTrashCan } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { useAppDispatch } from "@hooks"
import React, { Suspense, useMemo } from "react"
import { useMenu } from "@provider/menu/menu.provider"
import { toClipboard } from "@utils/functions"
import React, { Suspense, useEffect, useMemo, useRef } from "react"
import { Table } from "react-bootstrap"
import { useTranslation } from "react-i18next"
import { useJsonViewer } from "../viewmodel/json_viewer.viewmodel"
import './json_viewer.scss'
......@@ -12,7 +14,36 @@ type JsonViewerProbs = {
export const JsonViewer = ({ json }: JsonViewerProbs) => {
const { getSubset, breadcrumbs, isCollapsed, collapseable, collapse } = useJsonViewer();
const dispatch = useAppDispatch();
const { subscribe } = useMenu();
const htmlContainer = useRef(null);
const { t } = useTranslation('common');
useEffect(() => {
if (htmlContainer.current) {
const subscription = subscribe({
target: htmlContainer.current,
actions: [
{
key: t('json_viewer.copy'),
icon: faCopy,
action: (clickedElement) => {
let parent = clickedElement;
while (parent && parent.tagName !== 'TR') {
parent = parent.parentNode;
}
const copyValue = parent.dataset.copyValue
toClipboard(copyValue)
}
}
]
})
return () => {
subscription.unsubscribe();
}
}
}, [])
const breadcrumbHTML = useMemo(() => {
return (
......@@ -46,10 +77,15 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => {
return (
<React.Fragment key={`${nested}-${key}`}>
<tr className={"list-item-td " + key + " " + nested + " " + (isObject ? 'object' : '')} onClick={() => { isObject ? collapse(key, nested, value) : null }} >
<tr className={"list-item-td " + key + " " + nested + " " + (isObject ? 'object' : '')} data-copy-value={readableValue} onClick={() => { isObject ? collapse(key, nested, value) : null }} >
<td style={{ marginLeft: tabs + 'em' }} className={"d-flex align-items-center "}>{icon}<span>&ensp;{key}</span></td>
<td>{readableValue}</td>
<td className="text-end">comands</td>
<td className="text-end">
<div className="d-flex icons justify-content-end align-items-center">
<FontAwesomeIcon icon={faPenToSquare} size="sm" />
<FontAwesomeIcon icon={faTrashCan} size="sm" />
</div>
</td>
</tr >
{isObject && collapsed ? renderInner(value, nested + 1) : ''}
</React.Fragment >
......@@ -83,7 +119,7 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => {
}, [json, collapseable])
return (
<div>
<div ref={htmlContainer}>
{breadcrumbHTML}
{hierarchyHTML}
</div>
......
.menu-container {
box-shadow: 0px 0px 5px gray;
border-radius: 4px !important;
}
.menu-button {
& > span {
margin-left: 10px;
}
}
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
import './menu.provider.scss';
interface MenuSubscription {
unsubscribe: () => void
}
// describes a action decorated with a item name
// on click the action is getting executed
type Action = {
key: string,
icon: IconDefinition,
action: (clickedHtmlElement: HTMLElement | undefined) => void
}
interface MenuProviderType {
subscribe: (value: SubscriptionValue) => MenuSubscription
}
const MenuContext = createContext<MenuProviderType>({
subscribe: function (value: SubscriptionValue): MenuSubscription {
throw new Error("Function not implemented.");
}
})
interface SubscriptionValue {
target: HTMLElement,
actions: Array<Action>
}
export const MenuProvider = ({ children }) => {
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
const [showMenu, setShowMenu] = useState(false);
const [subscribedTargets, setSubscribedTargets] = useState<Array<SubscriptionValue>>([])
const [menuItems, setMenuItems] = useState<Array<SubscriptionValue>>([]);
const [clickedHtmlElement, setClickedHtmlElement] = useState<HTMLElement>()
const handleContextMenu = (event) => {
event.preventDefault();
const targets = subscribedTargets.filter(({ target }) => target.contains(event.target))
setMenuPosition({ top: event.pageY, left: event.pageX });
setMenuItems(targets)
setClickedHtmlElement(event.target)
displayMenu()
};
const displayMenu = () => {
setShowMenu(true);
}
const hideMenu = () => {
setShowMenu(false);
setMenuItems([]);
}
const handleClick = () => hideMenu();
useEffect(() => {
document.addEventListener('keyup', (e) => {
if (e.code === "Escape") {
hideMenu();
}
});
}, [])
const value = useMemo<MenuProviderType>(() => {
return {
subscribe(target) {
const index = subscribedTargets.length;
setSubscribedTargets([...subscribedTargets, target])
const subscription: MenuSubscription = {
unsubscribe() {
setSubscribedTargets([...subscribedTargets.splice(index, 1)])
},
}
return subscription
},
} as MenuProviderType
}, [])
return (
<MenuContext.Provider value={value}>
<div onContextMenu={handleContextMenu} onClick={handleClick} style={{ height: "100vh" }}>
<div
className={`menu-container dropdown-menu ${showMenu ? "show" : ""}`}
style={{ position: "absolute", top: `${menuPosition.top}px`, left: `${menuPosition.left}px` }}
>
{
menuItems.map((item, i) => {
// for each new action array (for each new subscription entity) draw a seperator line except the last action
const seperator = i < menuItems.length - 1 ? (<li><hr className="dropdown-divider"></hr></li>) : (<React.Fragment key={i}></React.Fragment>)
const dropdownItems = item.actions.map(({ action, icon, key }) => {
const disabled = !(clickedHtmlElement instanceof HTMLElement) || !clickedHtmlElement?.textContent
return (
<button className="menu-button dropdown-item" key={key + " " + i} disabled={disabled} onClick={() => action(clickedHtmlElement)}>
<FontAwesomeIcon icon={icon} size="sm" />
<span>{key}</span>
</button>
)
})
return [...dropdownItems, seperator]
})
}
</div>
{children}
</div>
</MenuContext.Provider>
)
}
export const useMenu = () => {
return useContext(MenuContext)
}
\ No newline at end of file
import { t } from "i18next";
import { toast } from "react-toastify";
export const toClipboard = (text: string) => {
navigator.clipboard.writeText(text);
toast.info(t('global.toast.copied'))
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment