Skip to content
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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
"@stylistic/eslint-plugin": "^5.10.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@types/bootstrap": "^5.2.11",
"@types/form-serialize": "^0.7.4",
"@types/jest": "^30.0.0",
"@types/jquery": "^4.0.1",
"@types/jstree": "^3.3.46",
Expand Down Expand Up @@ -90,7 +92,7 @@
"sass-loader": "^17.0.0",
"terser-webpack-plugin": "^5.6.1",
"ts-loader": "~9.5.2",
"typescript": "~5.8.0",
"typescript": "~6.0.0",
"typescript-eslint": "^8.61.1",
"webpack": "^5.107.2",
"webpack-cli": "^7.0.3"
Expand Down
1 change: 0 additions & 1 deletion src/frontend/components/bootstrap-popover/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ import { initializeComponent } from "component";
import BootstrapPopoverComponent from "./lib/component";

export default (scope:any) =>{
//@ts-expect-error Typings on initializeComponent are incorrect
initializeComponent(scope, "[data-bs-toggle=\"popover\"]", BootstrapPopoverComponent);
};
2 changes: 1 addition & 1 deletion src/frontend/components/button/lib/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class ButtonComponent extends Component {
if (!this.buttonsMap) throw "Buttons map is not initialized";
if (!this.buttonsMap.has(className)) return;
this.linkedClasses.push(className);
this.buttonsMap.get(className)(el);
this.buttonsMap.get(className)?.(el);
});
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/frontend/components/button/lib/delete-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ export default function createDeleteButton(element: JQuery<HTMLElement>) {
element.on("click", function (e: JQuery.ClickEvent) {
e.stopPropagation();
});
if (window.test) throw e;
// TS6 is a bit odd - it recognises window.test as a function and wants to call it so we resort to typechecks
if (typeof window.test !== "undefined") throw e;
}

