diff --git a/.eslintrc.json b/.eslintrc.json index cfb7b4048f9..5057ae7524b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -47,6 +47,7 @@ "tabWidth": 2, "ignoreComments": true }], + "indent": ["error", 2], "no-unexpected-multiline": "off", "no-unused-vars": "error", "no-useless-escape": "off", @@ -110,8 +111,9 @@ }} ], "jsdoc/require-param-description": "error", - "jsdoc/require-returns": "error", "jsdoc/require-returns-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns-type": "off", "no-undef": "off", "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-explicit-any": "off" diff --git a/.github/workflows/loristest.yml b/.github/workflows/loristest.yml index b6ed076cf69..b48b67cf342 100644 --- a/.github/workflows/loristest.yml +++ b/.github/workflows/loristest.yml @@ -11,7 +11,7 @@ jobs: EEG_VIS_ENABLED: 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install EEG package dependencies # We only need to install protobuf-compiler @@ -36,13 +36,13 @@ jobs: - name: Create node_modules tarball run: tar cfvz node_modules.tar.gz node_modules - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: Upload node_modules artifact with: name: node_modules path: node_modules.tar.gz - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: Upload lorisjs.tar.gz artifact with: name: lorisjs @@ -52,9 +52,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['8.1', '8.2', '8.3'] + php: ['8.3'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -83,7 +83,7 @@ jobs: - name: Create vendor tarball run: tar cfvz vendor-php${{matrix.php}}.tar.gz vendor - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: Upload vendor-php${{matrix.php}}.tar.gz artifact with: name: vendor-php${{matrix.php}} @@ -104,33 +104,34 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.1', '8.2', '8.3'] + php: ['8.3'] apiversion: ['v0.0.3', 'v0.0.4-dev'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 - name: Download node_modules artifact + # Cache node_modules to avoid downloading dependencies if already cached + - name: Cache node_modules + uses: actions/cache@v3 with: - name: node_modules - path: . - - - uses: actions/download-artifact@v3 - name: Download compiled LORIS javascript artifact + path: node_modules + key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + # Download and extract compiled LORIS JavaScript artifact (from build job) + - name: Download compiled LORIS javascript artifact + uses: actions/download-artifact@v4 with: name: lorisjs path: . - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 name: Download PHP dependencies artifact with: name: vendor-php${{matrix.php}} path: . - - name: Extract node_modules - run: tar xfvz node_modules.tar.gz - - name: Extract compiled JS run: tar xfvz lorisjs.tar.gz @@ -209,28 +210,20 @@ jobs: fail-fast: false matrix: testsuite: ['integration'] - php: ['8.1','8.2', '8.3'] + php: ['8.3'] ci_node_index: [0,1,2,3] include: # add a variable but do not display it in the job's name - ci_node_total: 4 - - testsuite: 'static' - php: '8.1' - - testsuite: 'static' - php: '8.2' - testsuite: 'static' php: '8.3' - - testsuite: 'unit' - php: '8.1' - - testsuite: 'unit' - php: '8.2' - testsuite: 'unit' php: '8.3' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b6daa1373..36e1e9291e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ core section.*** - ***When possible please provide the number of the pull request(s) containing the changes in the following format: PR #1234*** -## LORIS 26.0 (Release Date: ????-??-??) +## LORIS 26.0 (Release Date: 2024-06-13) ### Core #### Features - Add OpenID Connect authorization support to LORIS (PR #8255) @@ -26,7 +26,7 @@ changes in the following format: PR #1234*** - While proposing a project or editing a project in publications module, prevent indefinite "File to upload" fields from being added if files are browsed then cancelled (PR #9179) - Conflict resolver fixed when Test_name is not equal to table name. This is done be replacing the "TableName" variable with "TestName" everywhere in resolved & unresolved conflicts tables as well as modules (PR #9270) -## LORIS 25.0 (Release Date: ????-??-??) +## LORIS 25.0 (Release Date: 2023-07-17) ### Core #### Features - Added new interface intended to be used for querying module data from PHP (PR #8215) diff --git a/README.md b/README.md index cba522a0958..34cc39dce4c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ LORIS (Longitudinal Online Research and Imaging System) is a self-hosted web app * Try the LORIS demo instance at https://demo.loris.ca. -This Readme covers installation of LORIS version 25.0 on Ubuntu. +This Readme covers installation of LORIS version 26.0 on Ubuntu. ([CentOS Readme also available](docs/wiki/00_SERVER_INSTALL_AND_CONFIGURATION/01_LORIS_Install/CentOS/README.md)). diff --git a/SQL/0000-00-00-schema.sql b/SQL/0000-00-00-schema.sql index b3894004603..deabd1eff4f 100644 --- a/SQL/0000-00-00-schema.sql +++ b/SQL/0000-00-00-schema.sql @@ -292,7 +292,7 @@ CREATE TABLE `instrument_data` ( CREATE TABLE `flag` ( `ID` int(10) unsigned NOT NULL auto_increment, `SessionID` int(10) unsigned NOT NULL, - `Test_name` varchar(255) NOT NULL default '', + `TestID` int(10) unsigned NOT NULL, `CommentID` varchar(255) NOT NULL default '', `Data_entry` enum('In Progress','Complete') default NULL, `Required_elements_completed` enum('Y','N') NOT NULL default 'N', @@ -305,15 +305,14 @@ CREATE TABLE `flag` ( PRIMARY KEY (`CommentID`), KEY `flag_ID` (`ID`), KEY `flag_SessionID` (`SessionID`), - KEY `flag_Test_name` (`Test_name`), KEY `flag_Exclusion` (`Exclusion`), KEY `flag_Data_entry` (`Data_entry`), KEY `flag_Validity` (`Validity`), KEY `flag_Administration` (`Administration`), KEY `flag_UserID` (`UserID`), CONSTRAINT `FK_flag_1` FOREIGN KEY (`SessionID`) REFERENCES `session` (`ID`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `FK_flag_2` FOREIGN KEY (`Test_name`) REFERENCES `test_names` (`Test_name`), - CONSTRAINT `FK_flag_3` FOREIGN KEY (`DataID`) REFERENCES `instrument_data` (`ID`) + CONSTRAINT `FK_flag_3` FOREIGN KEY (`DataID`) REFERENCES `instrument_data` (`ID`), + CONSTRAINT `FK_ibfk_1` FOREIGN KEY (`TestID`) REFERENCES `test_names` (`ID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `history` ( @@ -342,6 +341,7 @@ CREATE TABLE `test_battery` ( `CenterID` int(11) default NULL, `firstVisit` enum('Y','N') default NULL, `instr_order` tinyint(4) default NULL, + `DoubleDataEntryEnabled` enum('Y','N') default 'N', PRIMARY KEY (`ID`), KEY `age_test` (`AgeMinDays`,`AgeMaxDays`,`Test_name`), KEY `FK_test_battery_1` (`Test_name`), @@ -1529,6 +1529,7 @@ CREATE TABLE `issues` ( `candID` int(6) DEFAULT NULL, `category` varchar(255) DEFAULT NULL, `description` longtext DEFAULT NULL, + `instrument` int(10) unsigned DEFAULT NULL, PRIMARY KEY (`issueID`), KEY `fk_issues_1` (`reporter`), KEY `fk_issues_2` (`assignee`), @@ -1537,20 +1538,22 @@ CREATE TABLE `issues` ( KEY `fk_issues_5` (`centerID`), KEY `fk_issues_6` (`lastUpdatedBy`), KEY `fk_issues_8` (`category`), + KEY `fk_issues_instrument` (`instrument`), CONSTRAINT `fk_issues_8` FOREIGN KEY (`category`) REFERENCES `issues_categories` (`categoryName`), CONSTRAINT `fk_issues_1` FOREIGN KEY (`reporter`) REFERENCES `users` (`UserID`), CONSTRAINT `fk_issues_2` FOREIGN KEY (`assignee`) REFERENCES `users` (`UserID`), CONSTRAINT `fk_issues_3` FOREIGN KEY (`candID`) REFERENCES `candidate` (`CandID`), CONSTRAINT `fk_issues_4` FOREIGN KEY (`sessionID`) REFERENCES `session` (`ID`), CONSTRAINT `fk_issues_5` FOREIGN KEY (`centerID`) REFERENCES `psc` (`CenterID`), - CONSTRAINT `fk_issues_6` FOREIGN KEY (`lastUpdatedBy`) REFERENCES `users` (`UserID`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; + CONSTRAINT `fk_issues_6` FOREIGN KEY (`lastUpdatedBy`) REFERENCES `users` (`UserID`), + CONSTRAINT `fk_issues_instrument` FOREIGN KEY (`instrument`) REFERENCES `test_names` (`ID`) ON DELETE RESTRICT ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `issues_history` ( `issueHistoryID` int(11) unsigned NOT NULL AUTO_INCREMENT, `newValue` longtext NOT NULL, `dateAdded` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `fieldChanged` enum('assignee','status','comment','sessionID','centerID','title','category','module','lastUpdatedBy','priority','candID', 'description','watching') NOT NULL DEFAULT 'comment', + `fieldChanged` enum('assignee','status','comment','sessionID','centerID','title','category','module','lastUpdatedBy','priority','candID', 'description','watching','instrument') NOT NULL DEFAULT 'comment', `issueID` int(11) unsigned NOT NULL, `addedBy` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`issueHistoryID`), @@ -2096,7 +2099,9 @@ CREATE TABLE `data_release` ( `file_name` varchar(255), `version` varchar(255), `upload_date` date, - PRIMARY KEY (`id`) + `ProjectID` INT(10) UNSIGNED NULL, + PRIMARY KEY (`id`), + FOREIGN KEY (ProjectID) REFERENCES Project (ProjectID) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `data_release_permissions` ( diff --git a/SQL/0000-00-03-ConfigTables.sql b/SQL/0000-00-03-ConfigTables.sql index f170f9775bf..e7b4d637a0d 100644 --- a/SQL/0000-00-03-ConfigTables.sql +++ b/SQL/0000-00-03-ConfigTables.sql @@ -51,7 +51,6 @@ INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'useConsent', 'Enable if the study uses the loris architecture for consent', 1, 0, 'boolean', ID, 'Use consent', 16 FROM ConfigSettings WHERE Name="study"; INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'additional_user_info', 'Display additional user profile fields on the User accounts page (e.g. Institution, Position, Country, Address)', 1, 0, 'boolean', ID, 'Additional user information', 17 FROM ConfigSettings WHERE Name="study"; INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'excluded_instruments', "Instruments to be excluded from the Data Dictionary and download via the Data Query Tool", 1, 1, 'instrument', ID, 'Excluded instruments', 18 FROM ConfigSettings WHERE Name="study"; -INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'DoubleDataEntryInstruments', "Instruments for which double data entry should be enabled", 1, 1, 'instrument', ID, 'Double data entry instruments', 19 FROM ConfigSettings WHERE Name="study"; INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'InstrumentResetting', 'Allows resetting of instrument data', 1, 0, 'boolean', ID, 'Instrument Resetting', 20 FROM ConfigSettings WHERE Name="study"; INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'SupplementalSessionStatus', 'Display supplemental session status information on Timepoint List page', 1, 0, 'boolean', ID, 'Use Supplemental Session Status', 21 FROM ConfigSettings WHERE Name="study"; INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'useScanDone', 'Used for identifying timepoints that have (or should have) imaging data', 1, 0, 'boolean', ID, 'Use Scan Done', 22 FROM ConfigSettings WHERE Name="study"; @@ -63,6 +62,7 @@ INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'dateDisplayFormat', 'The date format to use throughout LORIS for displaying date information - formats for date inputs are browser- and locale-dependent.', 1, 0, 'text', ID, 'Date display format', 28 FROM ConfigSettings WHERE Name="study"; INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'adminContactEmail', 'An email address that users can write to in order to report issues or ask question', 1, 0, 'text', ID, 'Administrator Email', 29 FROM ConfigSettings WHERE Name="study"; INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'UserMaximumDaysInactive', 'The maximum number of days since last login before making a user inactive', 1, 0, 'text', ID, 'Maximum Days Before Making User Inactive', 30 FROM ConfigSettings WHERE Name="study"; +INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'useDoB', 'Use DoB (Date of Birth)', 1, 0, 'boolean', ID, 'Use DoB', 31 FROM ConfigSettings WHERE Name="study"; INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, Label, OrderNumber) VALUES ('paths', 'Specify directories where LORIS-related files are stored or created. Take care when editing these fields as changing them incorrectly can cause certain modules to lose functionality.', 1, 0, 'Paths', 2); INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'imagePath', 'Path to images for display in Imaging Browser (e.g. /data/$project/data/) ', 1, 0, 'text', ID, 'Images', 9 FROM ConfigSettings WHERE Name="paths"; @@ -188,6 +188,7 @@ INSERT INTO Config (ConfigID, Value) SELECT ID, "Example Study" FROM ConfigSetti INSERT INTO Config (ConfigID, Value) SELECT ID, "

Example Study Description

\r\n

This is a sample description for this study, because it is a new LORIS install that has not yet customized this text.

\r\n

A LORIS administrator can customize this text in the configuration module, under the configuration option labeled \"Study Description\"

\r\n

Useful Links

