diff --git a/eslint.config.mjs b/eslint.config.mjs index b402bafb9..12c843cd7 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -20,7 +20,6 @@ export default defineConfig([ rules: { "@typescript-eslint/no-explicit-any": "off", 'react/prop-types': 'off', - 'react/no-deprecated': 'off', // We are currently using deprecated React features, so we disable this rule - this will change in the future '@stylistic/quotes': ['error', 'single'], '@stylistic/no-extra-semi': 'error', '@stylistic/semi': ['error', 'always'], diff --git a/package.json b/package.json index 63da4b860..98df9009f 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,11 @@ "popper.js": "^1.16.1", "postcss": "^8.1.0", "propagating-hammerjs": "^3.0.0", - "react": "^16.13.1", - "react-app-polyfill": "^1.0.6", - "react-dom": "^16.13.1", - "react-grid-layout": "^0.18.3", - "react-modal": "^3.11.2", + "react": "^19.1.1", + "react-bootstrap": "^2.10.10", + "react-dom": "^19.1.1", + "react-grid-layout": "^1.5.2", + "react-modal": "^3.16.3", "regenerator-runtime": "^0.14.1", "summernote": "^0.9.1", "tippy.js": "^6.3.7", @@ -56,21 +56,22 @@ "@eslint/js": "^9.31.0", "@jest/globals": "^30.0.5", "@stylistic/eslint-plugin": "^5.2.0", - "@testing-library/dom": "^10.4.1", - "@testing-library/react": "12", + "@testing-library/dom": "^10.0.0", + "@testing-library/react": "16.3.0", + "@types/form-serialize": "^0.7.4", "@types/jest": "^30.0.0", "@types/jquery": "^3.5.32", "@types/jstree": "^3.3.46", "@types/node": "^24.2.0", - "@types/react": "^17.0.41", - "@types/react-dom": "^17.0.14", + "@types/react": "^19.1.9", + "@types/react-dom": "^19.1.7", "@types/react-grid-layout": "^1.3.2", + "@types/react-modal": "^3.16.3", "@types/typeahead.js": "^0.11.6", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", "clean-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "13.0.0", - "core-js": "^3.44.0", "css-loader": "^7.1.2", "cypress": "^14.5.3", "eslint": "^9.31.0", diff --git a/src/frontend/components/button/lib/delete-button.ts b/src/frontend/components/button/lib/delete-button.ts index a947171d2..7f281ef5e 100644 --- a/src/frontend/components/button/lib/delete-button.ts +++ b/src/frontend/components/button/lib/delete-button.ts @@ -25,6 +25,7 @@ export default function createDeleteButton(element: JQuery) { element.on('click', function (e: JQuery.ClickEvent) { e.stopPropagation(); }); + /* @ts-expect-error Global function for testing */ if (window.test) throw e; } diff --git a/src/frontend/components/button/lib/submit-field-button.ts b/src/frontend/components/button/lib/submit-field-button.ts index 554fa7886..6b2629a03 100644 --- a/src/frontend/components/button/lib/submit-field-button.ts +++ b/src/frontend/components/button/lib/submit-field-button.ts @@ -132,6 +132,7 @@ export default class SubmitFieldButton { * @returns {string} The URL for the tree API */ private getURL(data: JQuery.PlainObject): string { + /* @ts-expect-error Global function for testing */ if (window.test) return ''; const devEndpoint = window.siteConfig && window.siteConfig.urls.treeApi; diff --git a/src/frontend/components/collapsible/lib/component.test.ts b/src/frontend/components/collapsible/lib/component.test.ts index 84786d424..b2ac1af55 100644 --- a/src/frontend/components/collapsible/lib/component.test.ts +++ b/src/frontend/components/collapsible/lib/component.test.ts @@ -3,7 +3,6 @@ import Collapsible from './component'; describe('Collapsible', () => { beforeEach(() => { - // Set up the HTML structure for the collapsible component document.body.innerHTML = `
diff --git a/src/frontend/components/dashboard/_dashboard.scss b/src/frontend/components/dashboard/_dashboard.scss index 60feb0826..a79c73726 100644 --- a/src/frontend/components/dashboard/_dashboard.scss +++ b/src/frontend/components/dashboard/_dashboard.scss @@ -41,12 +41,12 @@ left: 0; height: 24px; margin: auto; - font-size: 24px; + @include font-size(24px); text-align: center; } .react-grid-item .minMax { - font-size: 12px; + @include font-size(12px); } .react-grid-item .add { @@ -54,7 +54,8 @@ } .react-grid-dragHandleExample { - cursor: move; /* fallback if grab cursor is unsupported */ + cursor: move; + /* fallback if grab cursor is unsupported */ cursor: grab; } @@ -71,8 +72,19 @@ } .react-grid-item:hover { + .react-resizable-handle, .ld-widget-handlers { opacity: 1; } } + +.ld-footer-container { + .btn-group { + margin-right: $padding-base-vertical; + + &:last-of-type { + margin-right: 0; + } + } +} \ No newline at end of file diff --git a/src/frontend/components/dashboard/dashboard-graph/lib/component.js b/src/frontend/components/dashboard/dashboard-graph/lib/component.js index 77d0cf46d..49b4a8d34 100644 --- a/src/frontend/components/dashboard/dashboard-graph/lib/component.js +++ b/src/frontend/components/dashboard/dashboard-graph/lib/component.js @@ -2,21 +2,21 @@ import { do_plot_json } from '../../../graph/lib/chart'; import GraphComponent from '../../../graph/lib/component'; /** - * DashboardGraphComponent class that initializes the dashboard graph and renders the graph using do_plot_json. + * Graph component for the dashboard */ class DashboardGraphComponent extends GraphComponent { /** - * Create a DashboardGraphComponent instance. - * @param {HTMLElement} element The HTML element that this component will be attached to. - */ + * Create a new dashboard graph component + * @param {HTMLElement} element The element to attach the component to + */ constructor(element) { super(element); this.initDashboardGraph(); } /** - * Initialize the dashboard graph by rendering the graph using do_plot_json. - */ + * Initialize the dashboard graph + */ initDashboardGraph() { const $graph = $(this.element); const graph_data = $graph.data('plot-data'); diff --git a/src/frontend/components/dashboard/lib/component.js b/src/frontend/components/dashboard/lib/component.tsx similarity index 63% rename from src/frontend/components/dashboard/lib/component.js rename to src/frontend/components/dashboard/lib/component.tsx index 68ec6f9b4..9b94ca74a 100644 --- a/src/frontend/components/dashboard/lib/component.js +++ b/src/frontend/components/dashboard/lib/component.tsx @@ -1,29 +1,23 @@ import { Component } from 'component'; -import 'react-app-polyfill/stable'; - -import 'core-js/es/array/is-array'; -import 'core-js/es/map'; -import 'core-js/es/set'; -import 'core-js/es/object/define-property'; -import 'core-js/es/object/keys'; -import 'core-js/es/object/set-prototype-of'; - -import './react/polyfills/classlist'; import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './react/app'; +import ReactDOM from 'react-dom/client'; +import App from './react/App'; import ApiClient from './react/api'; +import { ReactGridLayoutProps } from 'react-grid-layout'; /** - * DashboardComponent class that initializes the dashboard and renders the App component. + * DashboardComponent class to initialize and render the dashboard */ -class DashboardComponent extends Component { +export default class DashboardComponent extends Component { + el: JQuery; + gridConfig: ReactGridLayoutProps; + /** - * Create a DashboardComponent instance. - * @param {HTMLElement} element The HTML element that this component will be attached to. + * Create a new instance of DashboardComponent + * @param {HTMLElement} element The HTML element to attach the dashboard component to */ - constructor(element) { + constructor(element: HTMLElement) { super(element); this.el = $(this.element); @@ -38,18 +32,20 @@ class DashboardComponent extends Component { } /** - * Initialize the dashboard by rendering the App component with widgets and configurations. + * Initialize the dashboard by rendering the App component */ initDashboard() { this.element.className = ''; const widgetsEls = Array.prototype.slice.call(document.querySelectorAll('#ld-app > div')); - const widgets = widgetsEls.map(el => ({ + const widgets = widgetsEls.map((el: HTMLElement) => ({ html: el.innerHTML, config: JSON.parse(el.getAttribute('data-grid')) })); const api = new ApiClient(this.element.getAttribute('data-dashboard-endpoint') || ''); - ReactDOM.render( + const root = ReactDOM.createRoot(this.element); + + root.render( , - this.element + gridConfig={this.gridConfig} /> ); } } - -export default DashboardComponent; diff --git a/src/frontend/components/dashboard/lib/react/App.tsx b/src/frontend/components/dashboard/lib/react/App.tsx new file mode 100644 index 000000000..212377b0e --- /dev/null +++ b/src/frontend/components/dashboard/lib/react/App.tsx @@ -0,0 +1,344 @@ +import React, { useEffect, useRef } from 'react'; + +import Header from './Header'; +import Footer from './Footer'; +import { sidebarObservable } from 'components/sidebar/lib/sidebarObservable'; +import DashboardView from './Dashboard/DashboardView'; +import EditModal from './EditModal/EditModal'; + +import { AppProps } from './types'; +import serialize from 'form-serialize'; +import { initializeRegisteredComponents } from 'component'; + +/** + * Create the application component + * @param {AppProps} props The application properties + * @returns {React.JSX.Element} The rendered application component + */ +export default function App(props: AppProps): React.JSX.Element { + const formRef = useRef(null); + + const [editModalOpen, setEditModalOpen] = React.useState(false); + const [editHtml, setEditHtml] = React.useState(''); + const [loadingEditHtml, setLoadingEditHtml] = React.useState(false); + const [editError, setEditError] = React.useState(''); + const [loading, setLoading] = React.useState(false); // eslint-disable-line @typescript-eslint/no-unused-vars + const [layout, setLayout] = React.useState(props.widgets.map((widget) => widget.config)); + const [widgets, setWidgets] = React.useState(props.widgets); + const [activeItem, setActiveItem] = React.useState(''); + + useEffect(() => { + sidebarObservable.addSubscriberFunction(handleSideBarChange); + + initializeGlobeComponents(); + }, []); + + useEffect(() => { + if (editModalOpen && !loadingEditHtml && formRef) { + initializeSummernoteComponent(); + } + + if (!editModalOpen && !loadingEditHtml) { + initializeComponents(); + } + }, [editModalOpen, loadingEditHtml]); + + useEffect(() => { + initializeComponents(); + }, [layout]); + + /** + * Initialize all components that need to be set up + */ + const initializeComponents = () => { + initializeRegisteredComponents(document.body); + initializeGlobeComponents(); + }; + + /** + * Update the HTML of a widget + * @param {string} id The ID of the widget to update + */ + const updateWidgetHtml = async (id: string) => { + const newHtml = await props.api.getWidgetHtml(id); + const newWidgets = widgets.map(widget => { + if (widget.config.i === id) { + return { + ...widget, + html: newHtml + }; + } + return widget; + }); + setWidgets(newWidgets); + }; + + /** + * Fetch the edit form HTML for a widget + * @param {string} id The ID of the widget to fetch the edit form for + */ + const fetchEditForm = async (id: string) => { + const editFormHtml = await props.api.getEditForm(id); + if (editFormHtml.is_error) { + setLoadingEditHtml(false); + setEditError(editFormHtml.message); + return; + } + setLoadingEditHtml(false); + setEditError(''); + setEditHtml(editFormHtml.content); + }; + + /** + * Action for the on edit click event + * @param {string} id The ID of the widget to edit + */ + const onEditClick = (id: string) => (event: React.MouseEvent) => { + event.preventDefault(); + showEditForm(id); + }; + + /** + * Show the edit form for a widget + * @param {string} id The ID of the widget to show the edit form for + */ + const showEditForm = (id) => { + setEditModalOpen(true); + setLoadingEditHtml(true); + setActiveItem(id); + fetchEditForm(id); + }; + + /** + * Close the edit modal + */ + const closeModal = () => { + setEditModalOpen(false); + }; + + /** + * Delete the active widget + */ + const deleteActiveWidget = () => { + if (!window.confirm('Deleting a widget is permanent! Are you sure?')) return; + + setWidgets(widgets.filter(item => item.config.i !== activeItem)); + setEditModalOpen(false); + props.api.deleteWidget(activeItem); + }; + + /** + * Save the active widget + * @param {*} event The submit event + */ + const saveActiveWidget = async (event: any) => { + event.preventDefault(); + const formEl = formRef.current.querySelector('form'); + if (!formEl) { + console.error('No form element was found!'); + return; + } + + const form = serialize(formEl, { hash: true }); + const result = await props.api.saveWidget(formEl.getAttribute('action'), form); + if (result.is_error) { + setEditError(result.message); + return; + } + updateWidgetHtml(activeItem); + closeModal(); + }; + + /** + * Check if the placement conflicts with existing widgets + * @param {number} x The x-coordinate of the widget + * @param {number} y The y-coordinate of the widget + * @param {number} w The width of the widget + * @param {number} h The height of the widget + * @returns {boolean} Whether the grid conflicts with existing widgets + */ + const isGridConflict = (x: number, y: number, w: number, h: number): boolean => { + const ulc = { x, y }; + const drc = { x: x + w, y: y + h }; + return layout.some((widget) => { + if (ulc.x >= (widget.x + widget.w) || widget.x >= drc.x) { + return false; + } + if (ulc.y >= (widget.y + widget.h) || widget.y >= drc.y) { + return false; + } + return true; + }); + }; + + /** + * Get the first available position for a widget + * @param {number} w The width of the widget + * @param {number} h The height of the widget + * @returns {{ x: number, y: number }} The first available position for the widget + */ + const firstAvailableSpot = (w: number, h: number): { x: number; y: number; } => { + let x = 0; + let y = 0; + while (isGridConflict(x, y, w, h)) { + if ((x + w) < props.gridConfig.cols) { + x += 1; + } else { + y += 1; + } + if (y > 200) break; + } + return { x, y }; + }; + + /** + * Add a new widget to the dashboard + * @param {string} type The type of widget to add + */ + const addWidget = async (type: string) => { + setLoading(true); + const result = await props.api.createWidget(type); + if (result.error) { + setLoading(false); + alert(result.message); + return; + } + const id = result.message; + const { x, y } = firstAvailableSpot(1, 1); + const widgetLayout = { + i: id, + x, + y, + w: 1, + h: 1 + }; + const newLayout = layout.concat(widgetLayout); + setWidgets(widgets.concat({ + config: widgetLayout, + html: 'Loading...' + })); + setLayout(newLayout); + setLoading(false); + props.api.saveLayout(props.dashboardId, newLayout); + showEditForm(id); + }; + + /** + * Triggered when the layout of the dashboard changes + * @param {*} newLayout The new layout of the dashboard + */ + const onLayoutChange = (newLayout: any) => { + if (shouldSaveLayout(layout, newLayout)) { + props.api.saveLayout(props.dashboardId, newLayout); + } + setLayout(newLayout); + }; + + /** + * Check if the layout should be saved + * @param {*} prevLayout The previous layout of the dashboard + * @param {*} newLayout The new layout of the dashboard + * @returns {boolean} Whether the layout should be saved + */ + const shouldSaveLayout = (prevLayout: any, newLayout: any): boolean => { + if (prevLayout.length !== newLayout.length) { + return true; + } + for (let i = 0; i < prevLayout.length; i += 1) { + const entriesNew = Object.entries(newLayout[i]); + const isDifferent = entriesNew.some((keypair) => { + const [key, value] = keypair; + if (key === 'moved' || key === 'static') return false; + if (value !== prevLayout[i][key]) return true; + return false; + }); + if (isDifferent) return true; + } + return false; + }; + + /** + * Overwrite the submit event listener for the form + */ + const overWriteSubmitEventListener = () => { // eslint-disable-line + const formContainer = document.getElementById('ld-form-container'); + if (!formContainer) + return; + + const form = formContainer.querySelector('form'); + if (!form) + return; + + form.addEventListener('submit', saveActiveWidget); + const submitButton = document.createElement('input'); + submitButton.setAttribute('type', 'submit'); + submitButton.setAttribute('style', 'visibility: hidden'); + form.appendChild(submitButton); + }; + + /** + * Handle sidebar changes + */ + const handleSideBarChange = () => { + window.dispatchEvent(new Event('resize')); + }; + + /** + * Initialize the Summernote component if it exists in the form + */ + const initializeSummernoteComponent = () => { + const summernoteEl = formRef.current.querySelector('.summernote'); + if (summernoteEl) { + import(/* WebpackChunkName: "summernote" */ '../../../summernote/lib/component') + .then(({ default: SummerNoteComponent }) => { + new SummerNoteComponent(summernoteEl as HTMLElement); + }); + } + }; + + /** + * Initialize the Globe components if they exist in the DOM + */ + const initializeGlobeComponents = () => { + const arrGlobe = document.querySelectorAll('.globe'); + import(/* WebpackChunkName: "globe" */ '../../../globe/lib/component').then(({ default: GlobeComponent }) => { + arrGlobe.forEach((globe) => { + new GlobeComponent(globe as HTMLElement); + }); + }); + }; + + return ( +
+ {props.hideMenu ||
} + + + {props.hideMenu ||
} +
+ ); +} diff --git a/src/frontend/components/dashboard/lib/react/Dashboard/DashboardView.test.tsx b/src/frontend/components/dashboard/lib/react/Dashboard/DashboardView.test.tsx new file mode 100644 index 000000000..ff446680d --- /dev/null +++ b/src/frontend/components/dashboard/lib/react/Dashboard/DashboardView.test.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/dom'; +import { describe, it, expect, jest } from '@jest/globals'; + +import DashboardView from './DashboardView'; +import { WidgetProps } from '../types'; +import { ReactGridLayoutProps } from 'react-grid-layout'; + +describe('DashboardView', () => { + it('Creates a dashboard', () => { + const gridConfig:ReactGridLayoutProps = { + cols: 2, + margin: [32, 32], + containerPadding: [0, 10], + rowHeight: 80 + }; + + const widgets:WidgetProps[] = [ + { + config:{ + h: 1, + i: '0', + w: 1, + x: 0, + y: 0 + }, + html: '
Widget 1
' + } + ]; + + const props = { + readOnly: false, + gridConfig, + layout: widgets.map(w => w.config), + onEditClick: jest.fn(), + onLayoutChange: jest.fn(), + widgets + }; + render(); + + expect(screen.getByTestId('widget1')).toBeInstanceOf(HTMLDivElement); + expect(screen.getByTestId('widget1').textContent).toBe('Widget 1'); + }); + + it('should trigger event on edit button click', () => { + const gridConfig:ReactGridLayoutProps = { + cols: 2, + margin: [32, 32], + containerPadding: [0, 10], + rowHeight: 80 + }; + + const widgets:WidgetProps[] = [ + { + config:{ + h: 1, + i: '0', + w: 1, + x: 0, + y: 0 + }, + html: '
Widget 1
' + } + ]; + + const props = { + readOnly: false, + gridConfig, + layout: widgets.map(w => w.config), + onEditClick: jest.fn(), + onLayoutChange: jest.fn(), + widgets + }; + render(); + + const editButton = screen.getByTestId('edit'); + expect(editButton).toBeInstanceOf(HTMLAnchorElement); + editButton.click(); + expect(props.onEditClick).toHaveBeenCalledWith('0'); + }); +}); \ No newline at end of file diff --git a/src/frontend/components/dashboard/lib/react/Dashboard/DashboardView.tsx b/src/frontend/components/dashboard/lib/react/Dashboard/DashboardView.tsx new file mode 100644 index 000000000..2a1eac1e8 --- /dev/null +++ b/src/frontend/components/dashboard/lib/react/Dashboard/DashboardView.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import RGL, { WidthProvider } from 'react-grid-layout'; +import Widget from '../Widget/Widget'; +import { DashboardViewProps } from '../types'; + +const ReactGridLayout = WidthProvider(RGL); + +/** + * Render the Dashboard view. + * @param {DashboardViewProps} param0 Dashboard properties + * @returns {React.JSX.Element} Rendered Dashboard view + */ +export default function DashboardView({ readOnly, layout, onLayoutChange, gridConfig, widgets, onEditClick }: DashboardViewProps): React.JSX.Element { + return (
+ + {widgets.map(widget => ( +
+ +
+ ))} +
+
); +} diff --git a/src/frontend/components/dashboard/lib/react/EditModal/EditModal.test.tsx b/src/frontend/components/dashboard/lib/react/EditModal/EditModal.test.tsx new file mode 100644 index 000000000..8604a45c4 --- /dev/null +++ b/src/frontend/components/dashboard/lib/react/EditModal/EditModal.test.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/dom'; +import { describe, it, expect, jest } from '@jest/globals'; + +import EditModal from './EditModal'; +import {AppModalProps} from '../types'; + +describe('EditModal', () => { + it('Creates a modal',()=>{ + const modalProps:AppModalProps = { + closeModal:()=>{}, + deleteActiveWidget:()=>{}, + editError:'', + editHtml:'', + editModalOpen:true, + formRef:React.createRef(), + loadingEditHtml:true, + saveActiveWidget:()=>{} + }; + + render( +
+
+ +
+ ); + + expect(screen.getByText('Edit widget')).toBeInstanceOf(HTMLHeadingElement); + expect(screen.getByText('Loading...')).toBeInstanceOf(HTMLSpanElement); + }); + + it('Creates a modal with the HTML content',()=>{ + const modalProps:AppModalProps = { + closeModal:()=>{}, + deleteActiveWidget:()=>{}, + editError:'', + editHtml:'
Test
', + editModalOpen:true, + formRef:React.createRef(), + loadingEditHtml:false, + saveActiveWidget:()=>{} + }; + + render( +
+
+ +
+ ); + + expect(screen.getByText('Edit widget')).toBeInstanceOf(HTMLHeadingElement); + expect(screen.getByText('Test')).toBeInstanceOf(HTMLDivElement); + }); + + it('Creates a modal with the error message',()=>{ + const modalProps:AppModalProps = { + closeModal:()=>{}, + deleteActiveWidget:()=>{}, + editError:'Error', + editHtml:'', + editModalOpen:true, + formRef:React.createRef(), + loadingEditHtml:false, + saveActiveWidget:()=>{} + }; + + render( +
+
+ +
+ ); + + expect(screen.getByText('Edit widget')).toBeInstanceOf(HTMLHeadingElement); + expect(screen.getByText('Error')).toBeInstanceOf(HTMLParagraphElement); + }); + + it('Fires the close event as expected', ()=>{ + const modalProps:AppModalProps = { + closeModal:jest.fn(), + deleteActiveWidget:jest.fn(), + editError:'', + editHtml:'', + editModalOpen:true, + formRef:React.createRef(), + loadingEditHtml:true, + saveActiveWidget:jest.fn() + }; + + render( +
+
+ +
+ ); + + screen.getByText('Close').click(); + expect(modalProps.closeModal).toHaveBeenCalled(); + }); + + it('Fires the delete event as expected', ()=>{ + const modalProps:AppModalProps = { + closeModal:jest.fn(), + deleteActiveWidget:jest.fn(), + editError:'', + editHtml:'', + editModalOpen:true, + formRef:React.createRef(), + loadingEditHtml:true, + saveActiveWidget:jest.fn() + }; + + render( +
+
+ +
+ ); + + screen.getByText('Delete').click(); + expect(modalProps.deleteActiveWidget).toHaveBeenCalled(); + }); + + it('Fires the save event as expected', ()=>{ + const modalProps:AppModalProps = { + closeModal:jest.fn(), + deleteActiveWidget:jest.fn(), + editError:'', + editHtml:'', + editModalOpen:true, + formRef:React.createRef(), + loadingEditHtml:true, + saveActiveWidget:jest.fn() + }; + + render( +
+
+ +
+ ); + + screen.getByText('Save').click(); + expect(modalProps.saveActiveWidget).toHaveBeenCalled(); + }); +}); diff --git a/src/frontend/components/dashboard/lib/react/EditModal/EditModal.tsx b/src/frontend/components/dashboard/lib/react/EditModal/EditModal.tsx new file mode 100644 index 000000000..c9b1237a2 --- /dev/null +++ b/src/frontend/components/dashboard/lib/react/EditModal/EditModal.tsx @@ -0,0 +1,69 @@ +/* eslint-disable @stylistic/semi */ +import React, { useEffect } from 'react'; +import Modal, {Styles} from 'react-modal'; +import { AppModalProps } from '../types'; + +/** + * Edit modal component + * @param {AppModalProps} props - The modal props + * @returns {React.JSX.Element} The rendered modal component + * @todo: A lot of state here - I think I will revisit this later + */ +export default function EditModal({ editModalOpen, closeModal, editError, loadingEditHtml, editHtml, formRef, deleteActiveWidget, saveActiveWidget }:AppModalProps): React.JSX.Element { + const modalStyle: Styles = { + content: { + minWidth: '350px', + maxWidth: '80vw', + maxHeight: '90vh', + top: '50%', + left: '50%', + right: 'auto', + bottom: 'auto', + marginRight: '-50%', + transform: 'translate(-50%, -50%)', + msTransform: 'translate(-50%, -50%)', + padding: '2rem 1.5rem' + }, + overlay: { + zIndex: 1030, + background: 'rgba(0, 0, 0, .15)' + } + }; + + useEffect(() => { + Modal.setAppElement('#ld-app'); + }, []); + + /* @ts-expect-error Global function for testing */ + const test = window.test; + + return ( +
+
+

Edit widget

+
+ +
+
+ {editError + &&

{editError}

} + {loadingEditHtml + ? Loading... :
} +
+
+
+ +
+
+ +
+
+ ) +} diff --git a/src/frontend/components/dashboard/lib/react/Footer.test.tsx b/src/frontend/components/dashboard/lib/react/Footer.test.tsx index 638188a46..bc34be58c 100644 --- a/src/frontend/components/dashboard/lib/react/Footer.test.tsx +++ b/src/frontend/components/dashboard/lib/react/Footer.test.tsx @@ -4,12 +4,13 @@ import '@testing-library/dom'; import { describe, it, expect, jest } from '@jest/globals'; import Footer from './Footer'; +import { FooterProps } from './types'; import 'testing/extensions'; describe('Footer', () => { it('Creates a footer', () => { - const footerProps = { + const footerProps: FooterProps = { addWidget: jest.fn(), currentDashboard: { name: 'Dashboard 1', @@ -24,16 +25,16 @@ describe('Footer', () => { render(