diff --git a/eslint.config.mjs b/eslint.config.mjs index 62c1c1a35..b402bafb9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,16 +5,17 @@ import pluginReact from "eslint-plugin-react"; import css from "@eslint/css"; import { defineConfig } from "eslint/config"; import stylistic from "@stylistic/eslint-plugin"; +import jsdoc from "eslint-plugin-jsdoc"; export default defineConfig([ { settings: { react: { version: "detect" } } }, { ignores: ["*.cjs", "eslint.config.mjs", "**/public/**", "**/node_modules/**", "**/cypress/**", "cypress.config.ts", ".stylelintrc.js", "src/frontend/testing/**", "src/frontend/css/stylesheets/external/**", "src/frontend/components/dashboard/lib/react/polyfills/**", "babel.config.js", "webpack.config.js", "jest.config.js", "tsconfig.json", "src/frontend/js/lib/jqplot/**", "src/frontend/js/lib/jquery/**", "src/frontend/js/lib/plotly/**", "src/frontend/components/timeline/**", "fengari-web.js"] }, { files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], plugins: { js }, extends: ["js/recommended"] }, - { files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], languageOptions: { globals: { ...globals.browser, ...globals.jquery } } }, + { files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], languageOptions: { globals: { ...globals.browser, ...globals.jquery, ...globals.jest } } }, tseslint.configs.recommended, pluginReact.configs.flat.recommended, { files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] }, - { plugins: {'@stylistic': stylistic} }, + { plugins: {'@stylistic': stylistic, jsdoc} }, { rules: { "@typescript-eslint/no-explicit-any": "off", @@ -25,7 +26,19 @@ export default defineConfig([ '@stylistic/semi': ['error', 'always'], '@stylistic/curly-newline': 'error', '@stylistic/indent': ['error', 4], - '@stylistic/comma-dangle': ['error', 'never'] + '@stylistic/comma-dangle': ['error', 'never'], + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false + } + } + ], } } ]); diff --git a/package.json b/package.json index 00b471fd8..2bd6bc20f 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "css-loader": "^3.2.0", "cypress": "^13.7.2", "eslint": "^9.31.0", + "eslint-plugin-jsdoc": "^52.0.0", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", "jest": "^29.7.0", diff --git a/src/frontend/components/alert/lib/alertBase.ts b/src/frontend/components/alert/lib/alertBase.ts index 9f5b0c617..d9550dfb9 100644 --- a/src/frontend/components/alert/lib/alertBase.ts +++ b/src/frontend/components/alert/lib/alertBase.ts @@ -1,6 +1,9 @@ import { Hidable, Renderable } from 'util/renderable'; import { AlertType } from './types'; +/** + * Base class for alert components that can be rendered to the DOM and hidden when necessary. + */ export abstract class AlertBase extends Hidable implements Renderable { /** * Create an instance of AlertBase. diff --git a/src/frontend/components/alert/lib/dangerAlert.ts b/src/frontend/components/alert/lib/dangerAlert.ts index af77e65f3..b853554e3 100644 --- a/src/frontend/components/alert/lib/dangerAlert.ts +++ b/src/frontend/components/alert/lib/dangerAlert.ts @@ -1,5 +1,8 @@ -import { AlertBase } from "./alertBase"; +import { AlertBase } from './alertBase'; +/** + * Class representing a danger alert. + */ export class DangerAlert extends AlertBase { /** * Create an instance of InfoAlert. diff --git a/src/frontend/components/alert/lib/infoAlert.ts b/src/frontend/components/alert/lib/infoAlert.ts index 6a9214dbb..1eff8111e 100644 --- a/src/frontend/components/alert/lib/infoAlert.ts +++ b/src/frontend/components/alert/lib/infoAlert.ts @@ -1,10 +1,12 @@ import { AlertBase } from './alertBase'; +/** + * InfoAlert class represents an informational alert in the application. + */ export class InfoAlert extends AlertBase { /** * Create an instance of InfoAlert. * This class extends AlertBase to provide a specific implementation for info alerts. - * It uses the AlertType.INFO to set the alert type. * @class * @public * @memberof alert.lib @@ -12,6 +14,6 @@ export class InfoAlert extends AlertBase { * @param {string} message - The message to be displayed in the info alert. */ constructor(message: string) { - super(message, "info"); + super(message, 'info'); } -} \ No newline at end of file +} diff --git a/src/frontend/components/alert/lib/successAlert.ts b/src/frontend/components/alert/lib/successAlert.ts index ee49396b6..99fdc280c 100644 --- a/src/frontend/components/alert/lib/successAlert.ts +++ b/src/frontend/components/alert/lib/successAlert.ts @@ -1,5 +1,8 @@ -import { AlertBase } from "./alertBase"; +import { AlertBase } from './alertBase'; +/** + * Class representing a success alert. +*/ export class SuccessAlert extends AlertBase { /** * Create an instance of InfoAlert. diff --git a/src/frontend/components/alert/lib/warningAlert.ts b/src/frontend/components/alert/lib/warningAlert.ts index 2dce92e08..9a668c638 100644 --- a/src/frontend/components/alert/lib/warningAlert.ts +++ b/src/frontend/components/alert/lib/warningAlert.ts @@ -1,10 +1,12 @@ -import { AlertBase } from "./alertBase"; +import { AlertBase } from './alertBase'; +/** + * Class representing a warning alert. This class extends AlertBase to provide a specific implementation for warning alerts. + */ export class WarningAlert extends AlertBase { /** * Create an instance of InfoAlert. * This class extends AlertBase to provide a specific implementation for info alerts. - * It uses the AlertType.INFO to set the alert type. * @class * @public * @memberof alert.lib @@ -12,6 +14,6 @@ export class WarningAlert extends AlertBase { * @param {string} message - The message to be displayed in the info alert. */ constructor(message: string) { - super(message, "warning"); + super(message, 'warning'); } } diff --git a/src/frontend/components/button/lib/RenderableButton.ts b/src/frontend/components/button/lib/RenderableButton.ts index b8004011a..b67b43559 100644 --- a/src/frontend/components/button/lib/RenderableButton.ts +++ b/src/frontend/components/button/lib/RenderableButton.ts @@ -1,18 +1,31 @@ -import { Renderable } from "util/renderable"; +import { Renderable } from 'util/renderable'; +/** + * A simple button component that can be rendered to the DOM. It takes a text, an onClick handler, and an optional list of CSS classes. + */ export class RenderableButton implements Renderable { classList: string[] = []; + /** + * Creates a new RenderableButton instance. + * @param text The text to display in the button + * @param onClick The onclick listener for when the button is clicked + * @param classList Any classes to add to the button + */ constructor(private readonly text: string, private readonly onClick: (ev: MouseEvent)=>void, ...classList: string[]) { this.classList = classList; } + /** + * Renders the button to an HTMLButtonElement. + * @returns A button element to attach to the DOM + */ render(): HTMLButtonElement { const button = document.createElement('button'); button.textContent = this.text; button.addEventListener('click', this.onClick); button.classList.add(...this.classList, 'btn'); - const btnType = this.classList.find(b=>b.startsWith('btn-')) ? '' : 'btn-default' + const btnType = this.classList.find(b=>b.startsWith('btn-')) ? '' : 'btn-default'; if(btnType) { button.classList.add(btnType); } diff --git a/src/frontend/components/button/lib/cancel-button.ts b/src/frontend/components/button/lib/cancel-button.ts index d7c7186a2..c9ba511cd 100644 --- a/src/frontend/components/button/lib/cancel-button.ts +++ b/src/frontend/components/button/lib/cancel-button.ts @@ -1,5 +1,10 @@ import { clearSavedFormValues } from './common'; +/** + * Create a cancel button that navigates away from the page + * This component will navigate away to the parameter defined in the data-href attribute, or will navigate back + * @param { HTMLElement | JQuery } el The button element + */ export default function createCancelButton(el: HTMLElement | JQuery) { const $el = $(el); if ($el[0].tagName !== 'BUTTON') return; @@ -12,4 +17,4 @@ export default function createCancelButton(el: HTMLElement | JQuery else window.history.back(); }); -} \ No newline at end of file +} diff --git a/src/frontend/components/button/lib/common.test.ts b/src/frontend/components/button/lib/common.test.ts index 424e19510..ee0c1c25e 100644 --- a/src/frontend/components/button/lib/common.test.ts +++ b/src/frontend/components/button/lib/common.test.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from '@jest/globals'; import { layoutId, recordId, table_key } from './common'; describe('Common button tests', () => { diff --git a/src/frontend/components/button/lib/common.ts b/src/frontend/components/button/lib/common.ts index 90a277767..970a97fec 100644 --- a/src/frontend/components/button/lib/common.ts +++ b/src/frontend/components/button/lib/common.ts @@ -2,7 +2,6 @@ import StorageProvider from 'util/storageProvider'; /** * Clear all saved form values for the current record - * @param $form The form to clear the data for */ export async function clearSavedFormValues() { const ls = storage(); @@ -13,33 +12,33 @@ export async function clearSavedFormValues() { /** * Get the layout identifier from the body data - * @returns The layout identifier + * @returns { number } The layout identifier */ -export function layoutId() { +export function layoutId(): number { return $('body').data('layout-identifier'); } /** * Get the record identifier from the body data - * @returns The record identifier + * @returns { number } The record identifier */ -export function recordId() { +export function recordId(): number { return $('body').find('.form-edit') .data('current-id') || 0; } /** * Get the key for the table used for saving form values - * @returns The key for the table + * @returns {string} The key for the table */ -export function table_key() { +export function table_key(): string { return `linkspace-record-change-${layoutId()}-${recordId()}`; } /** * Get the storage object - this originally was used in debugging to allow for the storage object to be mocked - * @returns The storage object + * @returns { StorageProvider } The storage object */ -export function storage() { +export function storage(): StorageProvider { return new StorageProvider(table_key()); -} \ No newline at end of file +} diff --git a/src/frontend/components/button/lib/component.test.ts b/src/frontend/components/button/lib/component.test.ts index 9f1b49ba6..c2267c634 100644 --- a/src/frontend/components/button/lib/component.test.ts +++ b/src/frontend/components/button/lib/component.test.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from '@jest/globals'; import ButtonComponent from './component'; describe('Button Component', () => { diff --git a/src/frontend/components/button/lib/component.ts b/src/frontend/components/button/lib/component.ts index 420de8074..1e5ae7768 100644 --- a/src/frontend/components/button/lib/component.ts +++ b/src/frontend/components/button/lib/component.ts @@ -2,7 +2,6 @@ import {Component} from 'component'; /** * Button component - * @extends Component */ class ButtonComponent extends Component { /** @@ -19,6 +18,7 @@ class ButtonComponent extends Component { /** * Get the map of button components + * @returns {Map) => void>} The map of button components */ private get buttonsMap(): Map) => void> { if (!ButtonComponent.staticButtonsMap) ButtonComponent.initMap(); @@ -27,7 +27,7 @@ class ButtonComponent extends Component { /** * Create a button component - * @param element {HTMLElement} The button element + * @param { HTMLElement } element The button element */ constructor(element: HTMLElement) { super(element); @@ -116,7 +116,7 @@ class ButtonComponent extends Component { /** * Initialize the button - * @param element {HTMLElement} The button element + * @param {HTMLElement} element The button element */ private initButton(element: HTMLElement) { const el: JQuery = $(element); diff --git a/src/frontend/components/button/lib/create-report-button.test.ts b/src/frontend/components/button/lib/create-report-button.test.ts index dc73d8b79..db5900a6e 100644 --- a/src/frontend/components/button/lib/create-report-button.test.ts +++ b/src/frontend/components/button/lib/create-report-button.test.ts @@ -1,6 +1,6 @@ import { validateRequiredFields } from 'validation'; import CreateReportButtonComponent from './create-report-button'; -import { describe, it, expect } from '@jest/globals'; +import { describe, it, expect, jest } from '@jest/globals'; describe('create-report-button', () => { it('does not submit form if no checkboxes are checked', () => { diff --git a/src/frontend/components/button/lib/delete-button.ts b/src/frontend/components/button/lib/delete-button.ts index a0ff639eb..a947171d2 100644 --- a/src/frontend/components/button/lib/delete-button.ts +++ b/src/frontend/components/button/lib/delete-button.ts @@ -1,10 +1,8 @@ -// noinspection ExceptionCaughtLocallyJS - import { logging } from 'logging'; /** * Create delete button - * @param element {JQuery} - Element to act as a delete button + * @param {JQuery} element Element to act as a delete button */ export default function createDeleteButton(element: JQuery) { element.on('click', (ev) => { diff --git a/src/frontend/components/button/lib/remove-curval-button.ts b/src/frontend/components/button/lib/remove-curval-button.ts index 7d615bd19..3250be2bc 100644 --- a/src/frontend/components/button/lib/remove-curval-button.ts +++ b/src/frontend/components/button/lib/remove-curval-button.ts @@ -1,6 +1,6 @@ /** * Create remove curval button - * @param element {JQuery} - The element to function as a remove curval button + * @param {JQuery} element The element to function as a remove curval button */ export default function createRemoveCurvalButton(element: JQuery) { element.on('click', (ev: JQuery.ClickEvent) => { diff --git a/src/frontend/components/button/lib/remove-unload-button.ts b/src/frontend/components/button/lib/remove-unload-button.ts index ddcd9adef..ffc6d39cd 100644 --- a/src/frontend/components/button/lib/remove-unload-button.ts +++ b/src/frontend/components/button/lib/remove-unload-button.ts @@ -1,6 +1,6 @@ /** * Create a button that removes the unload event listener - * @param element {JQuery} - The button element to add the click event to + * @param {JQuery} element The button element to add the click event to */ export default function createRemoveUnloadButton(element: JQuery) { element.on('click', () => { diff --git a/src/frontend/components/button/lib/rename-button.ts b/src/frontend/components/button/lib/rename-button.ts index 023e5e611..09a205b1e 100644 --- a/src/frontend/components/button/lib/rename-button.ts +++ b/src/frontend/components/button/lib/rename-button.ts @@ -170,6 +170,11 @@ class RenameButton { this.hideRenameControls(id, button); } + /** + * Hides the rename controls + * @param {number} id The id of the field + * @param {JQuery} button The button that was clicked + */ private hideRenameControls(id: number, button: JQuery) { $(`#current-${id}`).removeClass('hidden') .attr('aria-hidden', 'false'); diff --git a/src/frontend/components/button/lib/save-view-button.ts b/src/frontend/components/button/lib/save-view-button.ts index 657522ab5..cb17960f3 100644 --- a/src/frontend/components/button/lib/save-view-button.ts +++ b/src/frontend/components/button/lib/save-view-button.ts @@ -2,7 +2,8 @@ import { validateRequiredFields } from 'validation'; import '@lol768/jquery-querybuilder-no-eval'; /** - * SaveViewButtonComponent + * Button component for saving views for an instance. + * @param {JQuery} el The jQuery element that represents the Save View button. */ export default function createSaveViewButtonComponent(el: JQuery) { const $form = el.closest('form'); @@ -34,4 +35,4 @@ export default function createSaveViewButtonComponent(el: JQuery) { .val(JSON.stringify(res, null, 2)); }); }); -} \ No newline at end of file +} diff --git a/src/frontend/components/button/lib/show-blank-button.ts b/src/frontend/components/button/lib/show-blank-button.ts index 1a5346064..e668bc64d 100644 --- a/src/frontend/components/button/lib/show-blank-button.ts +++ b/src/frontend/components/button/lib/show-blank-button.ts @@ -1,6 +1,6 @@ /** * Create a button that toggles the visibility of blank fields. - * @param element {JQuery} The element to attach the button to. + * @param {JQuery} element The element to attach the button to. */ export default function createShowBlankButton(element: JQuery) { element.on('click', (ev) => { @@ -14,4 +14,4 @@ export default function createShowBlankButton(element: JQuery) { ? 'Hide blank values' : 'Show blank values'; }); -} \ No newline at end of file +} diff --git a/src/frontend/components/button/lib/submit-draft-record-button.ts b/src/frontend/components/button/lib/submit-draft-record-button.ts index 540535a7b..0f6700592 100644 --- a/src/frontend/components/button/lib/submit-draft-record-button.ts +++ b/src/frontend/components/button/lib/submit-draft-record-button.ts @@ -2,7 +2,7 @@ import { clearSavedFormValues } from './common'; /** * Create a submit draft record button - * @param element {JQuery} The button element + * @param {JQuery} element The button element */ export default function createSubmitDraftRecordButton(element: JQuery) { element.on('click', async (ev: JQuery.ClickEvent) => { diff --git a/src/frontend/components/button/lib/submit-field-button.test.ts b/src/frontend/components/button/lib/submit-field-button.test.ts index 92799b361..ea0a4a3f3 100644 --- a/src/frontend/components/button/lib/submit-field-button.test.ts +++ b/src/frontend/components/button/lib/submit-field-button.test.ts @@ -1,3 +1,7 @@ +import { describe, it, expect, beforeEach } from '@jest/globals'; +/* eslint-disable jsdoc/require-jsdoc */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* @ts-ignore */ import { initGlobals } from 'testing/globals.definitions'; import SubmitFieldButtonComponent from './submit-field-button'; @@ -36,4 +40,4 @@ describe('Submit field button tests', () => { expect($.ajax).toHaveBeenCalled(); expect(window.alert).toHaveBeenCalled(); }); -}); \ No newline at end of file +}); diff --git a/src/frontend/components/button/lib/submit-field-button.ts b/src/frontend/components/button/lib/submit-field-button.ts index d2107c83e..905c3885f 100644 --- a/src/frontend/components/button/lib/submit-field-button.ts +++ b/src/frontend/components/button/lib/submit-field-button.ts @@ -1,8 +1,7 @@ -/* eslint-disable */ -import "jstree"; -import "datatables.net"; -import "@lol768/jquery-querybuilder-no-eval"; -import { validateQueryBuilder } from "validation"; +import 'jstree'; +import 'datatables.net'; +import '@lol768/jquery-querybuilder-no-eval'; +import { validateQueryBuilder } from 'validation'; declare global { interface Window { @@ -25,7 +24,7 @@ export default class SubmitFieldButton { /** * Create a submit field button - * @param element The submit button element + * @param {JQuery} element The submit button element */ constructor(element: JQuery) { element.on('click', (ev) => { @@ -43,7 +42,10 @@ export default class SubmitFieldButton { const $displayConditionsField = $('#displayConditions'); const $instanceIDField = $('#refers_to_instance_id'); - const $filterEl = $instanceIDField.length && $(`[data-builder-id='${$instanceIDField.val()}']`); + let $filterEl: JQuery | undefined = undefined; + if($instanceIDField.length) { + $filterEl = $(`[data-builder-id='${$instanceIDField.val()}']`); + } const $permissionTable = $('#default_field_permissions_table'); @@ -69,7 +71,7 @@ export default class SubmitFieldButton { bUpdateTree = true; } - if ($instanceIDField.length && !$instanceIDField.prop('disabled') && $filterEl.length) { + if ($instanceIDField.length && !$instanceIDField.prop('disabled') && $filterEl && $filterEl.length) { bUpdateFilter = true; } @@ -95,7 +97,7 @@ export default class SubmitFieldButton { url: this.getURL(data), data: { data: mytext, csrf_token: data.csrfToken } }).done(() => { - + alert('Tree has been updated'); }); } @@ -127,8 +129,8 @@ export default class SubmitFieldButton { /** * Get the URL for the tree API - * @param data The data for the tree - * @returns The URL for the tree API + * @param {JQuery.PlainObject} data The data for the tree + * @returns {string} The URL for the tree API */ private getURL(data: JQuery.PlainObject): string { if (window.test) return ''; diff --git a/src/frontend/components/button/lib/submit-record-button.ts b/src/frontend/components/button/lib/submit-record-button.ts index 71b0ae1d1..7cb42a193 100644 --- a/src/frontend/components/button/lib/submit-record-button.ts +++ b/src/frontend/components/button/lib/submit-record-button.ts @@ -10,7 +10,7 @@ export default class SubmitRecordButton { /** * Create a button to submit records - * @param el {JQuery} Element to create as a button + * @param {JQuery} el Element to create as a button */ constructor(private el: JQuery) { this.el.on('click', async (ev: JQuery.ClickEvent) => { diff --git a/src/frontend/components/button/lib/toggle-all-fields-button.ts b/src/frontend/components/button/lib/toggle-all-fields-button.ts index 5caae265d..547e620e5 100644 --- a/src/frontend/components/button/lib/toggle-all-fields-button.ts +++ b/src/frontend/components/button/lib/toggle-all-fields-button.ts @@ -1,6 +1,6 @@ /** * Toggles (switches) all fields from the source toggle table to the destination toggle table - * @param element {JQuery} - The button element + * @param {JQuery} element The button element */ export default function createToggleAllFieldsButton(element: JQuery) { element.on('click', (ev) => { diff --git a/src/frontend/components/calculator/lib/component.js b/src/frontend/components/calculator/lib/component.js index 068b80ce4..7dd9ef526 100644 --- a/src/frontend/components/calculator/lib/component.js +++ b/src/frontend/components/calculator/lib/component.js @@ -1,6 +1,14 @@ import { Component } from 'component'; +/** + * CalculatorComponent class to handle calculator functionality in the UI. + * It initializes a calculator dropdown for input fields, allowing users to perform basic arithmetic operations. + */ class CalculatorComponent extends Component { + /** + * Creates an instance of CalculatorComponent. + * @param {HTMLElement} element The element to be initialized as a calculator component. + */ constructor(element) { super(element); this.el = $(this.element); @@ -8,6 +16,11 @@ class CalculatorComponent extends Component { this.initCalculator(); } + /** + * Initializes the calculator functionality by creating a dropdown + * with buttons for arithmetic operations and an input field for numbers. + * @todo This method should be refactored to improve readability and maintainability. + */ initCalculator() { const selector = this.el.find('input:not([type="checkbox"])'); const $nodes = this.el.find('label:not(.checkbox-label)'); diff --git a/src/frontend/components/card/lib/component.js b/src/frontend/components/card/lib/component.js index 398a5c3f1..1bab1d970 100644 --- a/src/frontend/components/card/lib/component.js +++ b/src/frontend/components/card/lib/component.js @@ -1,7 +1,15 @@ -import { Component } from 'component'; import 'bootstrap'; +import { Component } from 'component'; +/** + * Creates an expandable card component. + */ class ExpandableCardComponent extends Component { + + /** + * Creates an instance of ExpandableCardComponent. + * @param {HTMLElement} element The HTML element to attach the component to. + */ constructor(element) { super(element); this.$el = $(this.element); @@ -14,6 +22,9 @@ class ExpandableCardComponent extends Component { } } + /** + * Initializes the expandable card functionality. + */ initExpandableCard() { const $collapsibleElm = this.$el.find('.collapse'); const $btnEdit = this.$el.find('.btn-js-edit'); @@ -65,6 +76,9 @@ class ExpandableCardComponent extends Component { }); } + /** + * Initializes the topic card functionality. + */ initTopicCard() { // Now that fields are shown/hidden on page load, for each topic check // whether it has zero displayed fields, in which case hide the whole @@ -85,6 +99,10 @@ class ExpandableCardComponent extends Component { } } + /** + * Checks if the edit class can be removed from the content block. + * @returns {boolean} True if the edit class can be removed, false otherwise. + */ canRemoveEditClass() { return !this.$contentBlock.find('.card--edit').length; } @@ -98,10 +116,10 @@ class ExpandableCardComponent extends Component { return message; }; - /* - In order to ensure headers on the view filter tables are the correct width, we need to remove any styling that has been added to the header elements. - And for some reason, using JQuery and DataTables, the styling is not reset as we expect it to be. - */ + /** + * In order to ensure headers on the view filter tables are the correct width, we need to remove any styling that has been added to the header elements. + * And for some reason, using JQuery and DataTables, the styling is not reset as we expect it to be. + */ clearupStyling() { const tables = $('.table-toggle'); tables.removeAttr('style'); diff --git a/src/frontend/components/collapsible/lib/component.js b/src/frontend/components/collapsible/lib/component.js index f228b8749..3a323a203 100644 --- a/src/frontend/components/collapsible/lib/component.js +++ b/src/frontend/components/collapsible/lib/component.js @@ -1,6 +1,13 @@ import { Component } from 'component'; +/** + * + */ class CollapsibleComponent extends Component { + /** + * Creates an instance of CollapsibleComponent. + * @param {HTMLElement} element The element to be initialized as a collapsible component. + */ constructor(element) { super(element); this.el = $(this.element); @@ -11,6 +18,10 @@ class CollapsibleComponent extends Component { this.initCollapsible(this.button); } + /** + * Initializes the collapsible component. + * @param {HTMLElement} button The button element that will toggle the collapsible content. + */ initCollapsible(button) { if (!button) { return; @@ -20,6 +31,9 @@ class CollapsibleComponent extends Component { button.click(() => { this.handleClick(); }); } + /** + * Handles the click event on the collapsible button. + */ handleClick() { this.titleExpanded.toggleClass('hidden'); this.titleCollapsed.toggleClass('hidden'); diff --git a/src/frontend/components/collapsible/lib/component.test.ts b/src/frontend/components/collapsible/lib/component.test.ts index 34f3d67e3..b2ac1af55 100644 --- a/src/frontend/components/collapsible/lib/component.test.ts +++ b/src/frontend/components/collapsible/lib/component.test.ts @@ -19,7 +19,7 @@ describe('Collapsible', () => {
Please make a secure note of this content now, as it will not be displayed again.
- `; + `; }); afterEach(() => { @@ -53,4 +53,4 @@ describe('Collapsible', () => { expect(titleCollapsed.classList.contains('hidden')).toBe(false); expect(titleExpanded.classList.contains('hidden')).toBe(true); }); -}); \ 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 7c4ace897..77d0cf46d 100644 --- a/src/frontend/components/dashboard/dashboard-graph/lib/component.js +++ b/src/frontend/components/dashboard/dashboard-graph/lib/component.js @@ -1,12 +1,22 @@ 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. + */ class DashboardGraphComponent extends GraphComponent { + /** + * Create a DashboardGraphComponent instance. + * @param {HTMLElement} element The HTML element that this component will be attached to. + */ constructor(element) { super(element); this.initDashboardGraph(); } + /** + * Initialize the dashboard graph by rendering the graph using do_plot_json. + */ 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.js index 3ed79d0cd..68ec6f9b4 100644 --- a/src/frontend/components/dashboard/lib/component.js +++ b/src/frontend/components/dashboard/lib/component.js @@ -15,7 +15,14 @@ import ReactDOM from 'react-dom'; import App from './react/app'; import ApiClient from './react/api'; +/** + * DashboardComponent class that initializes the dashboard and renders the App component. + */ class DashboardComponent extends Component { + /** + * Create a DashboardComponent instance. + * @param {HTMLElement} element The HTML element that this component will be attached to. + */ constructor(element) { super(element); this.el = $(this.element); @@ -30,6 +37,9 @@ class DashboardComponent extends Component { this.initDashboard(); } + /** + * Initialize the dashboard by rendering the App component with widgets and configurations. + */ initDashboard() { this.element.className = ''; const widgetsEls = Array.prototype.slice.call(document.querySelectorAll('#ld-app > div')); diff --git a/src/frontend/components/dashboard/lib/react/Footer.tsx b/src/frontend/components/dashboard/lib/react/Footer.tsx index 1ab39db38..e62ab4afe 100644 --- a/src/frontend/components/dashboard/lib/react/Footer.tsx +++ b/src/frontend/components/dashboard/lib/react/Footer.tsx @@ -1,6 +1,16 @@ import React from 'react'; -const Footer = ({ addWidget, widgetTypes, currentDashboard, readOnly, noDownload }) => { +/** + * Create a Footer component that displays options for downloading the dashboard and adding widgets. + * @param param0 The properties for the Footer component, including a function to add a widget, a list of widget types, the current dashboard, a read-only flag, and a no-download flag. + * @param {function} param0.addWidget Function to call when adding a widget. + * @param {Array} param0.widgetTypes The list of widget types available for addition. + * @param {object} param0.currentDashboard The currently active dashboard. + * @param {boolean} param0.readOnly A flag indicating whether the dashboard is in read-only mode. + * @param {boolean} param0.noDownload A flag indicating whether the download option + * @returns {JSX.Element} The rendered Footer component. + */ +const Footer = ({ addWidget, widgetTypes, currentDashboard, readOnly, noDownload }): JSX.Element => { return (
{noDownload ? null :
diff --git a/src/frontend/components/dashboard/lib/react/Header.tsx b/src/frontend/components/dashboard/lib/react/Header.tsx index 3c886d092..05a41b305 100644 --- a/src/frontend/components/dashboard/lib/react/Header.tsx +++ b/src/frontend/components/dashboard/lib/react/Header.tsx @@ -1,6 +1,16 @@ import React from 'react'; -const Header = ({ hMargin, dashboards, currentDashboard, loading, includeH1 }) => { +/** + * Create a Header component that displays a navigation header for dashboards. + * @param {object} param0 The properties for the Header component, including horizontal margin, list of dashboards, current dashboard, loading state, and a flag to include an H1 element. + * @param {number} param0.hMargin The horizontal margin to apply to the header. + * @param {Array} param0.dashboards The list of dashboards to display in the header. + * @param {{name: string}} param0.currentDashboard The currently active dashboard. + * @param {boolean} param0.loading A flag indicating whether the header is in a loading state. + * @param {boolean} param0.includeH1 A flag indicating whether to include an + * @returns {JSX.Element} The rendered Header component. + */ +const Header = ({ hMargin, dashboards, currentDashboard, loading, includeH1 }: { hMargin: number; dashboards: Array; currentDashboard: {name: string}; loading: boolean; includeH1: boolean; }): JSX.Element => { const renderMenuItem = (dashboard) => { if (dashboard.name === currentDashboard.name) { if (includeH1) { diff --git a/src/frontend/components/dashboard/lib/react/Widget.tsx b/src/frontend/components/dashboard/lib/react/Widget.tsx index 6cc4aace3..c049f54cf 100644 --- a/src/frontend/components/dashboard/lib/react/Widget.tsx +++ b/src/frontend/components/dashboard/lib/react/Widget.tsx @@ -1,10 +1,17 @@ import React from 'react'; import { initializeRegisteredComponents } from 'component'; +/** + * Widget component that renders a widget with HTML content. + */ export default class Widget extends React.Component { private ref; - constructor(props) { + /** + * Create a Widget component. + * @param {*} props The properties passed to the component, including the widget HTML and a flag for read-only mode. + */ + constructor(props: any) { super(props); this.ref = React.createRef(); @@ -25,6 +32,10 @@ export default class Widget extends React.Component { initializeRegisteredComponents(this.ref.current); }; + /** + * Render the Widget component. + * @returns {JSX.Element} The rendered widget component. + */ render() { return ( diff --git a/src/frontend/components/dashboard/lib/react/api.tsx b/src/frontend/components/dashboard/lib/react/api.tsx index 78eddc697..f199cddce 100644 --- a/src/frontend/components/dashboard/lib/react/api.tsx +++ b/src/frontend/components/dashboard/lib/react/api.tsx @@ -1,16 +1,32 @@ +/** + * API Client for interacting with the backend services. + * @todo Cleanup + */ export default class ApiClient { private baseUrl; private headers; private isDev; - constructor(baseUrl = '') { + /** + * Creates a new instance of ApiClient. + * @param {string} baseUrl Base URL for the API endpoints. + */ + constructor(baseUrl: string = '') { this.baseUrl = baseUrl; this.headers = {}; // @ts-expect-error "isDev is not valid" this.isDev = window.siteConfig && window.siteConfig.isDev; } - async _fetch(route, method, body) { + /** + * Execute a fetch request to the API. + * @description This is a basic wrapper around the fetch API. + * @param {string} route The API route to fetch. + * @param { 'GET'|'POST'|'PUT'|'PATCH'|'DELETE' } method The API method (GET, POST, PUT, PATCH, DELETE). + * @param {*} body The body of the request, if applicable. + * @returns {Promise} The response from the fetch call. + */ + async _fetch(route: string, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', body: any): Promise { if (!route) throw new Error('Route is undefined'); let csrfParam = ''; @@ -35,40 +51,100 @@ export default class ApiClient { return fetch(fullRoute, opts); } - GET(route) { return this._fetch(route, 'GET', null); } - - POST(route, body) { return this._fetch(route, 'POST', body); } - - PUT(route, body) { return this._fetch(route, 'PUT', body); } - - PATCH(route, body) { return this._fetch(route, 'PATCH', body); } - - DELETE(route) { return this._fetch(route, 'DELETE', null); } - - saveLayout = (id, layout) => { + /** + * Performs a GET request to the specified route. + * @param {string} route The API route to fetch. + * @returns {Promise} The response from the fetch call. + */ + GET(route: string): Promise { return this._fetch(route, 'GET', null); } + + /** + * Performs a POST request to the specified route. + * @param {string} route The API route to fetch. + * @param {*} body The body of the request, if applicable. + * @returns {Promise} The response from the fetch call. + */ + POST(route: string, body: any): Promise { return this._fetch(route, 'POST', body); } + + /** + * Performs a PUT request to the specified route. + * @param {string} route The API route to fetch. + * @param {*} body The body of the request, if applicable. + * @returns {Promise} The response from the fetch call. + */ + PUT(route: string, body: any): Promise { return this._fetch(route, 'PUT', body); } + + /** + * Performs a PATCH request to the specified route. + * @param {string} route The API route to fetch. + * @param {*} body The body of the request, if applicable. + * @returns {Promise} The response from the fetch call. + */ + PATCH(route: string, body: any): Promise { return this._fetch(route, 'PATCH', body); } + + /** + * Performs a DELETE request to the specified route. + * @param {string} route The API route to fetch. + * @returns {Promise} The response from the fetch call. + */ + DELETE(route: string): Promise { return this._fetch(route, 'DELETE', null); } + + /** + * Save the layout of a dashboard. + * @param {string} id The ID of the dashboard to save the layout for. + * @param {object} layout The layout to save, typically an array of widget configurations. + * @returns {Promise} The response from the save operation. + */ + saveLayout = (id: string, layout: Array): Promise => { if (!this.isDev) { const strippedLayout = layout.map(widget => ({ ...widget, moved: undefined })); return this.PUT(`/dashboard/${id}`, strippedLayout); } }; - createWidget = async type => { + /** + * Create a new widget. + * @param {string} type The type of widget to create. + * @returns {Promise} The response from the widget creation request. + */ + createWidget = async (type: string): Promise => { const response = this.isDev ? await this.GET(`/widget/create.json?type=${type}`) : await this.POST(`/widget?type=${type}`, null); return await response.json(); }; - getWidgetHtml = async id => { + /** + * Get the HTML content of a widget. + * @param {string} id The ID of the widget to retrieve HTML for. + * @returns {Promise} The HTML content of the widget. + */ + getWidgetHtml = async (id: string): Promise => { const html = this.isDev ? await this.GET(`/widget/${id}/create`) : await this.GET(`/widget/${id}`); return html.text(); }; - deleteWidget = id => !this.isDev && this.DELETE(`/widget/${id}`); - - getEditForm = async id => { + /** + * Delete a widget by its ID. + * @param {string} id The ID of the widget to delete. + * @returns {promise} A promise that resolves when the widget is deleted. + */ + deleteWidget = (id: string): Promise => !this.isDev && this.DELETE(`/widget/${id}`); + + /** + * Get the edit form for a widget. + * @param {string} id The ID of the widget to retrieve the edit form for. + * @returns {Promise} The JSON response containing the edit form data for the widget. + */ + getEditForm = async (id: string): Promise => { const response = await this.GET(`/widget/${id}/edit`); return response.json(); }; + /** + * Save a widget. + * @param {string} url The URL to save the widget. + * @param {object} params The parameters to save the widget. + * @returns {Promise} The JSON response containing the saved widget data. + */ saveWidget = async (url, params) => { const result = this.isDev ? await this.GET('/widget/update.json') : await this.PUT(`${url}`, params); return await result.json(); diff --git a/src/frontend/components/dashboard/lib/react/app.tsx b/src/frontend/components/dashboard/lib/react/app.tsx index 0537304db..d6fba359a 100644 --- a/src/frontend/components/dashboard/lib/react/app.tsx +++ b/src/frontend/components/dashboard/lib/react/app.tsx @@ -39,10 +39,18 @@ const modalStyle = { } }; +/** + * App component for the dashboard, managing widgets and layout. + */ class App extends React.Component { private formRef; - constructor(props) { + /** + * Create an instance of the App component. + * @param {any} props The properties passed to the component, including widgets, api, dashboardId, etc. + * @todo Use concrete types instead of `any` for better type safety. + */ + constructor(props: any) { super(props); Modal.setAppElement('#ld-app'); @@ -66,7 +74,7 @@ class App extends React.Component { this.initializeGlobeComponents(); }; - componentDidUpdate = (prevProps, prevState) => { + componentDidUpdate = (prevProps: any, prevState: any) => { window.requestAnimationFrame(this.overWriteSubmitEventListener); if (this.state.editModalOpen && prevState.loadingEditHtml && !this.state.loadingEditHtml && this.formRef) { @@ -92,7 +100,7 @@ class App extends React.Component { const arrGlobe = document.querySelectorAll('.globe'); import('../../../globe/lib/component').then(({ default: GlobeComponent }) => { arrGlobe.forEach((globe) => { - new GlobeComponent(globe); + new GlobeComponent(globe as HTMLElement); }); }); }; @@ -135,7 +143,6 @@ class App extends React.Component { }; deleteActiveWidget = () => { - if (!window.confirm('Deleting a widget is permanent! Are you sure?')) return; @@ -150,7 +157,6 @@ class App extends React.Component { event.preventDefault(); const formEl = this.formRef.current.querySelector('form'); if (!formEl) { - console.error('No form element was found!'); return; } @@ -194,7 +200,6 @@ class App extends React.Component { return { x, y }; }; - addWidget = async (type) => { this.setState({ loading: true }); const result = await this.props.api.createWidget(type); @@ -308,7 +313,11 @@ class App extends React.Component { window.dispatchEvent(new Event('resize')); }; - render() { + /** + * Renders the App component. + * @returns {React.JSX.Element} The rendered component, including the header, footer, and grid layout with widgets. + */ + render(): React.JSX.Element { return (
{this.props.hideMenu ? null :
{ + this.el.on('draw.dt', () => { this.initClickableTable(); }); } @@ -76,9 +86,12 @@ class DataTableComponent extends Component { }); } + /** + * Clears the table state for the current page + */ clearTableStateForPage() { for (let i = 0; i < localStorage.length; i++) { - const storageKey = localStorage.key( i ); + const storageKey = localStorage.key(i); if (!storageKey.startsWith('DataTables')) { continue; @@ -90,12 +103,15 @@ class DataTableComponent extends Component { continue; } - if(window.location.href.indexOf('/' + keySegments.slice(1).join('/')) !== -1) { + if (window.location.href.indexOf('/' + keySegments.slice(1).join('/')) !== -1) { localStorage.removeItem(storageKey); } } } + /** + * Initializes the clickable table functionality + */ initClickableTable() { const links = this.el.find('tbody td .link'); // Remove all existing click events to prevent multiple bindings @@ -107,6 +123,11 @@ class DataTableComponent extends Component { links.on('blur', (ev) => { this.toggleFocus(ev, false); }); } + /** + * Toggles focus on a row + * @param {JQuery.FocusEvent} ev The event that triggered the focus change + * @param {boolean} hasFocus Whether the row has focus or not + */ toggleFocus(ev, hasFocus) { const row = $(ev.target).closest('tr'); if (hasFocus) { @@ -116,6 +137,10 @@ class DataTableComponent extends Component { } } + /** + * Handle click event on a row + * @param {JQuery.ClickEvent} ev The click event + */ handleClick(ev) { const rowClicked = $(ev.target).closest('tr'); ev.preventDefault(); @@ -123,6 +148,10 @@ class DataTableComponent extends Component { $(this.modal).modal('show'); } + /** + * Fill the modal data from the clicked row + * @param {HTMLTableRowElement} row The row to fill the modal data from + */ fillModalData(row) { const fields = $(this.modal).find('input, textarea'); const btnReject = $(this.modal).find('.btn-js-reject-request-send'); @@ -156,15 +185,24 @@ class DataTableComponent extends Component { }); } + /** + * Get a checkbox element as an HTML string + * @param {number } id The ID for the checkbox element + * @param {string} label The label for the checkbox element + * @returns {string} The HTML string for the checkbox element + */ getCheckboxElement(id, label) { return ( '
' + - `` + - `` + - '
' + `` + + `` + + '
' ); } + /** + * Add a select all checkbox to the table header + */ addSelectAllCheckbox() { const $selectAllElm = this.el.find('thead th.check'); const $checkBoxes = this.el.find('tbody .check .checkbox input'); @@ -181,10 +219,10 @@ class DataTableComponent extends Component { }); // Check if the 'select all' checkbox is checked and all checkboxes need to be checked - $selectAllElm.find('input').on( 'click', (ev) => { + $selectAllElm.find('input').on('click', (ev) => { const checkbox = $(ev.target); - if ($(checkbox).is( ':checked' )) { + if ($(checkbox).is(':checked')) { this.checkAllCheckboxes($checkBoxes, true); } else { this.checkAllCheckboxes($checkBoxes, false); @@ -192,14 +230,24 @@ class DataTableComponent extends Component { }); } + /** + * Check or uncheck all checkboxes in the table + * @param {JQuery} $checkBoxes The checkboxes to check or uncheck + * @param {boolean} bCheckAll True to check all checkboxes, false to uncheck all + */ checkAllCheckboxes($checkBoxes, bCheckAll) { if (bCheckAll) { - $checkBoxes.prop( 'checked', true ); + $checkBoxes.prop('checked', true); } else { $checkBoxes.prop('checked', false); } } + /** + * Check or uncheck the 'select all' checkbox based on the state of the individual checkboxes + * @param {JQuery} $checkBoxes The checkboxes to check or uncheck + * @param {JQuery} $selectAllCheckBox The select all checkbox to update + */ checkSelectAll($checkBoxes, $selectAllCheckBox) { let bSelectAll = true; @@ -215,6 +263,12 @@ class DataTableComponent extends Component { } } + /** + * Add a sort button to the column header + * @param {DataTable} dataTable The DataTable instance + * @param {any} column The column to add the sort button to + * @param {any} headerContent The content of the column header + */ addSortButton(dataTable, column, headerContent) { const $header = $(column.header()); const $button = $(` @@ -231,9 +285,13 @@ class DataTableComponent extends Component { .find('.data-table__header-wrapper') .html($button); - dataTable.order.listener($button, column.index() ); + dataTable.order.listener($button, column.index()); } + /** + * Toggle the filter for a column + * @param {any} column The column to toggle the filter for + */ toggleFilter(column) { const $header = $(column.header()); @@ -246,15 +304,21 @@ class DataTableComponent extends Component { } } - // Self reference included due to scoping + /** + * Add a search dropdown to the column header + * @param {any} column The column to add the search dropdown to + * @param {string} id The ID of the column + * @param {number} index The index of the column + */ async addSearchDropdown(column, id, index) { + // Self reference included due to scoping const $header = $(column.header()); const title = $header.text().trim(); const searchValue = column.search(); const self = this; - const {context} = column; - const {oAjaxData} = context[0]; - const {columns} = oAjaxData; + const { context } = column; + const { oAjaxData } = context[0]; + const { columns } = oAjaxData; const columnId = columns[column.index()].name; const col = this.columns[column.index()]; @@ -352,7 +416,7 @@ class DataTableComponent extends Component { self.toggleFilter(column); // Update or add the filter to the searchParams - if(self.searchParams.has(id)) { + if (self.searchParams.has(id)) { self.searchParams.set(id, this.value); } else { self.searchParams.append(id, this.value); @@ -390,21 +454,42 @@ class DataTableComponent extends Component { }); } + /** + * Get the API endpoint for the column typeahead + * @param {number} columnId The ID of the column to get the API endpoint for + * @returns {string} The API endpoint for the column typeahead + */ getApiEndpoint(columnId) { const table = $('body').data('layout-identifier'); return `/${table}/match/layout/${columnId}?q=`; } + /** + * Encode HTML entities in a string + * @param {string} text The text to encode + * @returns {string} The encoded text + */ encodeHTMLEntities(text) { return $('