diff --git a/package.json b/package.json index 9892611b0..63da4b860 100644 --- a/package.json +++ b/package.json @@ -13,80 +13,82 @@ "dependencies": { "@egjs/hammerjs": "^2.0.0", "@lol768/jquery-querybuilder-no-eval": "^2.6.0", - "bootstrap": "4.6", + "bootstrap": "^4.6.0", "bootstrap-datepicker": "^1.9.0", "bootstrap-select": "^1.13.18", "component-emitter": "^1.3.0", - "datatables.net-bs4": "^2.0.8", - "datatables.net-buttons-bs4": "^3.0.2", - "datatables.net-responsive-bs4": "^3.0.2", + "datatables.net-bs4": "^2.3.2", + "datatables.net-buttons-bs4": "^3.2.4", + "datatables.net-responsive-bs4": "^3.0.5", "datatables.net-rowreorder-bs4": "^1.5.0", "form-serialize": "^0.7.2", "handlebars": "^4.7.7", "jquery": "^3.6.0", "jquery-ui-sortable-npm": "^1.0.0", - "jstree": "^3.3.12", - "keycharm": "^0.3.0", - "marked": "^9.1.1", + "jstree": "^3.3.17", + "keycharm": "^0.4.0", + "marked": "^15.0.12", "moment": "^2.24.0", "popper.js": "^1.16.1", - "propagating-hammerjs": "^1.4.0", + "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", - "regenerator-runtime": "^0.13.11", - "summernote": "^0.8.20", + "regenerator-runtime": "^0.14.1", + "summernote": "^0.9.1", "tippy.js": "^6.3.7", "typeahead.js": "^0.11.1", - "uuid": "^7.0.0", - "vis-data": "^6.3.0", - "vis-timeline": "7.4.3", - "vis-util": "^4.0.0" + "uuid": "^11.1.0", + "vis-data": "^8.0.1", + "vis-timeline": "^8.2.1", + "vis-util": "^6.0.0", + "xss": "^1.0.0" }, "devDependencies": { - "@babel/core": "^7.14.6", - "@babel/preset-env": "^7.14.7", - "@babel/preset-react": "^7.16.7", - "@babel/preset-typescript": "^7.16.7", + "@babel/core": "^7.28.0", + "@babel/preset-env": "^7.28.0", + "@babel/preset-react": "^7.27.1", + "@babel/preset-typescript": "^7.27.1", "@eslint/css": "^0.10.0", "@eslint/js": "^9.31.0", - "@jest/globals": "^29.7.0", + "@jest/globals": "^30.0.5", "@stylistic/eslint-plugin": "^5.2.0", "@testing-library/dom": "^10.4.1", "@testing-library/react": "12", - "@types/jest": "^29.5.6", - "@types/jquery": "^3.5.24", + "@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-grid-layout": "^1.3.2", "@types/typeahead.js": "^0.11.6", - "autoprefixer": "^9.8.8", - "babel-loader": "^8.2.2", + "autoprefixer": "^10.4.21", + "babel-loader": "^10.0.0", "clean-webpack-plugin": "^4.0.0", - "copy-webpack-plugin": "6", - "core-js": "^3.15.2", - "css-loader": "^3.2.0", - "cypress": "^13.7.2", + "copy-webpack-plugin": "13.0.0", + "core-js": "^3.44.0", + "css-loader": "^7.1.2", + "cypress": "^14.5.3", "eslint": "^9.31.0", "eslint-plugin-jsdoc": "^52.0.0", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "mini-css-extract-plugin": "^2.7.2", - "postcss-loader": "^3.0.0", - "sass": "^1.23.7", - "sass-loader": "^8.0.0", - "terser-webpack-plugin": "^5.3.6", - "ts-loader": "~8.2.0", - "typescript": "^5.9.2", + "jest": "^30.0.5", + "jest-environment-jsdom": "^30.0.5", + "mini-css-extract-plugin": "^2.9.2", + "postcss-loader": "^8.1.1", + "sass": "^1.89.2", + "sass-loader": "^16.0.5", + "terser-webpack-plugin": "^5.3.14", + "ts-loader": "~9.5.2", + "typescript": "~5.8.0", "typescript-eslint": "^8.37.0", - "webpack": "^5.75.0", - "webpack-cli": "^5.0.1" + "webpack": "^5.101.0", + "webpack-cli": "^6.0.1" }, "browserslist": [ "last 2 versions", diff --git a/src/frontend/components/button/lib/remove-curval-button.test.js b/src/frontend/components/button/lib/remove-curval-button.test.js index 5d854852c..a22dd35f8 100644 --- a/src/frontend/components/button/lib/remove-curval-button.test.js +++ b/src/frontend/components/button/lib/remove-curval-button.test.js @@ -61,4 +61,4 @@ describe('RemoveCurvalButton', () => { button.click(); expect(current.children.length).toBe(0); }); -}); +}); \ No newline at end of file diff --git a/src/frontend/components/button/lib/show-blank-button.test.ts b/src/frontend/components/button/lib/show-blank-button.test.ts index ee0237e2a..3d13895ca 100644 --- a/src/frontend/components/button/lib/show-blank-button.test.ts +++ b/src/frontend/components/button/lib/show-blank-button.test.ts @@ -29,4 +29,4 @@ describe('ShowBlankButton', () => { button.trigger('click'); expect(item.css('display')).toBe('none'); }); -}); +}); \ No newline at end of file diff --git a/src/frontend/components/data-table/_data-table.scss b/src/frontend/components/data-table/_data-table.scss index dab6a9882..a27097ffe 100644 --- a/src/frontend/components/data-table/_data-table.scss +++ b/src/frontend/components/data-table/_data-table.scss @@ -1,383 +1,440 @@ /* stylelint-disable selector-no-qualifying-type */ .data-table { - border-spacing: 0; - font-size: $font-size-sm; + border-spacing: 0; + font-size: $font-size-sm; - &.table-thead-hidden thead { - @include visually-hidden; - } + &.table-thead-hidden thead { + @include visually-hidden; + } - thead { - background-color: $white; - z-index: 1; - } + thead { + background-color: $white; + z-index: 1; + } - thead th { - border-bottom: 1px solid $gray; - text-transform: uppercase; - vertical-align: top; + thead th { + border-bottom: 1px solid $gray; + text-transform: uppercase; + vertical-align: top; - &[class*="sorting_asc"], - &[class*="sorting_desc"] { - color: $brand-secundary; - } + &[class*="sorting_asc"], + &[class*="sorting_desc"] { + color: $brand-secundary; + } - &.data-table__header--invisible span, - &.dt__header--inivisible span { - @include visually-hidden; + &.data-table__header--invisible span, + &.dt__header--inivisible span { + @include visually-hidden; + } } - } - tfoot { - background-color: $table-hover-bg; - font-weight: bold; - } + tfoot { + background-color: $table-hover-bg; + font-weight: bold; + } - &.table-lines th, - &.table-lines td { - border-top: 0; - border-bottom: 1px solid $gray; - } + &.table-lines th, + &.table-lines td { + border-top: 0; + border-bottom: 1px solid $gray; + } - .autosize { - max-height: 30px; - } + .autosize { + max-height: 30px; + } } /* Necessary to overrule default styling */ table.dataTable thead .sorting, table.dataTable thead .sorting_disabled { - /* stylelint-disable declaration-no-important */ - // Remove the sorting arrows from the sorting element, important needed to overrule default styling - &::before, - &::after { - content: normal !important; - } + /* stylelint-disable declaration-no-important */ + // Remove the sorting arrows from the sorting element, important needed to overrule default styling + &::before, + &::after { + content: normal !important; + } } // Styling of the generated dataTable .dataTables_wrapper { - margin-bottom: $padding-small-vertical; - font-size: $font-size-sm; + margin-bottom: $padding-small-vertical; + font-size: $font-size-sm; - &:last-child { - margin-bottom: 0; - } + &:last-child { + margin-bottom: 0; + } - .row { - width: 100%; - } + .row { + width: 100%; + } - .row--header, - .row--main { - margin-bottom: $padding-base-vertical; - } + .row--header, + .row--main { + margin-bottom: $padding-base-vertical; + } } .row--fiv-header { - margin-top: 1.9rem; + margin-top: 1.9rem; } .dataTables_toggle_full_width { - .btn-toggle, - .btn-toggle-off { - padding-top: 7px; - } + .btn-toggle, + .btn-toggle-off { + padding-top: 7px; + } } .data-table__container--scrollable { - overflow: auto; + overflow: auto; - thead { - position: sticky; - top: 0; - } + thead { + position: sticky; + top: 0; + } } .dataTables_info_wrapper { - display: none; + display: none; } .dataTables_length_wrapper { - margin-top: $padding-large-vertical; + margin-top: $padding-large-vertical; } .dataTables_length { - .form-control { - @include form-control; - } + .form-control { + @include form-control; + } } .dataTables_filter { - label { - @include input-search; + label { + @include input-search; - display: flex; - justify-content: flex-start; - } + display: flex; + justify-content: flex-start; + } - .form-control { - @include form-control; - } + .form-control { + @include form-control; + } } .data-table__sort { - display: flex; - align-items: flex-start; - order: 2; - padding: 0; - transition: 0.2s all ease-in; - border: 0; - border-bottom: 1px solid $transparent; - background-color: $transparent; - color: $gray-extra-dark; - font-weight: bold; - text-align: left; - text-transform: uppercase; - - .btn-sort { - margin-top: 0.1rem; - margin-left: 0.25rem; - opacity: 0; - - &:hover { - border-bottom: none; - } - } - - &:hover, - &:active, - &:focus, - .dt-ordering-asc &, - .dt-ordering-desc & { - color: $brand-secundary; + display: flex; + align-items: flex-start; + order: 2; + padding: 0; + transition: 0.2s all ease-in; + border: 0; + border-bottom: 1px solid $transparent; + background-color: $transparent; + color: $gray-extra-dark; + font-weight: bold; + text-align: left; + text-transform: uppercase; .btn-sort { - opacity: 1; - } - } - - .data-table__header--invisible & { - display: none; - } -} - -.data-table__search { - margin: 0 0.1rem 0 -1rem; + margin-top: 0.1rem; + margin-left: 0.25rem; + opacity: 0; - .dropdown-toggle { - margin-top: 0.1rem; - transition: 0.2s opacity ease-in; - opacity: 0; + &:hover { + border-bottom: none; + } + } &:hover, &:active, - &:focus { - opacity: 1; + &:focus, + .dt-ordering-asc &, + .dt-ordering-desc & { + color: $brand-secundary; + + .btn-sort { + opacity: 1; + } } - &::after { - content: normal; + .data-table__header--invisible & { + display: none; + } +} + +.data-table__search { + margin: 0 0.1rem 0 -1rem; + + .dropdown-toggle { + margin-top: 0.1rem; + transition: 0.2s opacity ease-in; + opacity: 0; + + &:hover, + &:active, + &:focus { + opacity: 1; + } + + &::after { + content: normal; + } } - } - &.show .dropdown-toggle { - opacity: 1; - } + &.show .dropdown-toggle { + opacity: 1; + } - label { - @include input-search; - } + label { + @include input-search; + } - .input .form-control { - width: auto; - } + .input .form-control { + width: auto; + } - .data-table__header--invisible & { - display: none; - } + .data-table__header--invisible & { + display: none; + } } .col { - .dt-search { - input[type="search"] { // The styling has to be _very_ specific, if I leave out the `.col` field, it won't work. - width: 98%; // I'm not sure where the bug this solves came from - I think it may be because of the DT upgrade (and just wasn't spotted) - if I use 100% it causes the other elements to be pushed down (I'm not keen on using percentiles, either). + .dt-search { + input[type="search"] { + // The styling has to be _very_ specific, if I leave out the `.col` field, it won't work. + width: 98%; // I'm not sure where the bug this solves came from - I think it may be because of the DT upgrade (and just wasn't spotted) - if I use 100% it causes the other elements to be pushed down (I'm not keen on using percentiles, either). + } } - } } .dataTables_scrollHead .table--bordered, .dt-scroll-head .table--bordered { - box-sizing: border-box; - border: 1px solid $gray; - border-bottom: 0; - border-top-left-radius: $table-border-radius; - border-top-right-radius: $table-border-radius; + box-sizing: border-box; + border: 1px solid $gray; + border-bottom: 0; + border-top-left-radius: $table-border-radius; + border-top-right-radius: $table-border-radius; - thead { - // Why is this not in the variables file?? Or even more important, why is this a variable? - $table-bordered-head-color: #585858; + thead { + // Why is this not in the variables file?? Or even more important, why is this a variable? + $table-bordered-head-color: #585858; - color: $table-bordered-head-color; - } + color: $table-bordered-head-color; + } } .dataTables_scrollBody:has(.table--bordered), .dt-scroll-body:has(.table--bordered) { - border: 1px solid $gray; - border-top: 0; - border-bottom-left-radius: $table-border-radius; - border-bottom-right-radius: $table-border-radius; + border: 1px solid $gray; + border-top: 0; + border-bottom-left-radius: $table-border-radius; + border-bottom-right-radius: $table-border-radius; - .table-striped { - border-bottom: 0; - } + .table-striped { + border-bottom: 0; + } } .dataTables_scrollFoot .table-striped.table--bordered, .dt-scroll-foot .table-striped.table--bordered { - border: 0; + border: 0; } .data-table__header-wrapper { - display: flex; - position: relative; - align-items: flex-start; + display: flex; + position: relative; + align-items: flex-start; - &.filter .data-table__search .dropdown-toggle.btn-search { - opacity: 1; - } + &.filter .data-table__search .dropdown-toggle.btn-search { + opacity: 1; + } - &:hover, - &:active, - &:focus { - .data-table__search .dropdown-toggle { - opacity: 1; + &:hover, + &:active, + &:focus { + .data-table__search .dropdown-toggle { + opacity: 1; + } } - } } // Pagination .dataTables_paginate .pagination { - justify-content: center; + justify-content: center; } .page-item { - .page-link { - transition: 0.2s all ease; - } + .page-link { + transition: 0.2s all ease; + } - &.active .page-link, - .page-link:hover { - border-color: $brand-secundary; - background-color: $brand-secundary; - color: $white; - } + &.active .page-link, + .page-link:hover { + border-color: $brand-secundary; + background-color: $brand-secundary; + color: $white; + } } /* Necessary to overrule default styling */ div.dataTables_wrapper div.dataTables_length { - text-align: left; + text-align: left; - label { - justify-content: flex-start; + label { + justify-content: flex-start; - .form-control { - margin-left: $padding-small-horizontal; + .form-control { + margin-left: $padding-small-horizontal; + } } - } } /* Necessary to overrule default styling */ div.dataTables_wrapper div.dataTables_filter input.form-control { - width: 100%; - margin-left: 0; + width: 100%; + margin-left: 0; } // Table fullscreen mode :fullscreen { - body { - padding: 0; - background-color: $white; - } + body { + padding: 0; + background-color: $white; + } - .main { - max-width: none; - } + .main { + max-width: none; + } - .sidebar, - .table-header, - .content-block__navigation, - .content-block__head { - display: none; - } + .sidebar, + .table-header, + .content-block__navigation, + .content-block__head { + display: none; + } - .content-block__main { - padding-top: 0; - } + .content-block__main { + padding-top: 0; + } - .dataTables_wrapper { - padding-top: $padding-large-vertical; - } + .dataTables_wrapper { + padding-top: $padding-large-vertical; + } - .data-table { - margin-top: 0 !important; //Necessary to overrule external styling - } + .data-table { + margin-top: 0 !important; //Necessary to overrule external styling + } } @include media-breakpoint-up(lg) { - .dataTables_wrapper .row--main { - margin-bottom: $padding-large-vertical; - } + .dataTables_wrapper .row--main { + margin-bottom: $padding-large-vertical; + } - .dataTables_length_wrapper { - margin-top: 0; - } + .dataTables_length_wrapper { + margin-top: 0; + } - .dataTables_length label { - justify-content: flex-end; - } + .dataTables_length label { + justify-content: flex-end; + } - .dataTables_info_wrapper { - display: block; - text-align: right; - } + .dataTables_info_wrapper { + display: block; + text-align: right; + } - .dataTables_paginate .pagination { - justify-content: flex-start; - } + .dataTables_paginate .pagination { + justify-content: flex-start; + } } div.dataTables_wrapper div.dataTables_processing, div.td-container div.dt-processing { - top: 10rem; + top: 10rem; } table.table-purge { - thead tr th, - tbody tr td { - text-align: center !important; // For some reason, this doesn't override unless I do !important - } - tbody tr td { - border: 1px solid $gray; - border-top: 0; - } - thead tr th { - background-color: $brand-secundary; - color: $white; - } + thead tr th, + tbody tr td { + text-align: center !important; // For some reason, this doesn't override unless I do !important + } + + tbody tr td { + border: 1px solid $gray; + border-top: 0; + } + + thead tr th { + background-color: $brand-secundary; + color: $white; + } } button.btn-remove, button.btn-add { - margin: $padding-base-vertical 0; + margin: $padding-base-vertical 0; } .dt-column-order { - display: none; - visibility: collapse; + display: none; + visibility: collapse; +} + +.dt-start, +.dt-end { + @extend .py-4; + @extend .align-content-center; + @extend .d-flex; + @extend .flex-row; + @extend .justify-content-between; + @extend .align-items-center; +} + +.dt-start { + flex-grow: 1; + + label { + @extend .sr-only; + } + + .dt-search { + width: 100%; + input { + width: 96% !important; // Necessary to override the default styling + } + } +} + +.dt-length { + display: flex; + flex-direction: row; + align-items: center; + align-content: center; + justify-content: flex-end; + + .custom-select { + margin: 0; + margin-left: 1rem; + max-width: 5rem; + } + + label { + white-space: nowrap; + margin: 0; + padding: 0; + } +} + +.dt-layout-row { + @extend .row; + @extend .justify-content-between; +} + +.dt-layout-cell:has(table.data-table) { + width: 100%; } diff --git a/src/frontend/components/data-table/lib/DataTablesPlugins.ts b/src/frontend/components/data-table/lib/DataTablesPlugins.ts new file mode 100644 index 000000000..ed262784f --- /dev/null +++ b/src/frontend/components/data-table/lib/DataTablesPlugins.ts @@ -0,0 +1,34 @@ +import DataTable from 'datatables.net'; + +/** + * Create a toggle button + * @param id The id of the toggle button + * @param label The label to use for the toggle button + * @param onToggle The function to call when the toggle button is toggled + * @returns A jQuery object representing the toggle button + */ +function createToggleButton(id:string, label:string, checked: boolean, onToggle:(ev:JQuery.Event)=>void) { + const element = $(` +
+
+ + +
+
`); + + element.find(`#${id}`).on('change', (ev) => { + const target = ev.target as HTMLInputElement; + target.checked = !target.checked; + onToggle(ev); + }); + + return element; +} + +// I feel using the "proper" toggle from bootstrap is better than the custom one and adding extra "fluff" to the datatables code in my opinion +DataTable.feature.register('fullscreen', function (settings, opts) { + const options = Object.assign({ + checked: false + }, opts); + return createToggleButton('fullscreen-button', 'Fullscreen', options.checked, options.onToggle); +}); diff --git a/src/frontend/components/data-table/lib/component.js b/src/frontend/components/data-table/lib/component.js index 8366479f6..e9bd93855 100644 --- a/src/frontend/components/data-table/lib/component.js +++ b/src/frontend/components/data-table/lib/component.js @@ -5,16 +5,16 @@ import 'datatables.net-bs4'; import 'datatables.net-buttons-bs4'; import 'datatables.net-responsive-bs4'; import 'datatables.net-rowreorder-bs4'; +import './DataTablesPlugins'; import { setupDisclosureWidgets, onDisclosureClick } from 'components/more-less/lib/disclosure-widgets'; import { moreLess } from 'components/more-less/lib/more-less'; import { bindToggleTableClickHandlers } from './toggle-table'; +import { logging } from 'logging'; const MORE_LESS_TRESHOLD = 50; /** * Component for initializing and managing DataTables - * @todo It is worth noting that there are significant changes between DataTables.net v1 and v2 (hence the major version increase) - We are currently using v2 in this component, but with various deprecated features in use that may need to be updated in the future */ class DataTableComponent extends Component { /** @@ -23,14 +23,20 @@ class DataTableComponent extends Component { */ constructor(element) { super(element); + this.table = element.cloneNode(true); this.el = $(this.element); this.hasCheckboxes = this.el.hasClass('table-selectable'); this.hasClearState = this.el.hasClass('table-clear-state'); this.forceButtons = this.el.hasClass('table-force-buttons'); this.searchParams = new URLSearchParams(window.location.search); this.base_url = this.el.data('href') ? this.el.data('href') : undefined; - this.isFullScreen = false; + this.fullscreen = false; this.initTable(); + $(window).on('resize', () => { + if (this.el.DataTable().responsive) { + this.el.DataTable().responsive.recalc(); + } + }); } /** @@ -324,29 +330,29 @@ class DataTableComponent extends Component { const $searchElement = $( `` + + + ` ); /* Construct search box for filtering. If the filter has a typeahead and if @@ -737,6 +743,20 @@ class DataTableComponent extends Component { return this.renderDataType(data); } + /** + * Setup the fullscreen mode for the DataTable + * @param {Config['layout']} layout The layout configuration for the DataTable + */ + setupFullscreen(layout) { + if (!layout) return; + if (!layout.topEnd) return; + if (Array.isArray(layout.topEnd) && layout.topEnd.includes('fullscreen')) { + layout.topEnd = [...layout.topEnd.filter((item) => item !== 'fullscreen'), { fullscreen: { checked: this.fullscreen, onToggle: (ev) => this.toggleFullScreenMode(ev) } }]; + } else if (layout.topEnd === 'fullscreen') { + layout.topEnd = { fullscreen: { checked: this.fullscreen, onToggle: (ev) => this.toggleFullScreenMode(ev) } }; + } + } + /** * Get the configuration object for the DataTable * @import { Config } from 'datatables.net-bs4'; @@ -753,11 +773,9 @@ class DataTableComponent extends Component { conf = confData; } - if (overrides) { - for (const key in overrides) { - conf[key] = overrides[key]; - } - } + conf = Object.assign({}, conf, overrides); + + this.setupFullscreen(conf.layout); conf.columns.forEach((column) => { column.orderable = column.orderable === 1; @@ -775,7 +793,7 @@ class DataTableComponent extends Component { const tableElement = this.el; const dataTable = tableElement.DataTable(); - this.json = json || undefined; + this.json = json; if (this.initializingTable || conf.reinitialize) { dataTable.columns().every(function (index) { @@ -847,79 +865,54 @@ class DataTableComponent extends Component { this.bindClickHandlersAfterDraw(conf); }; - conf['buttons'] = [ - { - text: 'Full screen', - enabled: false, - attr: { - id: 'full-screen-btn' - }, - className: 'btn btn-small btn-toggle-off', - action: (e) => { - this.toggleFullScreenMode(e); - } - } - ]; - return conf; } /** * Toggle full screen mode for the DataTable - * @param {HTMLButtonElement} buttonElement The button element that was clicked to toggle full screen mode + * @param {JQuery.ClickEvent} ev The click event that triggered the toggle */ - toggleFullScreenMode(buttonElement) { - /* - For some reason, the current code that is present doesn't enable/disable the button as expected; it will disable the button, but will not re-enable the button. - I have tried manually changing the DOM, as well as the methods already present in the code, and I currently believe there is a bug within the DataTables button - code that is meaning that this won't change (although I am open to the fact that I am being a little slow and missing something glaringly obvious). - */ - const table = document.querySelector('table.data-table'); - const currentTable = $(table); - if (currentTable && $.fn.dataTable.isDataTable(currentTable)) { - currentTable.DataTable().destroy(); - } - if (!this.isFullScreen) { - // Create new modal - const newModal = document.createElement('div'); - newModal.id = 'table-modal'; - newModal.classList.add('table-modal'); - newModal.classList.add('data-table__container--scrollable'); - - // Move data table into new modal - newModal.append(table); - document.body.appendChild(newModal); - if (currentTable && !($.fn.dataTable.isDataTable(currentTable))) { - currentTable.DataTable(this.getConf({ responsive: false, reinitialize: true })); - } + toggleFullScreenMode(ev) { + let conf; - $(document).on('keyup', (ev) => { - if (ev.key === 'Escape') { - this.toggleFullScreenMode(buttonElement); - } - }); - } else { - // Move data table back to original page - const mainContent = document.querySelector('.content-block__main-content'); - if (!mainContent) { - console.warn('Failed to close full screen; missing main content'); - return; - } + if ($.fn.DataTable.isDataTable(this.el)) + this.el.DataTable().destroy(); - mainContent.appendChild(table); - if (currentTable && !($.fn.dataTable.isDataTable(currentTable))) { - currentTable.DataTable(this.getConf({ reinitialize: true })); - } - // Remove the modal - document.querySelector('#table-modal').remove(); + if (!this.fullscreen) { + this.fullscreen = true; - $(document).off('keyup'); - } + const frame = document.createElement('div'); + frame.className = 'p-3'; + frame.id = 'fullscreen-frame'; + frame.style.position = 'fixed'; + frame.style.top = '0'; + frame.style.left = '0'; + frame.style.width = '100%'; + frame.style.height = '100%'; + frame.style.overflow = 'auto'; + frame.style.backgroundColor = 'white'; + frame.style.zIndex = '1021'; + frame.style.overflow = 'auto'; + + const newTable = this.table.cloneNode(true); + const $table = $(newTable); + + $table.appendTo(frame); - // Toggle the full screen button - this.isFullScreen = !this.isFullScreen; - $('#full-screen-btn').removeClass(this.isFullScreen ? 'btn-toggle-off' : 'btn-toggle'); - $('#full-screen-btn').addClass(this.isFullScreen ? 'btn-toggle' : 'btn-toggle-off'); + document.body.appendChild(frame); + + conf = this.getConf({ responsive: false, reinitialize: true, el: $table }); + $table.DataTable(conf); + + ev.stopPropagation(); + ev.preventDefault(); + } else if (this.fullscreen) { + this.fullscreen = false; + + $('#fullscreen-frame').remove(); + + this.el.DataTable(this.getConf({ reinitialize: true })); + } } /** @@ -940,8 +933,21 @@ class DataTableComponent extends Component { if (data) { // URL will be record link for standard view, or filtered URL for // grouped view (in which case _count parameter will be present not _id) - const url = data['_id'] ? `${this.base_url}/${data['_id']}` : `?${data['_count']['url']}`; + let url = undefined; + + try { + url = data['_id'] ? `${this.base_url}/${data['_id']}` : `?${data['_count']['url']}`; + } catch (e) { + if (data[0] && data[0].match(//)) { + // If the data is a string with an anchor tag, extract the URL + url = data[0].match(//)[1]; + } else { + logging.error('Error constructing URL for row:', data, e); + return; + } + } + if (!url) return; $(el).find('td:not(".dtr-control")') .on('click', (ev) => { // Only for table cells that are not part of a record-popup table row diff --git a/src/frontend/components/form-group/autosave/lib/autosave.test.ts b/src/frontend/components/form-group/autosave/lib/autosave.test.ts index 113cc014e..e88811f25 100644 --- a/src/frontend/components/form-group/autosave/lib/autosave.test.ts +++ b/src/frontend/components/form-group/autosave/lib/autosave.test.ts @@ -2,7 +2,6 @@ import AutosaveBase from './autosaveBase'; import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'; -// Mocking the AutosaveBase class for testing class TestAutosave extends AutosaveBase { initAutosave(): void { console.log('initAutosave'); diff --git a/src/frontend/components/help-view/lib/component.test.ts b/src/frontend/components/help-view/lib/component.test.ts index b5e4fa94c..d5720a849 100644 --- a/src/frontend/components/help-view/lib/component.test.ts +++ b/src/frontend/components/help-view/lib/component.test.ts @@ -2,7 +2,6 @@ import HelpView from './component'; import { describe, it, expect } from '@jest/globals'; -// Mock class to test the HelpView component exposing private members class TestHelpView extends HelpView { public get button() { return this.$button; diff --git a/src/frontend/components/modal/modals/curval/lib/component.js b/src/frontend/components/modal/modals/curval/lib/component.js index 4c966a5bd..4c53c0036 100644 --- a/src/frontend/components/modal/modals/curval/lib/component.js +++ b/src/frontend/components/modal/modals/curval/lib/component.js @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-this-alias */ import ModalComponent from '../../../lib/component'; import { setFieldValues } from 'set-field-values'; -import { guid as Guid } from 'guid'; import { initializeRegisteredComponents } from 'component'; import { validateRadioGroup, validateCheckboxGroup } from 'validation'; import { fromJson } from 'util/common'; @@ -156,7 +155,7 @@ class CurvalModalComponent extends ModalComponent { // guids in the autosave let is_new_row; if (!guid && !current_id) { - guid = Guid(); + guid = crypto.randomUUID(); is_new_row = true; } const hidden_input = $('').attr({ @@ -197,7 +196,7 @@ class CurvalModalComponent extends ModalComponent { $answersList.find('li input').prop('checked', false); } - guid ||= Guid(); + guid ||= crypto.randomUUID(); const id = `field${col_id}_${guid}`; const deleteButton = multi ? '' @@ -355,7 +354,7 @@ class CurvalModalComponent extends ModalComponent { if (mode === 'edit') { guid = hidden.data('guid'); if (!guid) { - guid = Guid(); + guid = crypto.randomUUID(); hidden.attr('data-guid', guid); } } diff --git a/src/frontend/js/lib/logging.js b/src/frontend/js/lib/logging.js index bc815b787..c757bbc47 100644 --- a/src/frontend/js/lib/logging.js +++ b/src/frontend/js/lib/logging.js @@ -23,7 +23,7 @@ class Logging { */ log(...message) { if (this.allowLogging) { - console.log(message); + console.log(...message); } else { const message = this.formatMessage('log', ...message); uploadMessage(message); @@ -36,7 +36,7 @@ class Logging { */ info(...message) { if (this.allowLogging) { - console.info(message); + console.info(...message); } else { const message = this.formatMessage('info', ...message); uploadMessage(message); @@ -49,7 +49,7 @@ class Logging { */ warn(...message) { if (this.allowLogging) { - console.warn(message); + console.warn(...message); } else { const message = this.formatMessage('warn', ...message); uploadMessage(message); @@ -62,7 +62,7 @@ class Logging { */ error(...message) { if (this.allowLogging) { - console.error(message); + console.error(...message); } else { const message = this.formatMessage('error', ...message); uploadMessage(message); diff --git a/src/frontend/js/lib/set-field-value.test.ts b/src/frontend/js/lib/set-field-value.test.ts index fe713ed83..d22c98ea6 100644 --- a/src/frontend/js/lib/set-field-value.test.ts +++ b/src/frontend/js/lib/set-field-value.test.ts @@ -13,7 +13,6 @@ import textAreaComponent from 'components/form-group/textarea'; import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { setFieldValues } from './set-field-values'; -// Mocking jQuery plugins declare global { interface JQuery { renameButton: (options?: any) => JQuery; @@ -28,7 +27,6 @@ declare global { $.fn.filedrag = jest.fn().mockReturnThis(); })(jQuery); -// DOM elements for testing const stringDom = `
{ document.body.appendChild(dom); const field = $(dom); /* @ts-ignore */ - expect(() => setFieldValues(field, 'test')).toThrowError('Attempt to set value for text without array'); + expect(() => setFieldValues(field, 'test')).toThrow('Attempt to set value for text without array'); }); describe('String field', () => { diff --git a/src/frontend/js/lib/util/encryptedStorage/lib/encryptedStorage.test.ts b/src/frontend/js/lib/util/encryptedStorage/lib/encryptedStorage.test.ts index eeb301acd..11d8d4b19 100644 --- a/src/frontend/js/lib/util/encryptedStorage/lib/encryptedStorage.test.ts +++ b/src/frontend/js/lib/util/encryptedStorage/lib/encryptedStorage.test.ts @@ -5,7 +5,6 @@ import { describe, it, expect, beforeAll, beforeEach, afterEach } from '@jest/gl import { setupCrypto } from 'testing/globals.definitions'; import { EncryptedStorage } from './encryptedStorage'; -// Mock implementation of the Storage interface for testing purposes class TestStorage implements Storage { private map = new Map(); @@ -16,7 +15,6 @@ class TestStorage implements Storage { this.map.clear(); this.length = 0; } - getItem(key: string): string | null { const ret = this.map.get(key); if (ret === undefined) { @@ -24,7 +22,6 @@ class TestStorage implements Storage { } return ret; } - key(index: number): string | null { const keys = Array.from(this.map.keys()); if (keys.length <= index) { @@ -32,7 +29,6 @@ class TestStorage implements Storage { } return keys[index]; } - removeItem(key: string): void { if (this.map.has(key)) { this.map.delete(key); @@ -40,7 +36,6 @@ class TestStorage implements Storage { this[key] = undefined; } } - setItem(key: string, value: string): void { this.map.set(key, value); this[key] = value; diff --git a/src/frontend/js/lib/util/filedrag/lib/filedrag.test.ts b/src/frontend/js/lib/util/filedrag/lib/filedrag.test.ts index 420307622..0abedd382 100644 --- a/src/frontend/js/lib/util/filedrag/lib/filedrag.test.ts +++ b/src/frontend/js/lib/util/filedrag/lib/filedrag.test.ts @@ -2,7 +2,6 @@ import { describe, it, expect, jest } from '@jest/globals'; import FileDrag from './filedrag'; -// Test class implementation to expose private methods for testing class FileDragTest extends FileDrag { constructor(element: HTMLElement, onDrop: (files: File, index?: number, length?: number) => void = ()=>{}) { super(element, { debug: true }, onDrop); diff --git a/src/frontend/js/lib/util/formatters/markdown.ts b/src/frontend/js/lib/util/formatters/markdown.ts index 59372ca6f..696434bdd 100644 --- a/src/frontend/js/lib/util/formatters/markdown.ts +++ b/src/frontend/js/lib/util/formatters/markdown.ts @@ -11,7 +11,6 @@ type stringLike = { toString(): string }; * @returns {MarkdownCode} The formatted markdown string, processed by the marked library. */ function MarkDown(strings: TemplateStringsArray, ...values: (stringLike | string | number | MarkdownCode)[]): MarkdownCode { - marked.use({ breaks: true }); let str = ''; for (let i = 0; i < strings.length; i++) { str += strings[i]; @@ -20,7 +19,7 @@ function MarkDown(strings: TemplateStringsArray, ...values: (stringLike | string } } str = str.replace(/\\n/g, '\n\n'); - return marked(str).trim(); + return marked(str, { async: false, breaks: true }).trim(); } export { MarkDown }; diff --git a/src/frontend/js/lib/util/gadsStorage/lib/GadsStorage.ts b/src/frontend/js/lib/util/gadsStorage/lib/GadsStorage.ts index 0634dfb91..5c76f225a 100644 --- a/src/frontend/js/lib/util/gadsStorage/lib/GadsStorage.ts +++ b/src/frontend/js/lib/util/gadsStorage/lib/GadsStorage.ts @@ -23,7 +23,7 @@ export class GadsStorage implements AppStorage { /** * Fetches the storage key used to encrypt data. - * @returns {Promise} The storage key used to encrypt data. + * @returns {Promise} The storage key used to encrypt data. */ private async getStorageKey(): Promise { if (window.test) { diff --git a/src/frontend/js/lib/util/typeahead/lib/TypeaheadBuilder.ts b/src/frontend/js/lib/util/typeahead/lib/TypeaheadBuilder.ts index c22134d88..0c6ff5ee9 100644 --- a/src/frontend/js/lib/util/typeahead/lib/TypeaheadBuilder.ts +++ b/src/frontend/js/lib/util/typeahead/lib/TypeaheadBuilder.ts @@ -135,7 +135,7 @@ export class TypeaheadBuilder { * @returns {Typeahead} The built Typeahead class * @throws {Error} If input, callback, name, or ajax source is not set */ - build(): Typeahead { + build() { if (!this.$input) throw new Error('Input not set'); if (!this.callback) throw new Error('Callback not set'); if (!this.name) throw new Error('Name not set'); diff --git a/src/frontend/js/lib/util/typeahead/lib/TypeaheadSourceOptions.ts b/src/frontend/js/lib/util/typeahead/lib/TypeaheadSourceOptions.ts index 26ec3c47b..5c10abc14 100644 --- a/src/frontend/js/lib/util/typeahead/lib/TypeaheadSourceOptions.ts +++ b/src/frontend/js/lib/util/typeahead/lib/TypeaheadSourceOptions.ts @@ -11,7 +11,7 @@ export class TypeaheadSourceOptions { * @param {MapperFunction} mapper The function to map the data * @param {boolean} appendQuery Whether to append the query to the AJAX request * @param {*} data Any additional data to send with the request - * @param {(...args:any[]) => any} dataBuilder A function to build the data for the request + * @param {(...args:any[]) => any} [dataBuilder] A function to build the data for the request * @param {'GET'|'POST'} method The request method to use */ constructor( diff --git a/views/admin/audit.tt b/views/admin/audit.tt index e593c8482..394858204 100644 --- a/views/admin/audit.tt +++ b/views/admin/audit.tt @@ -3,7 +3,12 @@ # PROCESS snippets/datum.tt # prepare files table config - table_dom = 'Bt'; + table_layout = { + topStart = undef, + topEnd = "fullscreen", + bottomStart = undef, + bottomEnd = undef + }; table_show_all_records = 1; table_caption = "Table for audit log"; @@ -80,7 +85,7 @@ Filter - +
  • Download @@ -90,7 +95,7 @@
  • - + [% INCLUDE tables/basic_table.tt; %] diff --git a/views/data_table.tt b/views/data_table.tt index 1e141fb24..7cca505a1 100755 --- a/views/data_table.tt +++ b/views/data_table.tt @@ -2,7 +2,7 @@
    - +
    [% IF layout.show_add_record AND layout.user_can('write_new') %] @@ -24,10 +24,15 @@
    [% END; - + # prepare table config table_class = (table_clear_state ? 'table-clear-state ' : '') _ 'table-striped table-hover table-search'; - table_dom = '<"row row--header"<"col"' _ (session.rewind ? '' : 'f') _ '><"col-lg-auto dataTables_length_wrapper"l><"col-lg-auto dataTables_toggle_full_width"B>><"row row--main"<"col-sm-12"tr>><"row row--footer"<"col-sm-12 col-lg-6"p><"col-sm-12 col-lg-6 dataTables_info_wrapper"i>>'; + table_layout = { + topStart => session.rewind ? undef : "search", + topEnd => ["pageLength", "fullscreen"], + bottomStart => "paging", + bottomEnd => "info", + }; table_ajax = url.page _ "/api/" _ layout.identifier _ "/records?csrf-token=" _ csrf_token _ "&group_filter=" _ group_filter _ "&curval_layout_id=" _ curval_layout_id _ "&curval_record_id=" _ curval_record_id; table_ajax_target = url.page _ "/" _ layout.identifier _ "/record"; # will be appended with /{id} by JS table_order = []; @@ -38,7 +43,7 @@ next => "Next page", previous => "Previous page" }, - search => "Search in table:", + search => "Search in table:", searchPlaceholder => "Search in this table", }; table_page_length = 50; @@ -49,7 +54,7 @@ table_columns = []; rows = []; processed_columns = []; - + IF is_group; table_columns.push({ name = '_count', @@ -59,14 +64,14 @@ filter = "html" }); END; - + col_counter = 0; - + FOREACH col IN columns; IF col.sort.type == 'asc' OR col.sort.type == 'desc'; table_order.push([ col_counter, col.sort.type ]); END; - + table_columns.push({ name = col.full_id, title = col.name, @@ -76,12 +81,12 @@ typeahead = col.has_typeahead, typeahead_use_id = col.typeahead_use_id, }); - + processed_columns.push(col.id); - + col_counter = col_counter + 1; END; - + IF aggregate; add_blank_footer = 1; END; diff --git a/views/edit.tt b/views/edit.tt index e99bceac4..d33fc27f1 100755 --- a/views/edit.tt +++ b/views/edit.tt @@ -27,15 +27,15 @@ > [% INCLUDE fields/hidden.tt name="csrf_token" value=csrf_token; - + IF submission_token; INCLUDE fields/hidden.tt name="submission_token" value=submission_token; END; - + IF cur_id; INCLUDE fields/hidden.tt name="current_id" value=record.current_id; END; - + IF child; INCLUDE fields/hidden.tt name="child" value=child; END; @@ -49,7 +49,7 @@ [% page_title | html %]
    - +
    [% IF NOT record.new_entry AND NOT edit_modal AND NOT view_modal %] @@ -59,7 +59,7 @@

    Child records

    - +
    @@ -77,12 +77,12 @@
    [% END %] - +

    Meta data

    - +
    @@ -99,14 +99,14 @@
    - +
  • Last edited time [% record.version_datetime.data.value | html %]
  • - +
    • @@ -121,7 +121,7 @@
    - +
  • Created time [% record.created_datetime.data.value | html %] @@ -131,12 +131,12 @@
  • - +

    Version history

    - +
    @@ -149,7 +149,7 @@
    [% dropdown_title = "Select version..."; - + FOREACH version IN record.versions; IF version.id == record.record_id; dropdown_title = version.created; @@ -165,7 +165,7 @@
    - - + +
    - +
    - +
    - + - + [% IF record.has_rag_column %]

    Legend

    - +
    [% INCLUDE snippets/rag_legend.tt %] @@ -213,26 +213,26 @@ [% END %]
    [% END %] - +
    [% PROCESS form %]
    - +