$deleteModal.find(".modal-title").text(modalTitle);
$deleteModal.find("button[type=submit]").val(id);
$deleteModal.find("button[type=submit]").val(id!);
});
}
4 changes: 1 addition & 3 deletions src/frontend/components/button/lib/remove-unload-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,5 @@
* @param {JQuery<HTMLElement>} element The button element to add the click event to
*/
export default function createRemoveUnloadButton(element: JQuery<HTMLElement>) {
element.on("click", () => {
$(window).off("beforeunload");
});
element.on("click", () => $(window).off("beforeunload"));
}
21 changes: 10 additions & 11 deletions src/frontend/components/button/lib/rename-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ declare global {
*/
class RenameButton {
private readonly dataClass = "rename-button";
private value: string;
private value?: string;

/**
* Attach event to button
Expand Down Expand Up @@ -162,7 +162,7 @@ class RenameButton {
private triggerRename(id: number, button: JQuery<HTMLButtonElement>) {
const previousValue = $(`#current-${id}`).text();
const extension = "." + previousValue.split(".").pop();
const newName = this.value.endsWith(extension) ? this.value : this.value + extension;
const newName = this.value?.endsWith(extension) ? this.value : this.value + extension;
if (newName === "" || newName === previousValue) return;
$(`#current-${id}`).text(newName);
const event = $.Event("rename", { oldName: previousValue, newName, target: button });
Expand Down Expand Up @@ -195,14 +195,13 @@ class RenameButton {
}
}

if (typeof jQuery !== "undefined") {
(function ($) {
$.fn.renameButton = function () {
return this.each(function (_: unknown, el: HTMLButtonElement) {
new RenameButton(el);
});
};
})(jQuery);
}
(function ($) {
$.fn.renameButton = function () {
return this.each(function (_: unknown, el: HTMLElement) {
if (!(el instanceof HTMLButtonElement)) return;
new RenameButton(el);
});
};
})(jQuery);

export { RenameEvent };
4 changes: 2 additions & 2 deletions src/frontend/components/button/lib/save-view-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ export default function createSaveViewButtonComponent(el: JQuery<HTMLElement>) {
const $input = $form.find("input[type=hidden][name=group_id]");
if ((ev.target as HTMLInputElement)?.checked) {
$input.attr("required", "required");
if ($dropdown && $dropdown.attr && $dropdown.attr("placeholder") && $dropdown.attr("placeholder").match(/All [Uu]sers/)) $dropdown.addClass("select--required");
if ($dropdown?.attr("placeholder")?.match(/All [Uu]sers/)) $dropdown.addClass("select--required");
} else {
$input.removeAttr("required");
if ($dropdown && $dropdown.attr && $dropdown.attr("placeholder") && $dropdown.attr("placeholder").match(/All [Uu]sers/)) $dropdown.removeClass("select--required");
if ($dropdown?.attr("placeholder")?.match(/All [Uu]sers/)) $dropdown.removeClass("select--required");
}
});
el.on("click", (ev) => {
Expand Down
7 changes: 3 additions & 4 deletions src/frontend/components/button/lib/submit-field-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ declare global {
treeApi: string;
}
}
UpdateFilter?: (filterEl: JQuery<HTMLElement> | undefined, ev: JQuery.ClickEvent) => void;
}
interface JQuery<TElement = HTMLElement> {
queryBuilder(operation: string): JQuery<TElement>;
Expand Down Expand Up @@ -85,7 +86,6 @@ export default class SubmitFieldButton {
}

if (bUpdateTree) {
//Bit of typecasting here, purely because the jstree plugin doesn't have types
const v = $jstreeEl.jstree(true).get_json("#", { flat: false });
const mytext = JSON.stringify(v);
const data = $jstreeEl.data();
Expand All @@ -100,9 +100,7 @@ export default class SubmitFieldButton {
});
}

// @ts-expect-error - This is a global function
if (bUpdateFilter && window.UpdateFilter) {
// @ts-expect-error - This is a global function
window.UpdateFilter($filterEl, ev);
}

Expand Down Expand Up @@ -131,7 +129,8 @@ export default class SubmitFieldButton {
* @returns {string} The URL for the tree API
*/
private getURL(data: JQuery.PlainObject): string {
if (window.test) return "";
// TS6 is a bit odd - it recognises window.test as a function and wants to call it so we resort to typechecks
if (typeof window.test !== "undefined") return "";

const devEndpoint = window.siteConfig && window.siteConfig.urls.treeApi;

Expand Down
4 changes: 2 additions & 2 deletions src/frontend/components/dashboard/lib/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default class DashboardComponent extends Component {
const widgetsEls = Array.prototype.slice.call(document.querySelectorAll("#ld-app > div"));
const widgets = widgetsEls.map((el: HTMLElement) => ({
html: el.innerHTML,
config: JSON.parse(el.getAttribute("data-grid"))
config: JSON.parse(el.getAttribute("data-grid")!)
}));
const api = new ApiClient(this.element.getAttribute("data-dashboard-endpoint") || "");

Expand All @@ -48,7 +48,7 @@ export default class DashboardComponent extends Component {
root.render(
<App
widgets={widgets}
dashboardId={this.element.getAttribute("data-dashboard-id")}
dashboardId={this.element.getAttribute("data-dashboard-id")!}
currentDashboard={JSON.parse(this.element.getAttribute("data-current-dashboard") || "{}")}
readOnly={this.element.getAttribute("data-dashboard-read-only") === "true"}
hideMenu={this.element.getAttribute("data-dashboard-hide-menu") === "true"}
Expand Down
24 changes: 12 additions & 12 deletions src/frontend/components/dashboard/lib/react/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from "react";
import React, { RefObject, useEffect, useRef } from "react";

import Header from "./Header";
import Footer from "./Footer";
Expand Down Expand Up @@ -81,12 +81,12 @@ export default function App(props: AppProps): React.JSX.Element {
const editFormHtml = await props.api.getEditForm(id);
if (editFormHtml.is_error) {
setLoadingEditHtml(false);
setEditError(editFormHtml.message);
setEditError(editFormHtml.message!);
return;
}
setLoadingEditHtml(false);
setEditError("");
setEditHtml(editFormHtml.content);
setEditHtml(editFormHtml.content!);
};

/**
Expand All @@ -102,7 +102,7 @@ export default function App(props: AppProps): React.JSX.Element {
* Show the edit form for a widget
* @param {string} id The ID of the widget to show the edit form for
*/
const showEditForm = (id) => {
const showEditForm = (id: string) => {
setEditModalOpen(true);
setLoadingEditHtml(true);
setActiveItem(id);
Expand Down Expand Up @@ -133,16 +133,16 @@ export default function App(props: AppProps): React.JSX.Element {
*/
const saveActiveWidget = async (event: any) => {
event.preventDefault();
const formEl = formRef.current.querySelector("form");
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);
const result = await props.api.saveWidget(formEl.getAttribute("action")!, form);
if (result.is_error) {
setEditError(result.message);
setEditError(result.message!);
return;
}
updateWidgetHtml(activeItem);
Expand Down Expand Up @@ -181,7 +181,7 @@ export default function App(props: AppProps): React.JSX.Element {
let x = 0;
let y = 0;
while (isGridConflict(x, y, w, h)) {
if ((x + w) < props.gridConfig.cols) {
if ((x + w) < props.gridConfig.cols!) {
x += 1;
} else {
y += 1;
Expand All @@ -203,7 +203,7 @@ export default function App(props: AppProps): React.JSX.Element {
alert(result.message);
return;
}
const id = result.message;
const id = result.message!;
const { x, y } = firstAvailableSpot(1, 1);
const widgetLayout = {
i: id,
Expand Down Expand Up @@ -287,7 +287,7 @@ export default function App(props: AppProps): React.JSX.Element {
* Initialize the Summernote component if it exists in the form
*/
const initializeSummernoteComponent = () => {
const summernoteEl = formRef.current.querySelector(".summernote");
const summernoteEl = formRef.current!.querySelector(".summernote");
if (summernoteEl) {
import(/* WebpackChunkName: "summernote" */ "../../../summernote/lib/component")
.then(({ default: SummerNoteComponent }) => {
Expand All @@ -311,7 +311,7 @@ export default function App(props: AppProps): React.JSX.Element {
return (
<div className="content-block">
{props.hideMenu || <Header
hMargin={props.gridConfig.containerPadding[0]}
hMargin={props.gridConfig.containerPadding![0]}
dashboards={props.dashboards}
currentDashboard={props.currentDashboard}
includeH1={props.includeH1}
Expand All @@ -322,7 +322,7 @@ export default function App(props: AppProps): React.JSX.Element {
editError={editError}
editHtml={editHtml}
editModalOpen={editModalOpen}
formRef={formRef}
formRef={formRef as RefObject<HTMLDivElement>}
loadingEditHtml={loadingEditHtml}
saveActiveWidget={saveActiveWidget} />
<DashboardView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function DashboardView({ readOnly, layout, onLayoutChange, gridCo
>
{widgets.map(widget => (
<div key={widget.config.i} className={`ld-widget-container ${readOnly || widget.config.static ? "" : "ld-widget-container--editable"}`}>
<Widget key={widget.config.i} html={widget.html} readOnly={readOnly || widget.config.static} onEditClick={onEditClick(widget.config.i)} />
<Widget key={widget.config.i} html={widget.html} readOnly={readOnly || widget.config.static || false} onEditClick={onEditClick(widget.config.i)} />
</div>
))}
</ReactGridLayout>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { RefObject } from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/dom';
import { describe, it, expect, jest } from '@jest/globals';
Expand All @@ -14,7 +14,7 @@ describe('EditModal', () => {
editError:'',
editHtml:'',
editModalOpen:true,
formRef:React.createRef(),
formRef:React.createRef() as RefObject<HTMLDivElement>,
loadingEditHtml:true,
saveActiveWidget:()=>{}
};
Expand All @@ -37,7 +37,7 @@ describe('EditModal', () => {
editError:'',
editHtml:'<div>Test</div>',
editModalOpen:true,
formRef:React.createRef(),
formRef:React.createRef() as RefObject<HTMLDivElement>,
loadingEditHtml:false,
saveActiveWidget:()=>{}
};
Expand All @@ -60,7 +60,7 @@ describe('EditModal', () => {
editError:'Error',
editHtml:'',
editModalOpen:true,
formRef:React.createRef(),
formRef:React.createRef() as RefObject<HTMLDivElement>,
loadingEditHtml:false,
saveActiveWidget:()=>{}
};
Expand All @@ -83,7 +83,7 @@ describe('EditModal', () => {
editError:'',
editHtml:'',
editModalOpen:true,
formRef:React.createRef(),
formRef:React.createRef() as RefObject<HTMLDivElement>,
loadingEditHtml:true,
saveActiveWidget:jest.fn()
};
Expand All @@ -106,7 +106,7 @@ describe('EditModal', () => {
editError:'',
editHtml:'',
editModalOpen:true,
formRef:React.createRef(),
formRef:React.createRef() as RefObject<HTMLDivElement>,
loadingEditHtml:true,
saveActiveWidget:jest.fn()
};
Expand All @@ -129,7 +129,7 @@ describe('EditModal', () => {
editError:'',
editHtml:'',
editModalOpen:true,
formRef:React.createRef(),
formRef:React.createRef() as RefObject<HTMLDivElement>,
loadingEditHtml:true,
saveActiveWidget:jest.fn()
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default function EditModal({ editModalOpen, closeModal, editError, loadin
{editError
&& <p className="alert alert-danger">{editError}</p>}
{loadingEditHtml
? <span className='ld-modal__loading'>Loading...</span> : <div ref={formRef} dangerouslySetInnerHTML={{ __html: editHtml }} />}
? <span className='ld-modal__loading'>Loading...</span> : <div ref={formRef} dangerouslySetInnerHTML={{ __html: editHtml! }} />}
</div>
<div className='modal-footer'>
<div className='modal-footer__left'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { describe, it, expect, jest } from '@jest/globals';
import Footer from './Footer';
import { FooterProps } from './types';

//@ts-ignore
import 'testing/extensions';

describe('Footer', () => {
Expand Down
5 changes: 3 additions & 2 deletions src/frontend/components/dashboard/lib/react/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export default class ApiClient {
* @param {T} body The body of the request
* @returns {Promise<Response>} A promise that resolves to the response of the fetch request.
*/
POST<T extends object = object>(route: string, body: T): Promise<Response> { return this._fetch(route, "POST", body); }
POST<T extends object = object>(route: string, body: T | null): Promise<Response> { return this._fetch(route, "POST", body ?? {}); }

/**
* Perform a PUT request to the API.
Expand Down Expand Up @@ -100,6 +100,7 @@ export default class ApiClient {
const strippedLayout = layout.map(widget => ({ ...widget, moved: undefined }));
return this.PUT(`/dashboard/${id}`, strippedLayout);
}
return Promise.resolve(new Response());
};

/**
Expand Down Expand Up @@ -127,7 +128,7 @@ export default class ApiClient {
* @param {string} id The ID of the widget to delete.
* @returns {Promise<Response>} A promise that resolves to the response of the delete request.
*/
deleteWidget = (id: string): Promise<Response> => !this.isDev && this.DELETE(`/widget/${id}`);
deleteWidget = (id: string): Promise<Response> => this.isDev ? Promise.reject("Running in dev mode") : this.DELETE(`/widget/${id}`);

/**
* Get the edit form for a widget.
Expand Down
Loading
Loading