\r\n " FROM ConfigSettings WHERE Name="StudyDescription"; INSERT INTO Config (ConfigID, Value) SELECT ID, "images/neurorgb_web.jpg" FROM ConfigSettings WHERE Name="studylogo"; INSERT INTO Config (ConfigID, Value) SELECT ID, "false" FROM ConfigSettings WHERE Name="useEDC"; +INSERT INTO Config (ConfigID, Value) SELECT ID, "false" FROM ConfigSettings WHERE Name="useDoB"; INSERT INTO Config (ConfigID, Value) SELECT ID, 8 FROM ConfigSettings WHERE Name="ageMin"; INSERT INTO Config (ConfigID, Value) SELECT ID, 11 FROM ConfigSettings WHERE Name="ageMax"; INSERT INTO Config (ConfigID, Value) SELECT ID, "false" FROM ConfigSettings WHERE Name="useFamilyID"; diff --git a/SQL/Cleanup_patches/2024-09-13-NoTestName.sql b/SQL/Cleanup_patches/2024-09-13-NoTestName.sql new file mode 100644 index 00000000000..d6605cd5ca8 --- /dev/null +++ b/SQL/Cleanup_patches/2024-09-13-NoTestName.sql @@ -0,0 +1,2 @@ +ALTER TABLE flag DROP CONSTRAINT FK_flag_2; +ALTER TABLE flag DROP COLUMN test_name; diff --git a/SQL/New_patches/2023-12-08_useDoB.sql b/SQL/New_patches/2023-12-08_useDoB.sql new file mode 100644 index 00000000000..e96a7cf02e7 --- /dev/null +++ b/SQL/New_patches/2023-12-08_useDoB.sql @@ -0,0 +1,2 @@ +INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'useDoB', 'Use DoB (Date of Birth)', 1, 0, 'boolean', ID, 'Use DoB', 31 FROM ConfigSettings WHERE Name="study"; +INSERT INTO Config (ConfigID, Value) SELECT ID, "false" FROM ConfigSettings WHERE Name="useDoB"; diff --git a/SQL/New_patches/2024-07-19-issuetracker_AddsInstrumentToIssuesTable.sql b/SQL/New_patches/2024-07-19-issuetracker_AddsInstrumentToIssuesTable.sql new file mode 100644 index 00000000000..e0a8045d543 --- /dev/null +++ b/SQL/New_patches/2024-07-19-issuetracker_AddsInstrumentToIssuesTable.sql @@ -0,0 +1,11 @@ +ALTER TABLE `issues` + ADD `instrument` int(10) unsigned DEFAULT NULL + AFTER `description`; + +ALTER TABLE `issues` + ADD CONSTRAINT `fk_issues_instrument` + FOREIGN KEY (`instrument`) REFERENCES `test_names` (`ID`) + ON DELETE RESTRICT ON UPDATE CASCADE; + +ALTER TABLE `issues_history` + MODIFY `fieldChanged` enum('assignee','status','comment','sessionID','centerID','title','category','module','lastUpdatedBy','priority','candID', 'description','watching','instrument') NOT NULL DEFAULT 'comment'; diff --git a/SQL/New_patches/2024-09-13-TestID.sql b/SQL/New_patches/2024-09-13-TestID.sql new file mode 100644 index 00000000000..b2131bf97f4 --- /dev/null +++ b/SQL/New_patches/2024-09-13-TestID.sql @@ -0,0 +1,6 @@ +ALTER TABLE flag ADD COLUMN TestID int(10) unsigned AFTER test_name; +ALTER TABLE flag ADD CONSTRAINT FOREIGN KEY (TestID) REFERENCES test_names(ID); + +UPDATE flag f SET TestID=(SELECT ID FROM test_names tn WHERE f.test_name=tn.test_name); + +ALTER TABLE flag MODIFY COLUMN TestID int(10) unsigned NOT NULL; diff --git a/SQL/New_patches/2024-10-02-Data_Release_Project_separation.sql b/SQL/New_patches/2024-10-02-Data_Release_Project_separation.sql new file mode 100644 index 00000000000..d067ce80fc8 --- /dev/null +++ b/SQL/New_patches/2024-10-02-Data_Release_Project_separation.sql @@ -0,0 +1,4 @@ +ALTER TABLE data_release +ADD COLUMN ProjectID INT(10) UNSIGNED NULL DEFAULT NULL, +ADD CONSTRAINT FK_ProjectID +FOREIGN KEY (ProjectID) REFERENCES Project (ProjectID); \ No newline at end of file diff --git a/SQL/New_patches/2024_05_13_Add_DDE_To_Battery.sql b/SQL/New_patches/2024_05_13_Add_DDE_To_Battery.sql new file mode 100644 index 00000000000..5e508c7ef28 --- /dev/null +++ b/SQL/New_patches/2024_05_13_Add_DDE_To_Battery.sql @@ -0,0 +1,9 @@ +ALTER TABLE test_battery ADD COLUMN DoubleDataEntryEnabled enum("Y", "N") DEFAULT "N"; + +UPDATE test_battery SET DoubleDataEntryEnabled = 'Y' WHERE Test_name IN ( + SELECT Value from Config WHERE ConfigID = (SELECT ID FROM ConfigSettings WHERE Name = 'DoubleDataEntryInstruments') +); + +DELETE FROM Config WHERE ConfigID IN (SELECT ID FROM ConfigSettings WHERE Name = 'DoubleDataEntryInstruments'); + +DELETE FROM ConfigSettings WHERE Name = 'DoubleDataEntryInstruments'; diff --git a/docker-compose.yml b/docker-compose.yml index 36f6a6d60c8..2b20f2ccfb5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3.8' services: db: build: @@ -13,12 +13,9 @@ services: - MYSQL_RANDOM_ROOT_PASSWORD=yes selenium: - image: selenium/standalone-firefox-debug:3.141.59-zirconium - volumes: - - /dev/shm:/dev/shm + image: selenium/standalone-firefox:4.25 ports: - "5900:5900" - web: build: context: . @@ -62,58 +59,3 @@ services: - selenium - web entrypoint: /app/test/wait-for-services.sh - - selenium-debug: - image: selenium/standalone-firefox-debug:3.141.59-zirconium - links: - - web-debug:web - ports: - - "5901:5900" - - web-debug: - build: - context: . - dockerfile: Dockerfile.test.php8.debug - volumes: - - ./:/app - - ./test/test_instrument:/app/project/instruments - environment: - - LORIS_DB_CONFIG=/app/test/config.xml - - XDEBUG_CONFIG=remote_host=${XDEBUG_REMOTE_HOST} - - PHP_IDE_CONFIG=serverName=LorisTests - depends_on: - - db - command: php -S 0.0.0.0:8000 -t /app/htdocs /app/htdocs/router.php - - unit-tests-debug: - build: - context: . - dockerfile: Dockerfile.test.php8.debug - volumes: - - ./:/app - working_dir: /app - environment: - - LORIS_DB_CONFIG=test/config.xml - - XDEBUG_CONFIG=remote_host=${XDEBUG_REMOTE_HOST} - - PHP_IDE_CONFIG=serverName=LorisTests - depends_on: - - db - entrypoint: /app/test/wait-for-services.sh - - integration-tests-debug: - build: - context: . - dockerfile: Dockerfile.test.php8.debug - volumes: - - ./:/app - working_dir: /app - environment: - - LORIS_DB_CONFIG=test/config.xml - - SELENIUM_REQUIRED=true - - XDEBUG_CONFIG=remote_host=${XDEBUG_REMOTE_HOST} - - PHP_IDE_CONFIG=serverName=LorisTests - links: - - db - - selenium-debug:selenium - - web-debug:web - entrypoint: /app/test/wait-for-services.sh diff --git a/docs/wiki/00_SERVER_INSTALL_AND_CONFIGURATION/01_LORIS_Install/CentOS/README.md b/docs/wiki/00_SERVER_INSTALL_AND_CONFIGURATION/01_LORIS_Install/CentOS/README.md index 5f4f132ebe2..c77ecdbf548 100644 --- a/docs/wiki/00_SERVER_INSTALL_AND_CONFIGURATION/01_LORIS_Install/CentOS/README.md +++ b/docs/wiki/00_SERVER_INSTALL_AND_CONFIGURATION/01_LORIS_Install/CentOS/README.md @@ -10,9 +10,9 @@ For further details on the install process, please see the LORIS GitHub Wiki Cen # System Requirements - Install dependencies Default dependencies installed by CentOS 7.x may not meet the version requirements for LORIS deployment or development: -* MariaDB 10.3 is supported for LORIS 25. +* MariaDB 10.3 is supported for LORIS 26. -* PHP 8.1 (or higher) is supported for LORIS 25. +* PHP 8.2 (or higher) is supported for LORIS 26. In addition to the above, the following packages should be installed with `yum` and may also differ from the packages referenced in the main (Ubuntu) [LORIS Readme](../../../../../README.md). Detailed command examples are provided below (`sudo` privilege may be required depending on your system). * Apache 2.4 or higher diff --git a/htdocs/AjaxHelper.php b/htdocs/AjaxHelper.php index 09dff46b3db..a9521b74b95 100644 --- a/htdocs/AjaxHelper.php +++ b/htdocs/AjaxHelper.php @@ -1,4 +1,5 @@ - { - let combined; - if (remainder.length == 0) { - combined = value; - } else { - combined = new Uint8Array( - value.length + remainder.length - ); - for (let i = 0; i < remainder.length; i++) { - combined[i] = remainder[i]; - } - for (let i = 0; i < value.length; i++) { - combined[i+remainder.length] = value[i]; - } - } - return processLines(combined, rowcb, endstreamcb); - }).then(({remainder: rem, eos}) => { - chunkcb(eos); - doneLoop = eos; - remainder = rem; - }).catch((err) => { - console.error(err); - doneLoop = true; - }); - } + let remainder = []; + let doneLoop = false; + while (!doneLoop) { + await reader.read().then(({done, value}) => { + let combined; + if (remainder.length == 0) { + combined = value; + } else { + combined = new Uint8Array( + value.length + remainder.length + ); + for (let i = 0; i < remainder.length; i++) { + combined[i] = remainder[i]; + } + for (let i = 0; i < value.length; i++) { + combined[i+remainder.length] = value[i]; + } + } + return processLines(combined, rowcb, endstreamcb); + }).then(({remainder: rem, eos}) => { + chunkcb(eos); + doneLoop = eos; + remainder = rem; + }).catch((err) => { + console.error(err); + doneLoop = true; + }); + } } export default fetchDataStream; diff --git a/jsx/Breadcrumbs.js b/jsx/Breadcrumbs.js index cef326354d6..c5ab758b09b 100644 --- a/jsx/Breadcrumbs.js +++ b/jsx/Breadcrumbs.js @@ -106,9 +106,9 @@ class Breadcrumbs extends Component { } else { breadcrumbs.push( + href={url} + className='btn btn-primary' + onClick={onClick}>
{element.text}
@@ -121,10 +121,10 @@ class Breadcrumbs extends Component { breadcrumbDropdown = (
diff --git a/jsx/CSSGrid.js b/jsx/CSSGrid.js index 5c95e8b8aec..b678901d60a 100644 --- a/jsx/CSSGrid.js +++ b/jsx/CSSGrid.js @@ -17,111 +17,111 @@ import PropTypes from 'prop-types'; * @return {object} - A React component for a CSS grid of cards */ function CSSGrid(props) { - const cardsRef = useRef(null); - const [cardWidth, setCardWidth] = useState(0); - const [panelHeights, setPanelHeights] = useState({}); + const cardsRef = useRef(null); + const [cardWidth, setCardWidth] = useState(0); + const [panelHeights, setPanelHeights] = useState({}); - useEffect(() => { - // Upon load, store the calculated height of every rendered panel - // in state, so that we can use it to dynamically set the heights - // (number of rows spanned) in the CSS grid. - if (cardsRef.current.childNodes.length < 1) { - return; - } + useEffect(() => { + // Upon load, store the calculated height of every rendered panel + // in state, so that we can use it to dynamically set the heights + // (number of rows spanned) in the CSS grid. + if (cardsRef.current.childNodes.length < 1) { + return; + } - // All rows in the width have the same width, so only look - // up the first. - const wSize = cardsRef.current.childNodes[0].clientWidth; + // All rows in the width have the same width, so only look + // up the first. + const wSize = cardsRef.current.childNodes[0].clientWidth; - // Do not change the state unless the width changed to avoid - // infinite re-render loops. - if (wSize == cardWidth) { - return; - } - setCardWidth(wSize); + // Do not change the state unless the width changed to avoid + // infinite re-render loops. + if (wSize == cardWidth) { + return; + } + setCardWidth(wSize); - // Store the height in pixels of each panel. The first node is - // the CSS grid element, the first child is the panel. - // The childNodes are the DOM elements, not the React elements, - // but we make the assumption that they're in the same order - // as props.Cards in the DOM, and any re-arranging was done by - // using the CSS order property. - const heights = Array.from(cardsRef.current.childNodes.values()).map( - (node) => (node.firstChild.clientHeight) - ); - setPanelHeights(heights); - }); - const grid = { - display: 'grid', - gridTemplateColumns: '33% 33% 33%', - gridAutoFlow: 'row dense', - gridRowGap: '1em', - rowGap: '1em', - }; + // Store the height in pixels of each panel. The first node is + // the CSS grid element, the first child is the panel. + // The childNodes are the DOM elements, not the React elements, + // but we make the assumption that they're in the same order + // as props.Cards in the DOM, and any re-arranging was done by + // using the CSS order property. + const heights = Array.from(cardsRef.current.childNodes.values()).map( + (node) => (node.firstChild.clientHeight) + ); + setPanelHeights(heights); + }); + const grid = { + display: 'grid', + gridTemplateColumns: '33% 33% 33%', + gridAutoFlow: 'row dense', + gridRowGap: '1em', + rowGap: '1em', + }; - let orderedCards = []; - for (let i = 0; i < props.Cards.length; i++) { - orderedCards.push(props.Cards[i]); - if (!props.Cards[i].Order) { - orderedCards[i].Order = 1; - } + let orderedCards = []; + for (let i = 0; i < props.Cards.length; i++) { + orderedCards.push(props.Cards[i]); + if (!props.Cards[i].Order) { + orderedCards[i].Order = 1; } - orderedCards.sort((a, b) => (a.Order - b.Order)); + } + orderedCards.sort((a, b) => (a.Order - b.Order)); - let lastLargeCardIdx = 0; - for (let i = 0; i < orderedCards.length; i++) { - if (orderedCards[i].Width >= 2) { - lastLargeCardIdx = i; - } + let lastLargeCardIdx = 0; + for (let i = 0; i < orderedCards.length; i++) { + if (orderedCards[i].Width >= 2) { + lastLargeCardIdx = i; } + } - const cards = orderedCards.map((value, idx) => { - let cardID = 'card' + idx; + const cards = orderedCards.map((value, idx) => { + let cardID = 'card' + idx; - let pSize; - let style = {}; - if (value.Width) { - style.gridColumnEnd = 'span ' + value.Width; - if (value.Width == 1 || value.Width === 3) { - if (idx < lastLargeCardIdx) { - style.gridColumnStart = 1; - } - } else if (value.Width == 2) { - style.gridColumnStart = 2; - } - } - - if (cardWidth != 0) { - const pxHeight = panelHeights[idx]; - let spanHeight = 1; - const hSpan = 100; - if ((pxHeight % hSpan) === 0) { - spanHeight = pxHeight / hSpan; - } else { - spanHeight = Math.floor(pxHeight / hSpan) + 1; - } - style.gridRowEnd = 'span ' + spanHeight; - pSize = spanHeight * hSpan; - } - if (value.Order) { - style.order = value.Order; + let pSize; + let style = {}; + if (value.Width) { + style.gridColumnEnd = 'span ' + value.Width; + if (value.Width == 1 || value.Width === 3) { + if (idx < lastLargeCardIdx) { + style.gridColumnStart = 1; } + } else if (value.Width == 2) { + style.gridColumnStart = 2; + } + } - style.alignSelf = 'stretch'; - return ( - - {value.Content} - - ); - }); + if (cardWidth != 0) { + const pxHeight = panelHeights[idx]; + let spanHeight = 1; + const hSpan = 100; + if ((pxHeight % hSpan) === 0) { + spanHeight = pxHeight / hSpan; + } else { + spanHeight = Math.floor(pxHeight / hSpan) + 1; + } + style.gridRowEnd = 'span ' + spanHeight; + pSize = spanHeight * hSpan; + } + if (value.Order) { + style.order = value.Order; + } + style.alignSelf = 'stretch'; return ( -
{cards}
+ + {value.Content} + ); + }); + + return ( +
{cards}
+ ); } CSSGrid.propTypes = { - Cards: PropTypes.array, + Cards: PropTypes.array, }; export default CSSGrid; diff --git a/jsx/Card.js b/jsx/Card.js index d4cab9d2ec2..de2fb12f593 100644 --- a/jsx/Card.js +++ b/jsx/Card.js @@ -21,8 +21,13 @@ class Card extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); + this.state = {hasError: false}; } + static getDerivedStateFromError(error) { + console.error(error); + return {hasError: true}; + } /** * Delegate clicks on the card to the onClick handler * @@ -49,10 +54,10 @@ class Card extends Component { boxSizing: 'border-box', }; if (this.props.style) { - divStyling = {...divStyling, ...this.props.style}; + divStyling = {...divStyling, ...this.props.style}; } if (this.props.cardSize) { - divStyling.height = this.props.cardSize; + divStyling.height = this.props.cardSize; } return (
@@ -60,15 +65,19 @@ class Card extends Component { id={this.props.id} title={this.props.title} initCollapsed={this.props.initCollapsed} - style={{overflow: 'auto'}} - panelSize={this.props.cardSize} + style={{overflow: 'auto'}} + panelSize={this.props.cardSize} collapsing={this.props.collapsing} >
- {this.props.children} + {this.state.hasError ?
+ Something went wrong rendering this panel. + Please open a bug report. +
+ : this.props.children}
diff --git a/jsx/DataTable.js b/jsx/DataTable.js index be7f98c52a3..152f69251fe 100644 --- a/jsx/DataTable.js +++ b/jsx/DataTable.js @@ -21,8 +21,8 @@ class DataTable extends Component { rows: 20, }, sort: { - column: -1, - ascending: true, + column: -1, + ascending: true, }, }; @@ -120,17 +120,17 @@ class DataTable extends Component { // Map cell data to proper values if applicable. if (this.props.getMappedCell) { csvData = csvData - .map((row, i) => this.props.fields - .flatMap((field, j) => this.props.getMappedCell( + .map((row, i) => this.props.fields + .flatMap((field, j) => this.props.getMappedCell( field.label, row[j], row, this.props.fields.map( - (val) => val.label, + (val) => val.label, ), j - )) - ); + )) + ); } let csvworker = new Worker(loris.BaseURL + '/js/workers/savecsv.js'); @@ -175,7 +175,7 @@ class DataTable extends Component { let hasFilters = (filterValuesCount !== 0); if (hasFilters === false) { for (let i = 0; i < tableData.length; i++) { - filteredIndexes.push(i); + filteredIndexes.push(i); } return filteredIndexes; } @@ -206,7 +206,7 @@ class DataTable extends Component { if (headerCount === filterValuesCount && ((useKeyword === true && keywordMatch > 0) || (useKeyword === false && keywordMatch === 0))) { - filteredIndexes.push(i); + filteredIndexes.push(i); } } @@ -338,31 +338,31 @@ class DataTable extends Component { if (typeof filterData === 'string') { searchKey = filterData.toLowerCase(); switch (typeof data) { - case 'object': - // Handles the case where the data is an array (typeof 'object') - // and you want to search through it for - // the string you are filtering by - let searchArray = data.map((e) => e.toLowerCase()); - if (exactMatch) { - result = searchArray.includes(searchKey); - } else { - result = ( - searchArray.find( - (e) => (e.indexOf(searchKey) > -1) - ) - ) !== undefined; - } - break; - default: - searchString = data ? data.toString().toLowerCase() : ''; - if (exactMatch) { - result = (searchString === searchKey); - } else if (opposite) { - result = searchString !== searchKey; - } else { - result = (searchString.indexOf(searchKey) > -1); - } - break; + case 'object': + // Handles the case where the data is an array (typeof 'object') + // and you want to search through it for + // the string you are filtering by + let searchArray = data.map((e) => e.toLowerCase()); + if (exactMatch) { + result = searchArray.includes(searchKey); + } else { + result = ( + searchArray.find( + (e) => (e.indexOf(searchKey) > -1) + ) + ) !== undefined; + } + break; + default: + searchString = data ? data.toString().toLowerCase() : ''; + if (exactMatch) { + result = (searchString === searchKey); + } else if (opposite) { + result = searchString !== searchKey; + } else { + result = (searchString.indexOf(searchKey) > -1); + } + break; } } @@ -459,9 +459,9 @@ class DataTable extends Component { if (this.props.fields[i].freezeColumn === true) { headers.push( { - this.setSortColumn(i); - }}> + onClick={() => { + this.setSortColumn(i); + }}> {this.props.fields[i].label} ); @@ -485,59 +485,59 @@ class DataTable extends Component { // Format each cell for the data table. for (let i = currentPageRow; - (i < filteredCount) && (rows.length < rowsPerPage); - i++ + (i < filteredCount) && (rows.length < rowsPerPage); + i++ ) { - let rowIndex = index[i].RowIdx; - let rowData = this.props.data[rowIndex]; - let curRow = []; - - // Iterates through headers to populate row columns - // with corresponding data - for (let j = 0; j < this.props.fields.length; j += 1) { - if (this.props.fields[j].show === false) { - continue; - } - - let celldata = rowData[j]; - let cell = null; - - let row = {}; - this.props.fields - .forEach((field, k) => row[field.label] = rowData[k]); - - const headers = this.props.fields.map( - (val) => val.label - ); - - // Get custom cell formatting if available - if (this.props.getFormattedCell) { - cell = this.props.getFormattedCell( - this.props.fields[j].label, - celldata, - row, - headers, - j - ); - } else { - cell = {celldata}; - } - if (cell !== null) { - curRow.push(React.cloneElement(cell, {key: 'td_col_' + j})); - } else { - curRow.push(createFragment({celldata})); - } + let rowIndex = index[i].RowIdx; + let rowData = this.props.data[rowIndex]; + let curRow = []; + + // Iterates through headers to populate row columns + // with corresponding data + for (let j = 0; j < this.props.fields.length; j += 1) { + if (this.props.fields[j].show === false) { + continue; } - const rowIndexDisplay = index[i].Content; - rows.push( - - {this.props.hide.defaultColumn === true ? null : ( - {rowIndexDisplay} - )} - {curRow} - + let celldata = rowData[j]; + let cell = null; + + let row = {}; + this.props.fields + .forEach((field, k) => row[field.label] = rowData[k]); + + const headers = this.props.fields.map( + (val) => val.label ); + + // Get custom cell formatting if available + if (this.props.getFormattedCell) { + cell = this.props.getFormattedCell( + this.props.fields[j].label, + celldata, + row, + headers, + j + ); + } else { + cell = {celldata}; + } + if (cell !== null) { + curRow.push(React.cloneElement(cell, {key: 'td_col_' + j})); + } else { + curRow.push(createFragment({celldata})); + } + } + + const rowIndexDisplay = index[i].Content; + rows.push( + + {this.props.hide.defaultColumn === true ? null : ( + {rowIndexDisplay} + )} + {curRow} + + ); } let rowsPerPageDropdown = ( @@ -586,12 +586,12 @@ class DataTable extends Component { }}> {this.renderActions()} {this.props.hide.downloadCSV === true ? '' : ( - ) + ) } {headers} - {this.props.folder} + {this.props.folder} {rows} diff --git a/jsx/Filter.js b/jsx/Filter.js index 42f0122cd4f..e0feb0cde8e 100644 --- a/jsx/Filter.js +++ b/jsx/Filter.js @@ -1,14 +1,14 @@ import React, {useEffect} from 'react'; import PropTypes from 'prop-types'; import { - CheckboxElement, - DateElement, - FieldsetElement, - TimeElement, - FormElement, - NumericElement, - SelectElement, - TextboxElement, + CheckboxElement, + DateElement, + FieldsetElement, + TimeElement, + FormElement, + NumericElement, + SelectElement, + TextboxElement, } from 'jsx/Form'; import DateTimePartialElement from 'jsx/form/DateTimePartialElement'; @@ -49,7 +49,7 @@ function Filter(props) { const type = fields .find((field) => (field.filter||{}).name == name).filter.type; const exactMatch = (!(type === 'text' || type === 'date' - || type === 'datetime')); + || type === 'datetime' || type === 'multiselect')); if (value === null || value === '' || (value.constructor === Array && value.length === 0) || (type === 'checkbox' && value === false)) { @@ -70,47 +70,47 @@ function Filter(props) { if (filter && filter.hide !== true) { let element; switch (filter.type) { - case 'text': - element = ; - break; - case 'select': - element = ( - - ); - break; - case 'multiselect': - element = ( - - ); - break; - case 'numeric': - element = ; + break; + case 'select': + element = ( + ; - break; - case 'date': - element = ; - break; - case 'datetime': - element = ; - break; - case 'checkbox': - element = ; - break; - case 'time': - element = ; - break; - default: - element = ; + sortByValue={filter.sortByValue} + autoSelect={false} + /> + ); + break; + case 'multiselect': + element = ( + + ); + break; + case 'numeric': + element = ; + break; + case 'date': + element = ; + break; + case 'datetime': + element = ; + break; + case 'checkbox': + element = ; + break; + case 'time': + element = ; + break; + default: + element = ; } // The value prop has to default to false if the first two options diff --git a/jsx/FilterableDataTable.js b/jsx/FilterableDataTable.js index be3cb26adeb..086bab73b58 100644 --- a/jsx/FilterableDataTable.js +++ b/jsx/FilterableDataTable.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import Panel from 'jsx/Panel'; import DataTable from 'jsx/DataTable'; import Filter from 'jsx/Filter'; +import ProgressBar from 'jsx/ProgressBar'; /** * FilterableDataTable component. @@ -103,28 +104,28 @@ class FilterableDataTable extends Component { * @return {object} */ validFilters() { - let filters = {}; - this.props.fields.forEach((field) => { - if (!field.filter) { - return; - } - const filtername = field.filter.name; - const filterval = this.state.filters[filtername]; - if (!filterval) { - return; - } - - if (field.filter.type !== 'select') { - filters[filtername] = filterval; - return; - } - - if (!(filterval.value in field.filter.options)) { - return; - } + let filters = {}; + this.props.fields.forEach((field) => { + if (!field.filter) { + return; + } + const filtername = field.filter.name; + const filterval = this.state.filters[filtername]; + if (!filterval) { + return; + } + + if (field.filter.type !== 'select') { filters[filtername] = filterval; - }); - return filters; + return; + } + + if (!(filterval.value in field.filter.options)) { + return; + } + filters[filtername] = filterval; + }); + return filters; } /** @@ -149,7 +150,10 @@ class FilterableDataTable extends Component { /> ); - const dataTable = ( + const {progress} = this.props; + const dataTable = !isNaN(progress) && progress < 100 ? ( + + ) : ( void noMargins?: boolean placeholder?: string @@ -306,7 +305,6 @@ type fileElementProps = { disabled?: boolean required?: boolean allowMultiple?: boolean - hasError?: boolean errorMessage?: string onUserInput: (name: string, value: any) => void }; @@ -407,7 +405,6 @@ type dateElementProps = { dateFormat?: string disabled?: boolean required?: boolean - hasError?: boolean errorMessage?: string onUserInput: (name: string, value: any) => void }; @@ -595,7 +592,7 @@ type radioElementProps = { required?: boolean vertical?: boolean checked: boolean - errorMessage?: boolean + errorMessage?: string elementClass?: boolean onUserInput: (name: string, value: any) => void } diff --git a/jsx/Form.js b/jsx/Form.js index 9d089623914..9b9bf0bc6c6 100644 --- a/jsx/Form.js +++ b/jsx/Form.js @@ -7,6 +7,7 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; +import InputLabel from 'jsx/form/InputLabel'; /** * Form Component. @@ -329,20 +330,12 @@ export class SearchableDropdown extends Component { * @return {JSX} - React markup for the component */ render() { - let required = this.props.required ? 'required' : null; - let disabled = this.props.disabled ? 'disabled' : null; let sortByValue = this.props.sortByValue; let options = this.props.options; let strictMessage = 'Entry must be included in provided list of options.'; let errorMessage = null; - let requiredHTML = null; let elementClass = 'row form-group'; - // Add required asterix - if (required) { - requiredHTML = *; - } - // Add error message if (this.props.errorMessage) { errorMessage = {this.props.errorMessage}; @@ -391,10 +384,7 @@ export class SearchableDropdown extends Component { return (
- +
{optionList} @@ -451,7 +441,7 @@ SearchableDropdown.defaultProps = { disabled: false, required: false, sortByValue: true, - errorMessage: '', + errorMessage: null, placeHolder: '', onUserInput: function() { console.warn('onUserInput() callback is not set'); @@ -532,29 +522,20 @@ export class SelectElement extends Component { * @return {JSX} - React markup for the component */ render() { - let multiple = this.props.multiple ? 'multiple' : null; - let required = this.props.required ? 'required' : null; - let disabled = this.props.disabled ? 'disabled' : null; let sortByValue = this.props.sortByValue; let options = this.props.options; let disabledOptions = this.props.disabledOptions; let errorMessage = null; let emptyOptionHTML = null; - let requiredHTML = null; let elementClass = this.props.noMargins ? '' : 'row form-group'; - // Add required asterisk - if (required) { - requiredHTML = *; - } - // Add empty option if (this.props.emptyOption) { emptyOptionHTML = ; } // Add error message - if (this.props.hasError + if (this.props.errorMessage || (this.props.required && this.props.value === '') ) { errorMessage = {this.props.errorMessage}; @@ -603,37 +584,35 @@ export class SelectElement extends Component { } // Default to empty string for regular select and to empty array for 'multiple' select - const value = this.props.value || (multiple ? [] : ''); + const value = this.props.value || (this.props.multiple ? [] : ''); // Label prop needs to be provided to render label // (including empty label i.e. ) // and retain formatting. If label prop is not provided at all, the input // element will take up the whole row. - let label = null; let inputClass = this.props.noMargins ? '' : 'col-sm-12'; - if (this.props.label && this.props.label != '') { - label = ( - - ); + if (this.props.label) { inputClass = 'col-sm-9'; } return (
- {label} + {this.props.label && ( + + )}
@@ -1131,43 +1086,32 @@ export class TextboxElement extends Component { * @return {JSX} - React markup for the component */ render() { - let disabled = this.props.disabled ? 'disabled' : null; - let required = this.props.required ? 'required' : null; let errorMessage = null; - let requiredHTML = null; let elementClass = 'row form-group'; - // Add required asterix - if (required) { - requiredHTML = *; - } - // Add error message if (this.props.errorMessage) { errorMessage = {this.props.errorMessage}; elementClass = 'row form-group has-error'; } - // Label prop needs to be provided to render label // (including empty label i.e. ) // and retain formatting. If label prop is not provided at all, the input // element will take up the whole row. - let label = null; let inputClass = this.props.class; if (this.props.label || this.props.label == '') { - label = ( - - ); inputClass = 'col-sm-9'; } return (
- {label} + {(this.props.label || this.props.label == '') && ( + + )}
*; - } - // Add error message if (this.props.errorMessage) { errorMessage = {this.props.errorMessage}; elementClass = 'row form-group has-error'; } - // Label prop needs to be provided to render label // (including empty label i.e. ) // and retain formatting. If label prop is not provided at all, the input // element will take up the whole row. - let label = null; let inputClass = this.props.class; if (this.props.label || this.props.label == '') { - label = ( - - ); inputClass = 'col-sm-9'; } return (
- {label} + {(this.props.label || this.props.label == '') && ( + + )}
*; - } - // Add error message if (this.props.errorMessage) { errorMessage = {this.props.errorMessage}; elementClass = 'row form-group has-error'; } - let label = null; - if (this.props.label) { - label = ( - - ); - } const passwordDisplayType = this.state.active ? this.state.on.type : this.state.off.type; const passwordDisplayIcon = this.state.active ? this.state.on.icon : this.state.off.icon; + return (
- {label} + {this.props.label && ( + + )}
*; - } - // Add error message - if (this.props.hasError + if (this.props.errorMessage || (this.props.required && this.props.value === '') ) { errorMessage = {this.props.errorMessage}; @@ -1596,21 +1510,16 @@ export class DateElement extends Component { maxFullDate = maxYear + '-' + currentMonth; } - let labelHTML; - let classSz = 'col-sm-12'; - if (this.props.label) { - labelHTML = ; - classSz = 'col-sm-9'; - } + const wrapperClass = this.props.label ? 'col-sm-9' : 'col-sm-12'; return (
- {labelHTML} -
+ {this.props.label && ( + + )} +
{errorMessage}
@@ -1640,7 +1549,6 @@ DateElement.propTypes = { dateFormat: PropTypes.string, disabled: PropTypes.bool, required: PropTypes.bool, - hasError: PropTypes.bool, errorMessage: PropTypes.string, onUserInput: PropTypes.func, }; @@ -1655,8 +1563,7 @@ DateElement.defaultProps = { dateFormat: 'YMd', disabled: false, required: false, - hasError: false, - errorMessage: 'The field is required!', + errorMessage: null, onUserInput: function() { console.warn('onUserInput() callback is not set'); }, @@ -1692,31 +1599,16 @@ export class TimeElement extends Component { * @return {JSX} - React markup for the component */ render() { - let disabled = this.props.disabled ? 'disabled' : null; - let required = this.props.required ? 'required' : null; - let requiredHTML = null; - let label; - let classSz; - - // Add required asterix - if (required) { - requiredHTML = *; - } - if (this.props.label) { - label = ; - classSz = 'col-sm-9'; - } else { - classSz = 'col-sm-12'; - } - + const wrapperClass = this.props.label ? 'col-sm-9' : 'col-sm-12'; return (
- {label} -
+ {this.props.label && ( + + )} +
*; - } - if (this.props.label) { - label = ; - classSz = 'col-sm-9'; - } else { - classSz = 'col-sm-12'; - } - + const wrapperClass = this.props.label ? 'col-sm-9' : 'col-sm-12'; return (
- {label} -
+ {this.props.label && ( + + )} +
* : null; let errorMessage = null; let elementClass = 'row form-group'; + const wrapperClass = this.props.label ? 'col-sm-9' : 'col-sm-12'; // Add error message if (this.props.errorMessage) { @@ -1894,22 +1770,15 @@ export class NumericElement extends Component { elementClass = 'row form-group has-error'; } - let labelHTML; - let classSz = 'col-sm-12'; - if (this.props.label) { - labelHTML = ; - classSz = 'col-sm-9'; - } - return (
- {labelHTML} -
+ {this.props.label && ( + + )} +
{errorMessage} @@ -1992,39 +1861,31 @@ export class FileElement extends Component { * @return {JSX} - React markup for the component */ render() { - const required = this.props.required ? 'required' : null; - let fileName = undefined; if (this.props.value) { switch (typeof this.props.value) { - case 'string': - fileName = this.props.value; - break; - - case 'object': - if (this.props.value instanceof FileList) { - const files = this.props.value; - fileName = Array.from(files).map((file) => file.name).join(', '); - } else { - fileName = this.props.value.name; - } - break; - - default: - break; + case 'string': + fileName = this.props.value; + break; + + case 'object': + if (this.props.value instanceof FileList) { + const files = this.props.value; + fileName = Array.from(files).map((file) => file.name).join(', '); + } else { + fileName = this.props.value.name; + } + break; + + default: + break; } } - let requiredHTML = null; let errorMessage = ''; let elementClass = 'row form-group'; - // Add required asterix - if (required) { - requiredHTML = *; - } - const truncateEllipsis = { display: 'table', tableLayout: 'fixed', @@ -2039,7 +1900,7 @@ export class FileElement extends Component { }; // Add error message - if (this.props.hasError) { + if (this.props.errorMessage) { errorMessage = this.props.errorMessage; elementClass = 'row form-group has-error'; } @@ -2057,9 +1918,7 @@ export class FileElement extends Component { truncateEllipsis.paddingTop = '7px'; return (
- +
{fileName} @@ -2069,25 +1928,16 @@ export class FileElement extends Component { ); } - let labelHTML; - let classSz; - if (this.props.label) { - labelHTML = ; - classSz = 'col-sm-9'; - } else { - classSz = 'col-sm-12'; - } - + const wrapperClass = this.props.label ? 'col-sm-9' : 'col-sm-12'; return (
- {labelHTML} -
+ {this.props.label && ( + + )} +
+ className="form-control file-caption kv-fileinput-caption">
{fileName}
@@ -2101,7 +1951,7 @@ export class FileElement extends Component { className="fileUpload" name={this.props.name} onChange={this.handleChange} - required={required} + required={this.props.required} multiple={this.props.allowMultiple} />
@@ -2125,7 +1975,6 @@ FileElement.propTypes = { disabled: PropTypes.bool, required: PropTypes.bool, allowMultiple: PropTypes.bool, - hasError: PropTypes.bool, errorMessage: PropTypes.string, onUserInput: PropTypes.func, }; @@ -2138,8 +1987,7 @@ FileElement.defaultProps = { disabled: false, required: false, allowMultiple: false, - hasError: false, - errorMessage: 'The field is required!', + errorMessage: null, onUserInput: function() { console.warn('onUserInput() callback is not set'); }, @@ -2175,17 +2023,11 @@ export class StaticElement extends Component { * @return {JSX} - React markup for the component */ render() { - let label = null; - if (this.props.label) { - label = ( - - ); - } return (
- {label} + {this.props.label && ( + + )}
{this.props.text} @@ -2277,9 +2119,7 @@ export class LinkElement extends Component { render() { return (
- +

{this.props.text} @@ -2333,8 +2173,6 @@ export class CheckboxElement extends React.Component { * @return {JSX} - React markup for the component */ render() { - let disabled = this.props.disabled ? 'disabled' : null; - let required = this.props.required ? 'required' : null; let errorMessage = null; let requiredHTML = null; let elementClass = this.props.class + ' ' + this.props.offset; @@ -2343,7 +2181,7 @@ export class CheckboxElement extends React.Component { : {paddingRight: '5px', display: 'inline-block'}; // Add required asterix - if (required) { + if (this.props.required) { requiredHTML = *; } @@ -2355,7 +2193,7 @@ export class CheckboxElement extends React.Component { return (

-
+
+ style={styleColumn}>
@@ -2699,7 +2535,7 @@ export class RadioElement extends React.Component { layout.push(
+ style={styleRow}> {content}
); @@ -2760,7 +2596,7 @@ RadioElement.defaultProps = { disabled: false, required: false, vertical: false, - errorMessage: '', + errorMessage: null, elementClass: 'row form-group', onUserInput: function() { console.warn('onUserInput() callback is not set'); @@ -2809,10 +2645,8 @@ export class SliderElement extends React.Component { let errorMessage = null; let requiredHTML = null; let elementClass = this.props.elementClass; - let disabled = this.props.disabled ? 'disabled' : null; - let required = this.props.required ? 'required' : null; // Add required asterix - if (required) { + if (this.props.required) { requiredHTML = *; } // Add error message @@ -2824,7 +2658,7 @@ export class SliderElement extends React.Component { return (
- ); -} - -Loader.propTypes = {size: PropTypes.string}; -Loader.defaultProps = {size: '120'}; - -export default Loader; diff --git a/jsx/Loader.tsx b/jsx/Loader.tsx new file mode 100644 index 00000000000..14eb6c1e6bf --- /dev/null +++ b/jsx/Loader.tsx @@ -0,0 +1,21 @@ +interface LoaderProps { + size?: number; +} + +/** + * Loader component renders a spinner wheel of a specified size. + * + * @param {LoaderProps} props - The properties for the Loader component + * @returns {JSX.Element} A div representing the loading spinner + */ +const Loader = ({size = 120}: LoaderProps) => { + const loaderStyle = { + width: size, + height: size, + borderWidth: size/15, + }; + + return
; +}; + +export default Loader; diff --git a/jsx/Modal.d.ts b/jsx/Modal.d.ts deleted file mode 100644 index 6cb1ba08e73..00000000000 --- a/jsx/Modal.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {ReactNode} from 'react'; - -type ModalProps = { - title?: string - onSubmit: () => Promise, - onClose: () => void, - show: boolean, - throwWarning?: boolean, - children: React.ReactNode, - width?: string - -} - -/** - * The Modal class. See Modal.js - */ -class Modal { - props: ModalProps - state: any - context: object - refs: {[key: string]: ReactInstance} - - /** - * Construct a new modal - */ - constructor(props: ModalProps) - - /** - * React lifecycle method - * - * @returns {ReactNode} - */ - render(): ReactNode - - /** - * React lifecycle method - * - * @param {object} newstate - the state to overwrite - */ - setState(newstate: object): void - - /** - * React lifecycle method - */ - forceUpdate(): void -} - -export default Modal; diff --git a/jsx/Modal.js b/jsx/Modal.js deleted file mode 100644 index 5b7a42ca6c6..00000000000 --- a/jsx/Modal.js +++ /dev/null @@ -1,185 +0,0 @@ -/** - * This file contains the React Component for a Modal Window. - * - * @author Henri Rabalais - * @version 1.1.0 - */ -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -import swal from 'sweetalert2'; -import {ButtonElement} from 'jsx/Form'; - -/** - * Modal Component. - * React wrapper for a Modal Window. Allows to dynamically toggle a Modal - * window. - * - * ================================================ - * Usage: - * - Wrap the contents to be displayed by the Modal Window by the - * Modal Component. - * - Use the 'title' prop to set a title for the Modal Component. - * - Use the 'onSubmit' prop to set a submission *promise* object for the - * Modal's contents. - * - Use the 'onClose' prop to set a function that triggers upon Modal closure. - * - Use the 'throwWarning' prop to throw a warning upon closure of the - * Modal Window. - * ================================================= - * - */ -class Modal extends Component { - /** - * @constructor - */ - constructor() { - super(); - this.handleClose = this.handleClose.bind(this); - } - - /** - * Display a warning message on close - */ - handleClose() { - if (this.props.throwWarning) { - swal.fire({ - title: 'Are You Sure?', - text: 'Leaving the form will result in the loss of any information ' + - 'entered.', - type: 'warning', - showCancelButton: true, - confirmButtonText: 'Proceed', - cancelButtonText: 'Cancel', - }).then((result) => result.value && this.props.onClose()); - } else { - this.props.onClose(); - } - } - - /** - * Renders the React component. - * - * @return {JSX} - React markup for the component - */ - render() { - const {show, children, onSubmit, title, width} = this.props; - - const headerStyle = { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - height: '40px', - borderTopRightRadius: '10', - fontSize: 24, - padding: 35, - borderBottom: '1px solid #DDDDDD', - }; - - const glyphStyle = { - marginLeft: 'auto', - cursor: 'pointer', - }; - - const bodyStyle = { - padding: 15, - maxHeight: '75vh', - overflowY: 'scroll', - }; - - const modalContainer = { - display: 'block', - position: 'fixed', - zIndex: 9999, - paddingTop: '65px', - left: 0, - top: 0, - width: '100%', - height: '100%', - overflow: 'auto', - backgroundColor: 'rgba(0,0,0,0.7)', - visibility: show ? 'visible' : 'hidden', - }; - - const modalContent = { - opacity: show ? 1 : 0, - top: show ? 0 : '-300px', - position: 'relative', - backgroundColor: '#fefefe', - borderRadius: '7px', - margin: 'auto', - padding: 0, - border: '1px solid #888', - width: width || '700px', - boxShadow: '0 4px 8px 0 rbga(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19)', - transition: 'top 0.4s, opacity 0.4s', - }; - - const renderChildren = () => show && children; - - const footerStyle = { - borderTop: '1px solid #DDDDDD', - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - height: '40px', - padding: '35px 35px 20px 35px', - }; - - const submitStyle = { - marginLeft: 'auto', - marginRight: '20px', - }; - - const submitButton = () => { - if (onSubmit) { - const submit = () => onSubmit().then(() => this.props.onClose()) - .catch(() => {}); - return ( -
- -
- ); - } - }; - - return ( -
-
e.stopPropagation()} - > -
- {title} - - × - -
-
- {renderChildren()} -
-
- {submitButton()} -
-
-
- ); - } -} - -Modal.propTypes = { - title: PropTypes.string, - onSubmit: PropTypes.func, - onClose: PropTypes.func.isRequired, - show: PropTypes.bool.isRequired, - throwWarning: PropTypes.bool, - children: PropTypes.node, - width: PropTypes.string, -}; - -Modal.defaultProps = { - throwWarning: false, -}; - -export default Modal; diff --git a/jsx/Modal.tsx b/jsx/Modal.tsx new file mode 100644 index 00000000000..ca7f683d711 --- /dev/null +++ b/jsx/Modal.tsx @@ -0,0 +1,216 @@ +import {useState, PropsWithChildren, CSSProperties} from 'react'; +import Swal from 'sweetalert2'; +import Loader from './Loader'; +import { + ButtonElement, +} from 'jsx/Form'; + +type ModalProps = PropsWithChildren<{ + throwWarning?: boolean; + show: boolean; + onClose: () => void; + onSubmit?: () => Promise; + onSuccess?: (data: any) => void; + title?: string; +}>; + +/** + * Modal Component + * + * A React functional component that renders a modal dialog with optional + * form submission and loading indicators. Supports asynchronous form submission + * with loading and success feedback. + * + * @param {ModalProps} props - Properties for the modal component + * @returns {JSX.Element} - A modal dialog box w/ optional submit functionality + */ +const Modal = ({ + throwWarning = false, + show = false, + onClose, + onSubmit, + onSuccess, + title, + children, +}: ModalProps) => { + const [loading, setLoading] = useState(false); // Tracks loading during submit + const [success, setSuccess] = useState(false); // Tracks success after submit + + /** + * Handles modal close event. Shows a confirmation if `throwWarning` is true. + */ + const handleClose = () => { + if (throwWarning) { // Display warning if enabled + Swal.fire({ + title: 'Are You Sure?', + text: 'Leaving the form will result in the loss of any information ' + + 'entered.', + type: 'warning', + showCancelButton: true, + confirmButtonText: 'Proceed', + cancelButtonText: 'Cancel', + }).then((result) => result.value && onClose()); + } else { + onClose(); // Close immediately if no warning + } + }; + + /** + * Manages form submission with loading and success states, calling + * `onSubmit` and handling modal state based on success or failure. + */ + const handleSubmit = async () => { + if (!onSubmit) return; // Ensure onSubmit exists + + setLoading(true); // Show loader + + try { + const data = await onSubmit(); + setLoading(false); + setSuccess(true); // Show success + + await new Promise((resolve) => setTimeout(resolve, 2000)); // Close delay + + setSuccess(false); // Reset success state + onClose(); // Close modal + onSuccess?.(data); // call onSuccess if defined + } catch { + setLoading(false); + } + }; + + /** + * Renders submit button if `onSubmit` is provided and no loading or success. + * + * @returns {JSX.Element | undefined} - The submit button if conditions are met + */ + const submitButton = () => { + if (onSubmit && !(loading || success)) { // Show button if conditions met + return ( +
+ +
+ ); + } + }; + + const headerStyle: CSSProperties = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + height: '40px', + borderTopRightRadius: '10', + fontSize: 24, + padding: 35, + borderBottom: '1px solid #DDDDDD', + }; + + const glyphStyle: CSSProperties = { + marginLeft: 'auto', + cursor: 'pointer', + }; + + const bodyStyle: CSSProperties = { + padding: success ? 0 : '15px 15px', + maxHeight: success ? 0 : '75vh', + overflow: 'scroll', + opacity: success ? 0 : 1, + transition: '1s ease, opacity 0.3s', + }; + + const modalContainer: CSSProperties = { + display: 'block', + position: 'fixed', + zIndex: 9999, + paddingTop: '100px', + paddingBottom: '100px', + left: 0, + top: 0, + width: '100%', + height: '100%', + overflow: 'auto', + backgroundColor: 'rgba(0,0,0,0.7)', + visibility: show ? 'visible' : 'hidden', + }; + + const modalContent: CSSProperties = { + opacity: show ? 1 : 0, + top: show ? 0 : '-300px', + position: 'relative', + backgroundColor: '#fefefe', + borderRadius: '7px', + margin: 'auto', + padding: 0, + border: '1px solid #888', + width: '700px', + boxShadow: '0 4px 8px 0 rbga(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19)', + transition: '0.4s ease', + }; + + const footerStyle: CSSProperties = { + borderTop: '1px solid #DDDDDD', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + height: '40px', + padding: '35px', + backgroundColor: success ? '#e0ffec' : undefined, + }; + + const submitStyle: CSSProperties = { + marginLeft: 'auto', + marginRight: '20px', + }; + + const processStyle: CSSProperties = { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-evenly', + margin: '0px auto', + width: '90px', + }; + + /** + * Loader element displayed during form submission. + */ + const loader = loading && ( +
+ +
Saving
+
+ ); + + /** + * Success display element shown after successful form submission. + */ + const successDisplay = success && ( +
+ +
Success!
+
+ ); + + return ( +
+
e.stopPropagation()}> +
+ {title} + × +
+
+
{show && children}
+
+ {loader} + {successDisplay} + {submitButton()} +
+
+
+
+ ); +}; + +export default Modal; diff --git a/jsx/MultiSelectDropdown.js b/jsx/MultiSelectDropdown.js index 2658d60de95..06f1e4f1790 100644 --- a/jsx/MultiSelectDropdown.js +++ b/jsx/MultiSelectDropdown.js @@ -310,26 +310,26 @@ export class SelectDropdown extends Component { } } const overlay = this.state.open ? ( -
- ) : null; +
+ ) : null; return ( <>
    + role='menu'> {views}
@@ -81,9 +81,9 @@ const Panel = (props) => { // Add panel header, if title is set const panelHeading = props.title || props.views ? (
+ data-parent={props.parentId + ? `#${props.parentId}` + : null}>

{props.views && props.views[activeView]['title'] ? props.views[activeView]['title'] @@ -94,10 +94,10 @@ const Panel = (props) => { ? + onClick={toggleCollapsed} + data-toggle='collapse' + data-target={`#${props.id}`} + style={{cursor: 'pointer'}}/> : null}

) : ''; @@ -109,16 +109,16 @@ const Panel = (props) => { */ return (
+ style={{height: props.panelSize}}> {panelHeading}
+ className={props.collapsed ? + 'panel-collapse collapse' : + 'panel-collapse collapse in'} + role='tabpanel' + style={{height: 'calc(100% - 3em)'}}>
+ style={{...props.style, height: props.height}}> {content.length > 0 ? content : props.children}
diff --git a/jsx/StaticDataTable.js b/jsx/StaticDataTable.js index 50f3dae0448..fb64bba1238 100644 --- a/jsx/StaticDataTable.js +++ b/jsx/StaticDataTable.js @@ -233,8 +233,8 @@ class StaticDataTable extends Component { let useKeyword = false; let filterMatchCount = 0; let filterValuesCount = (this.props.Filter ? - Object.keys(this.props.Filter).length : - 0 + Object.keys(this.props.Filter).length : + 0 ); let tableData = this.props.Data; let headersData = this.props.Headers; @@ -450,7 +450,7 @@ class StaticDataTable extends Component { if (this.props.Headers[i] === this.props.freezeColumn) { headers.push( + onClick={this.setSortColumn(i).bind(this)}> {this.props.Headers[i]} ); @@ -482,8 +482,8 @@ class StaticDataTable extends Component { // Push rows to data table for (let i = 0; - (i < this.props.Data.length) && (rows.length < rowsPerPage); - i++ + (i < this.props.Data.length) && (rows.length < rowsPerPage); + i++ ) { curRow = []; @@ -549,7 +549,7 @@ class StaticDataTable extends Component { if (matchesFound > currentPageRow) { const rowIndex = index[i].Content; const rowCell = this.state.Hide.defaultColumn !== true ? - {rowIndex} : null; + {rowIndex} : null; rows.push( diff --git a/jsx/Tabs.js b/jsx/Tabs.js index 0ee5d730f47..7d551878e38 100644 --- a/jsx/Tabs.js +++ b/jsx/Tabs.js @@ -103,10 +103,10 @@ class Tabs extends Component { key={tab.id} > {tab.label} @@ -253,10 +253,10 @@ class VerticalTabs extends Component { key={tab.id} > {tab.label} @@ -282,7 +282,7 @@ class VerticalTabs extends Component { key: key, }); } - }.bind(this)); + }.bind(this)); return tabPanes; } @@ -304,9 +304,9 @@ class VerticalTabs extends Component {
    + className="nav nav-pills nav-stacked" + role="tablist" + style={tabStyle}> {tabs}
@@ -336,11 +336,11 @@ VerticalTabs.defaultProps = { * Used to wrap content for every tab. */ class TabPane extends Component { - /** - * React lifecycle method - * - * @return {object} - */ + /** + * React lifecycle method + * + * @return {object} + */ render() { let classList = 'tab-pane'; let title; diff --git a/jsx/form/DateTimePartialElement.tsx b/jsx/form/DateTimePartialElement.tsx index b103efb0b6d..0dc33802e0a 100644 --- a/jsx/form/DateTimePartialElement.tsx +++ b/jsx/form/DateTimePartialElement.tsx @@ -1,4 +1,5 @@ -import {ChangeEvent, ReactNode, useState} from 'react'; +import React, {ChangeEvent, ReactNode, useState} from 'react'; +import InputLabel from 'jsx/form/InputLabel'; const format = 'YYYY-MM-DD hh:mm:ss'; @@ -91,7 +92,7 @@ function formatDatetime(oldDateTime: string, newDateTime: string) { return newDateTime; } -interface MaskProps { +type MaskProps = { value: string; children: ReactNode; } @@ -102,34 +103,32 @@ interface MaskProps { * @param props The props of the component * @returns The corresponding React element */ -function Mask(props: MaskProps) { - // '\u00A0' is a non-breakable space. - return ( -
- {props.children} -
= ({value, children}) => ( +
+ {children} +
+
-
- {'\u00A0'.repeat(props.value.length)} - {format.slice(props.value.length)} -
+ {/* '\u00A0' is a non-breakable space */} + {'\u00A0'.repeat(value.length)} + {format.slice(value.length)}
- ); -} +
+); -interface DateTimePartialElementProps { +type DateTimePartialElementProps = { name: string; label: string; value?: string; @@ -137,7 +136,6 @@ interface DateTimePartialElementProps { dateFormat: string; required?: boolean; disabled?: boolean; - hasError?: boolean; errorMessage?: string; onUserInput: (name: string, value: string) => void; } @@ -150,7 +148,9 @@ interface DateTimePartialElementProps { * @param props The props of the component * @returns The corresponding React element */ -function DateTimePartialElement(props: DateTimePartialElementProps) { +const DateTimePartialElement: React.FC = ( + props, +) => { const onUserInput = props.onUserInput !== undefined ? props.onUserInput : () => console.warn('onUserInput() callback is not set'); @@ -177,40 +177,24 @@ function DateTimePartialElement(props: DateTimePartialElementProps) { ); } - const required = props.required ?? false; - const disabled = props.disabled ?? false; let errorMessage = null; let elementClass = 'row form-group'; if (props.required && value == '') { errorMessage = This field is required; elementClass += ' has-error'; - } else if (props.hasError) { + } else if (props.errorMessage) { errorMessage = {props.errorMessage}; elementClass += ' has-error'; } - let labelHTML; - let classSz = 'col-sm-12'; - if (props.label) { - classSz = 'col-sm-9'; - labelHTML = ( - - ); - } - + const wrapperClass = props.label ? 'col-sm-9' : 'col-sm-12'; return (
- {labelHTML} -
+ {props.label && ( + + )} +
@@ -227,6 +211,6 @@ function DateTimePartialElement(props: DateTimePartialElementProps) {
); -} +}; export default DateTimePartialElement; diff --git a/jsx/form/InputLabel.tsx b/jsx/form/InputLabel.tsx new file mode 100644 index 00000000000..4e9ecc08313 --- /dev/null +++ b/jsx/form/InputLabel.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +type InputLabelProps = { + // The label to be displayed to the user + label: string; + // Whether or not the input is required, `false` by default + required?: boolean; +}; + +/** + * Input label React component + * + * @param props The props of the component + * @returns The corresponding React element + */ +const InputLabel: React.FC = ({label, required}) => ( + +); + +export default InputLabel; diff --git a/modules/acknowledgements/jsx/acknowledgementsIndex.js b/modules/acknowledgements/jsx/acknowledgementsIndex.js index 201b828405b..563bdc77aea 100644 --- a/modules/acknowledgements/jsx/acknowledgementsIndex.js +++ b/modules/acknowledgements/jsx/acknowledgementsIndex.js @@ -8,11 +8,11 @@ import Panel from 'Panel'; import Loader from 'Loader'; import FilterableDataTable from 'FilterableDataTable'; import { - SelectElement, - FormElement, - TextboxElement, - DateElement, - ButtonElement, + SelectElement, + FormElement, + TextboxElement, + DateElement, + ButtonElement, } from 'jsx/Form'; /** @@ -228,16 +228,16 @@ class AcknowledgementsIndex extends Component { let result = {cell}; switch (column) { - case 'Affiliations': - result = {this.parseMultiple(cell, 'affiliationsOptions')}; - break; - case 'Degrees': - result = {this.parseMultiple(cell, 'degreesOptions')}; - break; + case 'Affiliations': + result = {this.parseMultiple(cell, 'affiliationsOptions')}; + break; + case 'Degrees': + result = {this.parseMultiple(cell, 'degreesOptions')}; + break; - case 'Roles': - result = {this.parseMultiple(cell, 'rolesOptions')}; - break; + case 'Roles': + result = {this.parseMultiple(cell, 'rolesOptions')}; + break; } return result; } diff --git a/modules/acknowledgements/test/AcknowledgementsTest.php b/modules/acknowledgements/test/AcknowledgementsTest.php index 363c2129e01..1fabaa16b6c 100644 --- a/modules/acknowledgements/test/AcknowledgementsTest.php +++ b/modules/acknowledgements/test/AcknowledgementsTest.php @@ -1,4 +1,5 @@ -_candidate->getCandID()) { + if ($visitinfo['CandID'] !== $this->_candidate->getCandID()->__toString()) { return new \LORIS\Http\Response\JSON\BadRequest( 'CandID does not match this candidate' ); @@ -227,7 +228,7 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator $centerid = array_search($visitinfo['Site'], \Utility::getSiteList()); if ($centerid === false - || !in_array(new \CenterID("$centerid"), $user->getCenterIDs()) + || !in_array(\CenterID::singleton($centerid), $user->getCenterIDs()) ) { return new \LORIS\Http\Response\JSON\Forbidden( "You can't create or modify candidates visit for the site " . @@ -237,7 +238,7 @@ class Visit extends Endpoint implements \LORIS\Middleware\ETagCalculator // \Utility::getSiteList key was a string. Now that the // validation is done, convert to an object. - $centerid = new \CenterID("$centerid"); + $centerid = \CenterID::singleton($centerid); $cohortid = array_search( $visitinfo['Battery'], diff --git a/modules/api/php/endpoints/candidate/visit/visit_0_0_4_dev.class.inc b/modules/api/php/endpoints/candidate/visit/visit_0_0_4_dev.class.inc index c770afad0da..400fa5401da 100644 --- a/modules/api/php/endpoints/candidate/visit/visit_0_0_4_dev.class.inc +++ b/modules/api/php/endpoints/candidate/visit/visit_0_0_4_dev.class.inc @@ -1,4 +1,5 @@ getCenterIDs() ) ) { @@ -254,7 +255,7 @@ class Visit_0_0_4_Dev extends Endpoint implements \LORIS\Middleware\ETagCalculat } // Now that the validation is done, convert to an object. - $this->_centerID = new \CenterID(strval($centerID)); + $this->_centerID = \CenterID::singleton($centerID); return null; } diff --git a/modules/api/php/endpoints/candidates.class.inc b/modules/api/php/endpoints/candidates.class.inc index a3d2d469880..6c71445a496 100644 --- a/modules/api/php/endpoints/candidates.class.inc +++ b/modules/api/php/endpoints/candidates.class.inc @@ -1,4 +1,5 @@ getFirstRow()["CandID"])); } try { @@ -255,7 +257,7 @@ class Candidates extends Endpoint implements \LORIS\Middleware\ETagCalculator try { $candid = \Candidate::createNew( - new \CenterID("$centerid"), + \CenterID::singleton($centerid), $data['Candidate']['DoB'] ?? null, $data['Candidate']['EDC'] ?? null, $sex, diff --git a/modules/api/php/endpoints/login.class.inc b/modules/api/php/endpoints/login.class.inc index 2e42d8ce995..b91a0645466 100644 --- a/modules/api/php/endpoints/login.class.inc +++ b/modules/api/php/endpoints/login.class.inc @@ -1,4 +1,5 @@ _project = $project; } @@ -145,7 +149,8 @@ class Instruments extends Endpoint implements \LORIS\Middleware\ETagCalculator $array = (new \LORIS\api\Views\Project\Instruments( $this->_project, - iterator_to_array($instruments) + iterator_to_array($instruments), + $this->apiversion ))->toArray(); $this->_cache = new \LORIS\Http\Response\JsonResponse($array); diff --git a/modules/api/php/endpoints/project/project.class.inc b/modules/api/php/endpoints/project/project.class.inc index cc6c53c5adf..105483a0ac7 100644 --- a/modules/api/php/endpoints/project/project.class.inc +++ b/modules/api/php/endpoints/project/project.class.inc @@ -1,4 +1,5 @@ getAttribute("LORIS-API-Version") ?? "unknown"; + // Delegate to sub-endpoints $subendpoint = array_shift($pathparts); switch ($subendpoint) { @@ -109,7 +112,7 @@ class Project extends Endpoint implements \LORIS\Middleware\ETagCalculator $handler = new Images($this->_project); break; case 'instruments': - $handler = new Instruments($this->_project); + $handler = new Instruments($this->_project, $apiversion); break; case 'visits': $handler = new Visits($this->_project); diff --git a/modules/api/php/endpoints/project/recordings.class.inc b/modules/api/php/endpoints/project/recordings.class.inc index 597d87e224b..c128ba6de57 100644 --- a/modules/api/php/endpoints/project/recordings.class.inc +++ b/modules/api/php/endpoints/project/recordings.class.inc @@ -1,4 +1,5 @@ _candid = $row['CandID'] ?? null; $this->_projectname = $row['ProjectName'] ?? null; if ($row['ProjectID'] !== null) { - $this->_projectid = new \ProjectID($row['ProjectID']); + $this->_projectid = \ProjectID::singleton(intval($row['ProjectID'])); } $this->_pscid = $row['PSCID'] ?? null; $this->_sitename = $row['SiteName'] ?? null; $this->_edc = $row['EDC'] ?? null; $this->_dob = $row['DoB'] ?? null; $this->_sex = $row['Sex'] ?? null; - $this->_centerid = new \CenterID($row['CenterID']); + $this->_centerid = \CenterID::singleton(intval($row['CenterID'])); } /** diff --git a/modules/api/php/models/projectimagesrow.class.inc b/modules/api/php/models/projectimagesrow.class.inc index 10b60190895..5b97d9c51f8 100644 --- a/modules/api/php/models/projectimagesrow.class.inc +++ b/modules/api/php/models/projectimagesrow.class.inc @@ -1,4 +1,5 @@ _entitytype = $row['Entity_type'] ?? null; $this->_visitlabel = $row['Visit'] ?? null; $this->_visitdate = $row['Visit_date'] ?? null; - $this->_centerid = new \CenterID($row['CenterID']); + $this->_centerid = \CenterID::singleton(intval($row['CenterID'])); $this->_centername = $row['Site'] ?? null; $this->_filename = $row['File'] ?? null; $this->_inserttime = $row['InsertTime'] ?? null; diff --git a/modules/api/php/models/projectinstrumentsrow.class.inc b/modules/api/php/models/projectinstrumentsrow.class.inc index e93c94b9b27..76be7c809f7 100644 --- a/modules/api/php/models/projectinstrumentsrow.class.inc +++ b/modules/api/php/models/projectinstrumentsrow.class.inc @@ -1,4 +1,5 @@ _shortname = $row['shortname'] ?? null; + $this->_ddeVisits = \NDB_BVL_Battery::getDDEVisitsForInstrument( + $this->_shortname + ); - $this->_shortname = $shortname; $this->_fullname = $row['fullname'] ?? null; $this->_subgroupname = $row['subgroupname'] ?? null; - $this->_isDDE = in_array( - $shortname, - $ddeinstruments, - true - ); + $this->_isDDE = count($this->_ddeVisits) > 0; } /** @@ -87,6 +89,16 @@ class ProjectInstrumentsRow implements \LORIS\Data\DataInstance return $this->_isDDE; } + /** + * Accessor for ddeVisits. + * + * @return array + */ + public function getddeVisits(): array + { + return $this->_ddeVisits; + } + /** * Implements \LORIS\Data\DataInstance interface for this row. * @@ -94,11 +106,17 @@ class ProjectInstrumentsRow implements \LORIS\Data\DataInstance */ public function jsonSerialize() : array { - return [ + $obj = [ 'shortname' => $this->_shortname, 'fullname' => $this->_fullname, 'subgroup' => $this->_subgroupname, 'ddeenable' => $this->_isDDE, + 'ddevisits' => $this->_ddeVisits, ]; + if ($this->apiversion != 'v0.0.3') { + // api version >= v0.0.4 + $obj['ddevisits'] = $this->_ddeVisits; + } + return $obj; } } diff --git a/modules/api/php/models/projectrecordingsrow.class.inc b/modules/api/php/models/projectrecordingsrow.class.inc index 28d275f6798..e5d9884efd6 100644 --- a/modules/api/php/models/projectrecordingsrow.class.inc +++ b/modules/api/php/models/projectrecordingsrow.class.inc @@ -1,4 +1,5 @@ _id); + return \CenterID::singleton($this->_id); } } diff --git a/modules/api/php/module.class.inc b/modules/api/php/module.class.inc index 54a594e91fb..b4eacea8524 100644 --- a/modules/api/php/module.class.inc +++ b/modules/api/php/module.class.inc @@ -1,4 +1,5 @@ -_project = $project; $this->_instruments = $instruments; } @@ -69,6 +74,10 @@ class Instruments 'Subgroup' => $instrument->getSubgroupname(), 'DoubleDataEntryEnabled' => $instrument->isDDE(), ]; + if ($this->apiversion != 'v0.0.3') { + // >= v0.0.4 + $item['DoubleDataEntryVisits'] = $instrument->getddeVisits(); + } if (!is_null($shortname)) { $instruments[$shortname] = $item; diff --git a/modules/api/php/views/projects.class.inc b/modules/api/php/views/projects.class.inc index f348865b459..f26f53c1f49 100644 --- a/modules/api/php/views/projects.class.inc +++ b/modules/api/php/views/projects.class.inc @@ -1,4 +1,5 @@ If the duplicate entry is inactive, you will be given the option to active it. -
+

); @@ -68,7 +68,6 @@ class BatteryManagerForm extends Component { required={true} value={test.testName} errorMessage={errors.testName} - hasError={errors.testName} /> - + diff --git a/modules/battery_manager/jsx/batteryManagerIndex.js b/modules/battery_manager/jsx/batteryManagerIndex.js index 794ba742f61..40b3be79693 100644 --- a/modules/battery_manager/jsx/batteryManagerIndex.js +++ b/modules/battery_manager/jsx/batteryManagerIndex.js @@ -51,8 +51,8 @@ class BatteryManagerIndex extends Component { */ componentDidMount() { this.fetchData(this.props.testEndpoint, 'GET', 'tests') - .then(() => this.fetchData(this.props.optionEndpoint, 'GET', 'options')) - .then(() => this.setState({isLoaded: true})); + .then(() => this.fetchData(this.props.optionEndpoint, 'GET', 'options')) + .then(() => this.setState({isLoaded: true})); } /** @@ -66,12 +66,12 @@ class BatteryManagerIndex extends Component { fetchData(url, method, state) { return new Promise((resolve, reject) => { return fetch(url, {credentials: 'same-origin', method: method}) - .then((resp) => resp.json()) - .then((data) => this.setState({[state]: data}, resolve)) - .catch((error) => { - this.setState({error: true}, reject); - console.error(error); - }); + .then((resp) => resp.json()) + .then((data) => this.setState({[state]: data}, resolve)) + .catch((error) => { + this.setState({error: true}, reject); + console.error(error); + }); }); } @@ -91,23 +91,23 @@ class BatteryManagerIndex extends Component { method: method, body: JSON.stringify(dataClone), }) - .then((response) => response.text() - .then((body) => { - body = JSON.parse(body); - if (response.ok) { - swal.fire('Submission successful!', body.message, 'success') - .then((result) => { - if (result.value) { - this.closeForm(); - resolve(body.message); + .then((response) => response.text() + .then((body) => { + body = JSON.parse(body); + if (response.ok) { + swal.fire('Submission successful!', body.message, 'success') + .then((result) => { + if (result.value) { + this.closeForm(); + resolve(body.message); + } + }); + } else { + swal.fire(body.error, '', 'error'); + reject(body.error); } - }); - } else { - swal.fire(body.error, '', 'error'); - reject(body.error); - } - }) - .catch((e) => reject(e))); + }) + .catch((e) => reject(e))); }); } @@ -120,28 +120,28 @@ class BatteryManagerIndex extends Component { */ mapColumn(column, value) { switch (column) { - case 'First Visit': - switch (value) { - case 'Y': - return 'Yes'; - case 'N': - return 'No'; - } - break; - case 'Active': - switch (value) { - case 'Y': - return 'Yes'; - case 'N': - return 'No'; - } - break; - case 'Change Status': - return ''; - case 'Edit Metadata': - return ''; - default: - return value; + case 'First Visit': + switch (value) { + case 'Y': + return 'Yes'; + case 'N': + return 'No'; + } + break; + case 'Active': + switch (value) { + case 'Y': + return 'Yes'; + case 'N': + return 'No'; + } + break; + case 'Change Status': + return ''; + case 'Edit Metadata': + return ''; + default: + return value; } } @@ -158,33 +158,33 @@ class BatteryManagerIndex extends Component { let result = {cell}; const testId = row['ID']; switch (column) { - case 'Instrument': - result = {this.state.options.instruments[cell]}; - break; - case 'Cohort': - result = {this.state.options.cohorts[cell]}; - break; - case 'Site': - result = {this.state.options.sites[cell]}; - break; - case 'Change Status': - if (row.Active === 'Y') { - result = { - this.deactivateTest(testId); - }}/>; - } else if (row.Active === 'N') { - result = { - this.activateTest(testId); - }}/>; - } - break; - case 'Edit Metadata': - const editButton = { - this.loadTest(testId); - this.setState({edit: true}); - }}/>; - result = {editButton}; - break; + case 'Instrument': + result = {this.state.options.instruments[cell]}; + break; + case 'Cohort': + result = {this.state.options.cohorts[cell]}; + break; + case 'Site': + result = {this.state.options.sites[cell]}; + break; + case 'Change Status': + if (row.Active === 'Y') { + result = { + this.deactivateTest(testId); + }}/>; + } else if (row.Active === 'N') { + result = { + this.activateTest(testId); + }}/>; + } + break; + case 'Edit Metadata': + const editButton = { + this.loadTest(testId); + this.setState({edit: true}); + }}/>; + result = {editButton}; + break; } return result; @@ -257,15 +257,15 @@ class BatteryManagerIndex extends Component { } }); this.checkDuplicate(test) - .then((test) => this.validateTest(test)) - .then((test) => this.postData( + .then((test) => this.validateTest(test)) + .then((test) => this.postData( this.props.testEndpoint+(test.id || ''), test, request - )) - .then(() => this.fetchData(this.props.testEndpoint, 'GET', 'tests')) - .then(() => resolve()) - .catch((e) => reject(e)); + )) + .then(() => this.fetchData(this.props.testEndpoint, 'GET', 'tests')) + .then(() => resolve()) + .catch((e) => reject(e)); }); } @@ -295,52 +295,57 @@ class BatteryManagerIndex extends Component { const fields = [ {label: 'ID', show: false}, {label: 'Instrument', show: true, filter: { - name: 'testName', - type: 'select', - options: options.instruments, - }}, + name: 'testName', + type: 'select', + options: options.instruments, + }}, {label: 'Minimum Age', show: true, filter: { - name: 'minimumAge', - type: 'numeric', - }}, + name: 'minimumAge', + type: 'numeric', + }}, {label: 'Maximum Age', show: true, filter: { - name: 'maximumAge', - type: 'numeric', - }}, + name: 'maximumAge', + type: 'numeric', + }}, {label: 'Stage', show: true, filter: { - name: 'stage', - type: 'select', - options: options.stages, - }}, + name: 'stage', + type: 'select', + options: options.stages, + }}, {label: 'Cohort', show: true, filter: { - name: 'cohort', - type: 'select', - options: options.cohorts, - }}, + name: 'cohort', + type: 'select', + options: options.cohorts, + }}, {label: 'Visit Label', show: true, filter: { - name: 'visitLabel', - type: 'select', - options: options.visits, - }}, + name: 'visitLabel', + type: 'select', + options: options.visits, + }}, {label: 'Site', show: true, filter: { - name: 'site', - type: 'select', - options: options.sites, - }}, + name: 'site', + type: 'select', + options: options.sites, + }}, {label: 'First Visit', show: true, filter: { - name: 'firstVisit', - type: 'select', - options: options.firstVisit, - }}, + name: 'firstVisit', + type: 'select', + options: options.firstVisit, + }}, {label: 'Instrument Order', show: true, filter: { - name: 'instrumentOrder', - type: 'text', - }}, + name: 'instrumentOrder', + type: 'text', + }}, + {label: 'Double Data Entry Enabled', show: true, filter: { + name: 'DoubleDataEntryEnabled', + type: 'select', + options: options.DoubleDataEntryEnabled, + }}, {label: 'Active', show: true, filter: { - name: 'active', - type: 'select', - options: options.active, - }}, + name: 'active', + type: 'select', + options: options.active, + }}, {label: 'Change Status', show: hasPermission('battery_manager_edit')}, {label: 'Edit Metadata', show: hasPermission('battery_manager_edit')}, ]; @@ -365,6 +370,7 @@ class BatteryManagerIndex extends Component { test.centerId, test.firstVisit, test.instrumentOrder, + test.DoubleDataEntryEnabled, test.active, ]; }); @@ -420,7 +426,8 @@ class BatteryManagerIndex extends Component { test.cohort == testCheck.cohort && test.visitLabel == testCheck.visitLabel && test.centerId == testCheck.centerId && - test.firstVisit == testCheck.firstVisit + test.firstVisit == testCheck.firstVisit && + test.DoubleDataEntryEnabled == testCheck.DoubleDataEntryEnabled ) { duplicate = testCheck; } diff --git a/modules/battery_manager/php/battery_manager.class.inc b/modules/battery_manager/php/battery_manager.class.inc index eea940044a6..884ce9fbf12 100644 --- a/modules/battery_manager/php/battery_manager.class.inc +++ b/modules/battery_manager/php/battery_manager.class.inc @@ -1,4 +1,5 @@ - $this->row['id'] ?? null, - 'Test_name' => $this->row['testName'] ?? null, - 'AgeMinDays' => $this->row['ageMinDays'] ?? null, - 'AgeMaxDays' => $this->row['ageMaxDays'] ?? null, - 'Stage' => $this->row['stage'] ?? null, - 'CohortID' => $this->row['cohort'] ?? null, - 'Visit_label' => $this->row['visitLabel'] ?? null, - 'CenterID' => $this->row['centerId'] ?? null, - 'firstVisit' => $this->row['firstVisit'] ?? null, - 'instr_order' => $this->row['instrumentOrder'] ?? null, - 'Active' => $this->row['active'] ?? null, + 'ID' => $this->row['id'] ?? null, + 'Test_name' => $this->row['testName'] ?? null, + 'AgeMinDays' => $this->row['ageMinDays'] ?? null, + 'AgeMaxDays' => $this->row['ageMaxDays'] ?? null, + 'Stage' => $this->row['stage'] ?? null, + 'CohortID' => $this->row['cohort'] ?? null, + 'Visit_label' => $this->row['visitLabel'] ?? null, + 'CenterID' => $this->row['centerId'] ?? null, + 'firstVisit' => $this->row['firstVisit'] ?? null, + 'DoubleDataEntryEnabled' => $this->row['DoubleDataEntryEnabled'] + ?? null, + 'instr_order' => $this->row['instrumentOrder'] ?? null, + 'Active' => $this->row['active'] ?? null, ]; } } diff --git a/modules/battery_manager/php/testendpoint.class.inc b/modules/battery_manager/php/testendpoint.class.inc index 87576d8e177..b3c17c3b0d9 100644 --- a/modules/battery_manager/php/testendpoint.class.inc +++ b/modules/battery_manager/php/testendpoint.class.inc @@ -1,4 +1,5 @@ -getBody()->getContents(), true); $test = new Test($this->loris, null, $testArray); $test->row['active'] = 'Y'; - return $this->_saveInstance($test); + return $this->_saveInstance($test, "post"); } /** * Generic save function for Test Instances. * - * @param Test $test The Test Instance to be saved. + * @param Test $test The Test Instance to be saved. + * @param string $method check the request method * * @return ResponseInterface response */ - private function _saveInstance(Test $test) + private function _saveInstance(Test $test , $method=null) { if (!$this->user->hasPermission('battery_manager_edit')) { return new \LORIS\Http\Response\JSON\Forbidden('Edit Permission Denied'); @@ -183,7 +185,7 @@ class TestEndpoint extends \NDB_Page implements RequestHandlerInterface } // check if instance is duplicate - if ($this->_isDuplicate($test)) { + if ($this->_isDuplicate($test) && $method =='post') { return new \LORIS\Http\Response\JSON\Conflict( 'This Test already exists in the database' ); @@ -217,7 +219,8 @@ class TestEndpoint extends \NDB_Page implements RequestHandlerInterface CohortID, Visit_label, CenterID, - firstVisit + firstVisit, + DoubleDataEntryEnabled as DDE_enabled FROM test_battery"; // Select duplicate entry from Test Battery $entries = $this->db->pselect($query, []); @@ -233,6 +236,7 @@ class TestEndpoint extends \NDB_Page implements RequestHandlerInterface && $testArray['Visit_label'] == $entry['Visit_label'] && $testArray['CenterID'] == $entry['CenterID'] && $testArray['firstVisit'] == $entry['firstVisit'] + && $testArray['DoubleDataEntryEnabled'] == $entry['DDE_enabled'] ) { return true; } diff --git a/modules/battery_manager/php/testoptionsendpoint.class.inc b/modules/battery_manager/php/testoptionsendpoint.class.inc index 79f6ead9873..6b2ba925349 100644 --- a/modules/battery_manager/php/testoptionsendpoint.class.inc +++ b/modules/battery_manager/php/testoptionsendpoint.class.inc @@ -1,4 +1,5 @@ -loris->getDatabaseConnection() ); return [ - 'instruments' => \NDB_BVL_Instrument::getInstrumentNamesList( + 'instruments' => \NDB_BVL_Instrument::getInstrumentNamesList( $this->loris ), - 'stages' => $this->_getStageList(), - 'cohorts' => \Utility::getCohortList(null), - 'visits' => $visitController->getVisitlabels(), - 'sites' => \Utility::getSiteList(false), - 'firstVisit' => $this->_getYesNoList(), - 'active' => $this->_getYesNoList(), + 'stages' => $this->_getStageList(), + 'cohorts' => \Utility::getCohortList(null), + 'visits' => $visitController->getVisitlabels(), + 'sites' => \Utility::getSiteList(false), + 'firstVisit' => $this->_getYesNoList(), + 'DoubleDataEntryEnabled' => $this->_getYesNoList(), + 'active' => $this->_getYesNoList(), ]; } diff --git a/modules/battery_manager/php/testprovisioner.class.inc b/modules/battery_manager/php/testprovisioner.class.inc index 3ed4b283339..2ee3944850e 100644 --- a/modules/battery_manager/php/testprovisioner.class.inc +++ b/modules/battery_manager/php/testprovisioner.class.inc @@ -1,4 +1,5 @@ -loris, isset($row['centerId']) - ? new \CenterID($row['centerId']) + ? \CenterID::singleton(intval($row['centerId'])) : null, $row ); diff --git a/modules/battery_manager/test/BatteryManagerTest.php b/modules/battery_manager/test/BatteryManagerTest.php index 29257f0bff5..ac2775621bf 100644 --- a/modules/battery_manager/test/BatteryManagerTest.php +++ b/modules/battery_manager/test/BatteryManagerTest.php @@ -1,4 +1,5 @@ - div > div > div:nth-child(1)'; + static $clearFilter = '.nav-tabs a'; /** * Tests that, when loading the BatteryManager module, some * text appears in the body. @@ -68,6 +76,17 @@ function testLoadsWithPermissionRead() "You do not have access to this page.", $bodyText ); + $bodyText = $this->safeFindElement( + WebDriverBy::cssSelector("#dynamictable > thead > tr") + )->getText(); + $this->assertStringNotContainsString( + "Change Status", + $bodyText + ); + $this->assertStringNotContainsString( + "Edit Metadata", + $bodyText + ); $this->resetPermissions(); } /** @@ -89,5 +108,230 @@ function testDoesNotLoadWithoutPermission() ); $this->resetPermissions(); } + /** + * Tests that the page does not load if the user does not have correct + * permissions + * + * @return void + */ + function testLoadsWithPermissionEdit() + { + $this->setupPermissions(["battery_manager_edit"]); + $this->safeGet($this->url . "/battery_manager/"); + $bodyText = $this->safeFindElement( + WebDriverBy::cssSelector("body") + )->getText(); + $this->assertStringNotContainsString( + "You do not have access to this page.", + $bodyText + ); + $bodyText = $this->safeFindElement( + WebDriverBy::cssSelector("#dynamictable > thead > tr") + )->getText(); + $this->assertStringContainsString( + "Change Status", + $bodyText + ); + $this->assertStringContainsString( + "Edit Metadata", + $bodyText + ); + $this->safeClick( + WebDriverBy::cssSelector( + "#dynamictable > tbody > tr:nth-child(1) > td:nth-child(14) > button" + ) + ); + $bodyText = $this->safeFindElement( + WebDriverBy::cssSelector( + "#lorisworkspace > div >". + " div:nth-child(2) > div > div:nth-child(1)" + ) + )->getText(); + $this->assertStringContainsString( + "Edit Test", + $bodyText + ); + + $this->resetPermissions(); + } + /** + * Tests that the page does not load if the user does not have correct + * permissions + * + * @return void + */ + function testEditform() + { + $this->safeGet($this->url . "/battery_manager/"); + $this->safeClick( + WebDriverBy::cssSelector( + "#dynamictable > tbody > tr > td:nth-child(14) > button" + ) + ); + $this->safeClick( + WebDriverBy::cssSelector( + "#lorisworkspace > div:nth-child(1) > div:nth-child(2) > ". + "div:nth-child(1) > div:nth-child(2) > div:nth-child(1) >". + " form:nth-child(1) > div:nth-child(1) >div:nth-child(2) >". + " div:nth-child(1) > div:nth-child(2) > select:nth-child(1) >" . + " option:nth-child(3)" + ), + 5 + ); + $this->safeFindElement( + WebDriverBy::cssSelector( + "div:nth-child(3) > div:nth-child(1) >". + " div:nth-child(2) > input:nth-child(1)", + 1 + ) + )->clear()->sendKeys('0'); + $this->safeFindElement( + WebDriverBy::cssSelector( + "div.col-sm-12:nth-child(4)>div:nth-child(1)". + ">div:nth-child(2)>input:nth-child(1)" + ), + 1 + )->clear()->sendKeys('1'); + $this->safeClick( + WebDriverBy::cssSelector( + "div:nth-child(5) > div:nth-child(1) > div:nth-child(2) > ". + "select:nth-child(1) > option:nth-child(2)" + ), + 1 + ); + $this->safeClick( + WebDriverBy::cssSelector( + "div.col-sm-9:nth-child(1) > button:nth-child(1)" + ), + 1 + ); + $bodyText = $this->safeFindElement( + WebDriverBy::cssSelector("#swal2-title"), + 1 + )->getText(); + $this->assertStringContainsString( + "Submission successful!", + $bodyText + ); + } + + /** + * Tests that the page does not load if the user does not have correct + * permissions + * + * @return void + */ + function testAddNew() + { + $this->safeGet($this->url . "/battery_manager/"); + $this->safeClick( + WebDriverBy::cssSelector( + "#default-panel > div > div > div.table-header >". + " div > div > div:nth-child(2) > button:nth-child(1)" + ) + ); + $this->safeClick( + WebDriverBy::cssSelector( + "#lorisworkspace > div:nth-child(1) > div:nth-child(2) >". + " div:nth-child(1) > div:nth-child(2) > div:nth-child(1) >". + " form:nth-child(1) > div:nth-child(1) >div:nth-child(2) > ". + "div:nth-child(1) > div:nth-child(2) > select:nth-child(1) >". + " option:nth-child(3)" + ), + 5 + ); + + $this->safeFindElement( + WebDriverBy::cssSelector( + "div:nth-child(3) > div:nth-child(1) > ". + "div:nth-child(2) > input:nth-child(1)", + 1 + ) + )->clear()->sendKeys('0'); + + $this->safeFindElement( + WebDriverBy::cssSelector( + "div.col-sm-12:nth-child(4)>div:nth-child(1)>". + "div:nth-child(2) >input:nth-child(1)" + ), + 1 + )->clear()->sendKeys('1'); + $this->safeClick( + WebDriverBy::cssSelector( + "div:nth-child(5) > div:nth-child(1) > div:nth-child(2) >". + " select:nth-child(1) > option:nth-child(2) +" + ), + 1 + ); + $this->safeClick( + WebDriverBy::cssSelector( + "div.col-sm-9:nth-child(1) > button:nth-child(1)" + ), + 1 + ); + $bodyText = $this->safeFindElement( + WebDriverBy::cssSelector("#swal2-title"), + 1 + )->getText(); + $this->assertStringContainsString( + "Submission successful!", + $bodyText + ); + } + /** + * Tests that the page does not load if the user does not have correct + * permissions + * + * @return void + */ + function testActivebtn() + { + $this->safeGet($this->url . "/battery_manager/"); + $this->safeClick( + WebDriverBy::cssSelector( + "#dynamictable > tbody > tr:nth-child(1) > td:nth-child(13) > button" + ) + ); + $bodyText = $this->safeFindElement( + WebDriverBy::cssSelector("#swal2-title") + )->getText(); + $this->assertStringContainsString( + "Submission successful!", + $bodyText + ); + } + /** + * Tests filter in the form + * The form should refreash and the data should be gone. + * + * @return void + */ + function testFilter() + { + $this->safeGet($this->url . "/battery_manager/"); + //testing data from RBdata.sql + $this->_filterTest( + self::$instrument, + self::$display, + self::$clearFilter, + 'AOSI', + '2 rows' + ); + $this->_filterTest( + self::$minimumAge, + self::$display, + self::$clearFilter, + '0', + '2 rows' + ); + $this->_filterTest( + self::$maximumAge, + self::$display, + self::$clearFilter, + '0', + '0 row' + ); + } } diff --git a/modules/battery_manager/test/TestPlan.md b/modules/battery_manager/test/TestPlan.md index 4a6973c3be9..f27930762b3 100644 --- a/modules/battery_manager/test/TestPlan.md +++ b/modules/battery_manager/test/TestPlan.md @@ -43,7 +43,7 @@ Test Battery. ### New Test Button -**Testing add functionality** +**Testing add functionality** [Automation Testing] Click the "New Test" button in the upper right corner of the data table. 1. Check that you cannot add an entry without filling out the required fields: `Instrument`, `Minimum age (days)`, `Maximum age (days)`, `Stage`. 2. Check that you can only enter positive numbers (including 0) in Minimum age (days) and Maximum age (days). @@ -76,7 +76,7 @@ Click the "New Test" button in the upper right corner of the data table. 1. Press the `Edit` button in the `Edit Metadata` column on an entry in the data table to edit. - Ensure that you are taken to an Edit page with a form that is populated with the entry's values. -**Test filters** +**Test filters** [Automation Testing] 1. A selection filter should be present on top of the page containing the following fields: - Minimum age, Maximum age, and Instrument Order (as text fields). - Instrument, Stage, Cohort, Visit Label, Site, First Visit, and Active (as dropdown fields with blank default option). @@ -87,7 +87,7 @@ Click the "New Test" button in the upper right corner of the data table. ### Edit window -**Testing edit (activate/deactivate/add) functionality** +**Testing edit (activate/deactivate/add) functionality** [Automation Testing] 1. Check that you cannot edit an entry without filling out the required fields: `Instrument`, `Minimum age (days)`, `Maximum age (days)`, `Stage`. 2. Check that you can only enter positive numbers (including 0) in Minimum age (days) and Maximum age (days). 3. Check that you can only enter positive numbers (including 0) in Instrument order. diff --git a/modules/behavioural_qc/jsx/tabs_content/behaviouralFeedback.js b/modules/behavioural_qc/jsx/tabs_content/behaviouralFeedback.js index 5ce9a202c6a..3d7fba2661a 100644 --- a/modules/behavioural_qc/jsx/tabs_content/behaviouralFeedback.js +++ b/modules/behavioural_qc/jsx/tabs_content/behaviouralFeedback.js @@ -82,35 +82,35 @@ class BehaviouralFeedback extends Component { formatColumn(column, cell, rowData, rowHeaders) { let reactElement; switch (column) { - case 'PSCID': - reactElement = ( - - + - {rowData['PSCID']} - - - ); - break; - case 'DCCID': - reactElement = ( - - + {rowData['PSCID']} + + + ); + break; + case 'DCCID': + reactElement = ( + +
- {rowData['DCCID']} - - - ); - break; - case 'Feedback Level': - let bvlLink = ''; - let bvlLevel = ''; - if (rowData['Instrument']) { - bvlLink = this.props.baseURL + + }> + {rowData['DCCID']} + + + ); + break; + case 'Feedback Level': + let bvlLink = ''; + let bvlLevel = ''; + if (rowData['Instrument']) { + bvlLink = this.props.baseURL + '/instruments/' + rowData['Test Name'] + '/?candID=' + @@ -119,36 +119,36 @@ class BehaviouralFeedback extends Component { rowData['sessionID'] + '&commentID=' + rowData['commentID']; - // Open feedback panel - bvlLink += '&showFeedback=true'; - bvlLevel ='Instrument : ' + rowData['Instrument']; - } else if (rowData['Visit']) { - bvlLink = this.props.baseURL + + // Open feedback panel + bvlLink += '&showFeedback=true'; + bvlLevel ='Instrument : ' + rowData['Instrument']; + } else if (rowData['Visit']) { + bvlLink = this.props.baseURL + '/instrument_list/' + '?candID=' + rowData['DCCID'] + '&sessionID=' + rowData['sessionID']; - // Open feedback panel - bvlLink += '&showFeedback=true'; - bvlLevel ='Visit : ' + rowData['Visit']; - } else { - bvlLink = this.props.baseURL + + // Open feedback panel + bvlLink += '&showFeedback=true'; + bvlLevel ='Visit : ' + rowData['Visit']; + } else { + bvlLink = this.props.baseURL + '/' + rowData['DCCID']; - // Open feedback panel - bvlLink += '/?showFeedback=true'; - bvlLevel ='Profile : ' + rowData['PSCID']; - } - reactElement = ( - - {bvlLevel} - - ); - break; - default: - reactElement = ( - {cell} - ); + // Open feedback panel + bvlLink += '/?showFeedback=true'; + bvlLevel ='Profile : ' + rowData['PSCID']; + } + reactElement = ( + + {bvlLevel} + + ); + break; + default: + reactElement = ( + {cell} + ); } return reactElement; } @@ -173,8 +173,8 @@ class BehaviouralFeedback extends Component { name: 'Instrument', type: 'select', options: Object.assign({}, ...Object.entries( - {...Object.values(options.instruments)}) - .map(([, b]) => ({[b]: b})) + {...Object.values(options.instruments)}) + .map(([, b]) => ({[b]: b})) ), }, }, diff --git a/modules/behavioural_qc/jsx/tabs_content/dataConflicts.js b/modules/behavioural_qc/jsx/tabs_content/dataConflicts.js index 0bebd905a08..c2b4d061185 100644 --- a/modules/behavioural_qc/jsx/tabs_content/dataConflicts.js +++ b/modules/behavioural_qc/jsx/tabs_content/dataConflicts.js @@ -82,48 +82,48 @@ class DataConflicts extends Component { formatColumn(column, cell, rowData, rowHeaders) { let reactElement = null; switch (column) { - case 'Visit': - reactElement = ( - - + - {rowData['Visit']} - - - ); - break; - case 'PSCID': - reactElement = ( - - + {rowData['Visit']} + + + ); + break; + case 'PSCID': + reactElement = ( + + - {rowData['PSCID']} - - - ); - break; - case 'DCCID': - reactElement = ( - - + {rowData['PSCID']} + + + ); + break; + case 'DCCID': + reactElement = ( + + - {rowData['DCCID']} - - - ); - break; - case 'Instrument': - reactElement = ( - - + {rowData['DCCID']} + + + ); + break; + case 'Instrument': + reactElement = ( + + - {rowData['Instrument']} - - - ); - break; - default: - reactElement = ( - {cell} - ); + }> + {rowData['Instrument']} + + + ); + break; + default: + reactElement = ( + {cell} + ); } return reactElement; } @@ -167,8 +167,8 @@ class DataConflicts extends Component { name: 'Instrument', type: 'select', options: Object.assign({}, ...Object.entries( - {...Object.values(options.instruments)}) - .map(([, b]) => ({[b]: b})) + {...Object.values(options.instruments)}) + .map(([, b]) => ({[b]: b})) ), }, }, diff --git a/modules/behavioural_qc/jsx/tabs_content/incompleteForms.js b/modules/behavioural_qc/jsx/tabs_content/incompleteForms.js index 4e6de6d5c55..8bf0ac9f615 100644 --- a/modules/behavioural_qc/jsx/tabs_content/incompleteForms.js +++ b/modules/behavioural_qc/jsx/tabs_content/incompleteForms.js @@ -82,48 +82,48 @@ class IncompleteForms extends Component { formatColumn(column, cell, rowData, rowHeaders) { let reactElement; switch (column) { - case 'Visit': - reactElement = ( - - + - {rowData['Visit']} - - - ); - break; - case 'PSCID': - reactElement = ( - - + {rowData['Visit']} + + + ); + break; + case 'PSCID': + reactElement = ( + + - {rowData['PSCID']} - - - ); - break; - case 'DCCID': - reactElement = ( - - + {rowData['PSCID']} + + + ); + break; + case 'DCCID': + reactElement = ( + + - {rowData['DCCID']} - - - ); - break; - case 'Instrument': - reactElement = ( - - + {rowData['DCCID']} + + + ); + break; + case 'Instrument': + reactElement = ( + + - {rowData['Instrument']} - - - ); - break; - default: - reactElement = ( - {cell} - ); + }> + {rowData['Instrument']} + + + ); + break; + default: + reactElement = ( + {cell} + ); } return reactElement; } @@ -167,8 +167,8 @@ class IncompleteForms extends Component { name: 'Instrument', type: 'select', options: Object.assign({}, ...Object.entries( - {...Object.values(options.instruments)}) - .map(([, b]) => ({[b]: b})) + {...Object.values(options.instruments)}) + .map(([, b]) => ({[b]: b})) ), }, }, diff --git a/modules/behavioural_qc/php/behavioural_qc.class.inc b/modules/behavioural_qc/php/behavioural_qc.class.inc index 2727cf4612e..c68ac32f41f 100644 --- a/modules/behavioural_qc/php/behavioural_qc.class.inc +++ b/modules/behavioural_qc/php/behavioural_qc.class.inc @@ -1,4 +1,5 @@ -_site)); + return \CenterID::singleton($this->_site); } /** @@ -137,7 +137,7 @@ class BehaviouralDTO implements \LORIS\Data\DataInstance, */ public function getProjectID(): \ProjectID { - return new \ProjectID(strval($this->_project)); + return \ProjectID::singleton($this->_project); } /** diff --git a/modules/behavioural_qc/php/models/conflictsdto.class.inc b/modules/behavioural_qc/php/models/conflictsdto.class.inc index 82564a5e353..7767ea0700f 100644 --- a/modules/behavioural_qc/php/models/conflictsdto.class.inc +++ b/modules/behavioural_qc/php/models/conflictsdto.class.inc @@ -51,9 +51,9 @@ class ConflictsDTO implements \LORIS\Data\DataInstance, /** * The project * - * @var string + * @var int */ - private $_project = ''; + private $_project; /** * The feedback_status @@ -65,9 +65,9 @@ class ConflictsDTO implements \LORIS\Data\DataInstance, /** * The site * - * @var string + * @var int */ - private $_site = ''; + private $_site; /** * The fieldName @@ -112,7 +112,7 @@ class ConflictsDTO implements \LORIS\Data\DataInstance, */ public function getCenterID(): \CenterID { - return new \CenterID(strval($this->_site)); + return \CenterID::singleton($this->_site); } /** @@ -123,7 +123,7 @@ class ConflictsDTO implements \LORIS\Data\DataInstance, */ public function getProjectID(): \ProjectID { - return new \ProjectID(strval($this->_project)); + return \ProjectID::singleton($this->_project); } /** diff --git a/modules/behavioural_qc/php/models/incompletedto.class.inc b/modules/behavioural_qc/php/models/incompletedto.class.inc index a2c1f3b33dc..50889e74c0a 100644 --- a/modules/behavioural_qc/php/models/incompletedto.class.inc +++ b/modules/behavioural_qc/php/models/incompletedto.class.inc @@ -51,9 +51,9 @@ class IncompleteDTO implements \LORIS\Data\DataInstance, /** * The project * - * @var string + * @var int */ - private $_project = ''; + private $_project; /** * The feedback_status @@ -65,9 +65,9 @@ class IncompleteDTO implements \LORIS\Data\DataInstance, /** * The site * - * @var string + * @var int */ - private $_site = ''; + private $_site; /** * The id @@ -112,7 +112,7 @@ class IncompleteDTO implements \LORIS\Data\DataInstance, */ public function getCenterID(): \CenterID { - return new \CenterID(strval($this->_site)); + return \CenterID::singleton($this->_site); } /** @@ -123,7 +123,7 @@ class IncompleteDTO implements \LORIS\Data\DataInstance, */ public function getProjectID(): \ProjectID { - return new \ProjectID(strval($this->_project)); + return \ProjectID::singleton($this->_project); } /** diff --git a/modules/behavioural_qc/php/module.class.inc b/modules/behavioural_qc/php/module.class.inc index 448a816e21a..1e7ce5d4700 100644 --- a/modules/behavioural_qc/php/module.class.inc +++ b/modules/behavioural_qc/php/module.class.inc @@ -1,4 +1,5 @@ -getSetting('DoubleDataEntryInstruments'); - $db = \NDB_Factory::singleton()->database(); - for ($i=0; $iquote($ddeInstruments[$i]); - } - $where = " - AND (f.commentid NOT LIKE 'DDE_%') - "; - if (count($ddeInstruments) > 0) { - $ddeInstruments = implode(',', $ddeInstruments); - $where = " - AND (f.test_name IN ($ddeInstruments) OR - f.commentid NOT LIKE 'DDE_%') - "; - } parent::__construct( " SELECT DISTINCT @@ -50,22 +34,29 @@ class IncompleteProvisioner extends \LORIS\Data\Provisioners\DBObjectProvisioner psc.CenterID AS _site, s.ID AS _id, f.SessionID as _sessionID, - f.test_name as _test_name, + t.test_name as _test_name, f.data_entry as _data_entry, f.commentid as _commentID FROM session s JOIN flag f ON (f.sessionid = s.id) JOIN candidate c ON (c.candid = s.candid) - JOIN test_names t ON (t.Test_name = f.Test_name) + JOIN test_names t ON (t.ID = f.TestID) JOIN psc ON (s.CenterID = psc.CenterID) + JOIN test_battery ON ( + t.Test_name = test_battery.Test_name + AND test_battery.Visit_label = s.Visit_label + ) WHERE s.Active = 'Y' AND c.Active = 'Y' AND coalesce(f.data_entry, 'In Progress') = 'In Progress' AND psc.Centerid != '1' AND c.Entity_type != 'Scanner' - $where + AND ( + test_battery.DoubleDataEntryEnabled = 'Y' + OR f.commentid NOT LIKE 'DDE_%' + ) ORDER BY f.commentid ", diff --git a/modules/behavioural_qc/test/behavioural_qcTest.php b/modules/behavioural_qc/test/behavioural_qcTest.php index 166202eafa2..07300a02ead 100644 --- a/modules/behavioural_qc/test/behavioural_qcTest.php +++ b/modules/behavioural_qc/test/behavioural_qcTest.php @@ -1,4 +1,5 @@ safeGet($this->url . "/behavioural_qc/"); + $bodyText = $this->safeFindElement( + WebDriverBy::cssSelector( + " #dynamictable >". + " tbody > tr:nth-child(1) > td:nth-child(2) > a" + ) + )->getAttribute('href'); + // check Instrument link + $this->assertStringContainsString( + "radiology_review/?candID=300001", + $bodyText + ); + $bodyText = $this->safeFindElement( + WebDriverBy::cssSelector( + " #dynamictable >". + " tbody > tr:nth-child(1) > td:nth-child(3) > a" + ) + )->getAttribute('href'); + // check Instrument link + $this->assertStringContainsString( + "300001", + $bodyText + ); + $bodyText = $this->safeFindElement( + WebDriverBy::cssSelector( + " #dynamictable >". + " tbody > tr:nth-child(1) > td:nth-child(4) > a" + ) + )->getAttribute('href'); + // check Instrument link + $this->assertStringContainsString( + "300001", + $bodyText + ); + $bodyText = $this->safeFindElement( + WebDriverBy::cssSelector( + " #dynamictable >". + " tbody > tr:nth-child(1) > td:nth-child(5) > a" + ) + )->getAttribute('href'); + // check Instrument link + $this->assertStringContainsString( + "instrument_list/?candID=300001", + $bodyText + ); + } } diff --git a/modules/brainbrowser/js/brainbrowser.loris.js b/modules/brainbrowser/js/brainbrowser.loris.js index 53d37e2449b..b9a0e00a24a 100644 --- a/modules/brainbrowser/js/brainbrowser.loris.js +++ b/modules/brainbrowser/js/brainbrowser.loris.js @@ -13,7 +13,7 @@ function getQueryVariable(variable) { for (i = 0; i < vars.length; i += 1) { pair = vars[i].split('='); if (pair[0] === variable) { - return unescape(pair[1]); + return unescape(pair[1]); } } } @@ -349,7 +349,7 @@ $(function() { value = volume.getVoxelMin(); } value = Math.max(volume.getVoxelMin(), - Math.min(value, volume.getVoxelMax())); + Math.min(value, volume.getVoxelMax())); this.value = value; // Update the slider. @@ -368,7 +368,7 @@ $(function() { value = volume.getVoxelMax(); } value = Math.max(volume.getVoxelMin(), - Math.min(value, volume.getVoxelMax())); + Math.min(value, volume.getVoxelMax())); this.value = value; // Update the slider. @@ -569,44 +569,44 @@ $(function() { fileNameID.tooltip(); $('#filename-'+volID).on('click', function() { - $('#filename-additional-info-'+volID).slideToggle('fast'); - let arrow = $(this).siblings('.arrow'); - if (arrow.hasClass('glyphicon-chevron-down')) { - arrow - .removeClass('glyphicon-chevron-down') - .addClass('glyphicon-chevron-up'); - } else { - arrow - .removeClass('glyphicon-chevron-up') - .addClass('glyphicon-chevron-down'); - } - }); - $('.filename-overlay').on('click', function() { - $('.filename-overlay-additional-info').slideToggle('fast'); - let arrow = $(this).siblings('.arrow'); - if (arrow.hasClass('glyphicon-chevron-down')) { - arrow - .removeClass('glyphicon-chevron-down') - .addClass('glyphicon-chevron-up'); - } else { - arrow - .removeClass('glyphicon-chevron-up') - .addClass('glyphicon-chevron-down'); - } - }); - - $('.arrow').on('click', function() { - $('#filename-additional-info-'+volID).slideToggle('fast'); - if ($('.arrow').hasClass('glyphicon-chevron-down')) { - $('.arrow') - .removeClass('glyphicon-chevron-down') - .addClass('glyphicon-chevron-up'); - } else { - $('.arrow') - .removeClass('glyphicon-chevron-up') - .addClass('glyphicon-chevron-down'); - } - }); + $('#filename-additional-info-'+volID).slideToggle('fast'); + let arrow = $(this).siblings('.arrow'); + if (arrow.hasClass('glyphicon-chevron-down')) { + arrow + .removeClass('glyphicon-chevron-down') + .addClass('glyphicon-chevron-up'); + } else { + arrow + .removeClass('glyphicon-chevron-up') + .addClass('glyphicon-chevron-down'); + } + }); + $('.filename-overlay').on('click', function() { + $('.filename-overlay-additional-info').slideToggle('fast'); + let arrow = $(this).siblings('.arrow'); + if (arrow.hasClass('glyphicon-chevron-down')) { + arrow + .removeClass('glyphicon-chevron-down') + .addClass('glyphicon-chevron-up'); + } else { + arrow + .removeClass('glyphicon-chevron-up') + .addClass('glyphicon-chevron-down'); + } + }); + + $('.arrow').on('click', function() { + $('#filename-additional-info-'+volID).slideToggle('fast'); + if ($('.arrow').hasClass('glyphicon-chevron-down')) { + $('.arrow') + .removeClass('glyphicon-chevron-down') + .addClass('glyphicon-chevron-up'); + } else { + $('.arrow') + .removeClass('glyphicon-chevron-up') + .addClass('glyphicon-chevron-down'); + } + }); // Contrast controls container.find('.contrast-div').each(function() { @@ -760,9 +760,9 @@ $(function() { let fgColor = getContrastYIQ(bgColor); $('#intensity-value-' + volID) - .css('background-color', '#' + bgColor) - .css('color', fgColor) - .html(Math.floor(value)); + .css('background-color', '#' + bgColor) + .css('color', fgColor) + .html(Math.floor(value)); if (volume.header && volume.header.time) { $('#time-slider-' + volID).slider( @@ -812,43 +812,43 @@ $(function() { 'imageinfo?fileids=' + mincIDs + '&fileurls=' + fileUrls, {credentials: 'same-origin', method: 'GET'} ) - .then((resp) => resp.json()) - .then((data) => { - for (const file of data) { + .then((resp) => resp.json()) + .then((data) => { + for (const file of data) { let volume = { - type: file.type, - template: { - element_id: 'volume-ui-template4d', - viewer_insert_class: 'volume-viewer-display', - }, + type: file.type, + template: { + element_id: 'volume-ui-template4d', + viewer_insert_class: 'volume-viewer-display', + }, }; if (file.type == 'nifti1') { - volume.nii_url = file.URL; + volume.nii_url = file.URL; } else { - volume.raw_data_url = file.URL; + volume.raw_data_url = file.URL; } mincVolumes.push(volume); mincFilenames.push(file.Filename); - } - bboptions.volumes = mincVolumes; - - // //////////////////////////// - // Load the default color map and then call - // render only after it's been loaded - // //////////////////////////// - viewer.loadDefaultColorMapFromURL( - colorMapConfig.url, - colorMapConfig.cursor_color, - function() { + } + bboptions.volumes = mincVolumes; + + // //////////////////////////// + // Load the default color map and then call + // render only after it's been loaded + // //////////////////////////// + viewer.loadDefaultColorMapFromURL( + colorMapConfig.url, + colorMapConfig.cursor_color, + function() { // /////////////////// // Load the volumes. // /////////////////// viewer.render(); // start the rendering viewer.loadVolumes(bboptions); // load the volumes - } - ); - }); + } + ); + }); return viewer; }); diff --git a/modules/brainbrowser/jsx/Brainbrowser.js b/modules/brainbrowser/jsx/Brainbrowser.js index ada35b847f4..4415748695b 100644 --- a/modules/brainbrowser/jsx/Brainbrowser.js +++ b/modules/brainbrowser/jsx/Brainbrowser.js @@ -88,9 +88,9 @@ class BrainBrowser extends Component {