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 = $(
`
-
- Search in ${title}
-
-
-
`
+
+ Search in ${title}
+
+
+ `
);
/* 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 = `
-
+
[% 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 @@
-
+
-
+
-
+
-
+
-
+
[% IF record.has_rag_column %]
-
+