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/SQL/0000-00-00-schema.sql b/SQL/0000-00-00-schema.sql
index 81e732446e8..ef64552e16c 100644
--- a/SQL/0000-00-00-schema.sql
+++ b/SQL/0000-00-00-schema.sql
@@ -341,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 +1530,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 +1539,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 +2100,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/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-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/htdocs/js/advancedMenu.js b/htdocs/js/advancedMenu.js
index 5aa6dc082e4..e52a52a8e14 100644
--- a/htdocs/js/advancedMenu.js
+++ b/htdocs/js/advancedMenu.js
@@ -8,8 +8,8 @@ function isElementSet() {
let set = 0;
let options = $('.advancedOptions option:selected'); // get all the selected dropdowns for the TR with the ID advancedOptions
let texts = $('.advancedOptions input[type=text]');
- // brows through the selected dropdowns
- // if any of the dropdown is not equal to 'All' then set the variable set to true
+ // brows through the selected dropdowns
+ // if any of the dropdown is not equal to 'All' then set the variable set to true
options.each(function() {
let value = $(this).text();
if (value !== 'All') {
@@ -17,8 +17,8 @@ function isElementSet() {
return;
}
});
- // browse though the text elements
- // /if any of the text element is not empty then set the variable set to true
+ // browse though the text elements
+ // /if any of the text element is not empty then set the variable set to true
texts.each(function() {
let value = $(this).val();
if (value !== '') {
diff --git a/htdocs/js/instrument_controlpanel_control.js b/htdocs/js/instrument_controlpanel_control.js
index 88b65888465..fe6e6983009 100644
--- a/htdocs/js/instrument_controlpanel_control.js
+++ b/htdocs/js/instrument_controlpanel_control.js
@@ -1,3 +1,3 @@
$(function() {
- $('[data-toggle="tooltip"]').tooltip();
+ $('[data-toggle="tooltip"]').tooltip();
});
diff --git a/htdocs/js/invalid_form_scroll.js b/htdocs/js/invalid_form_scroll.js
index 046cc86fcee..d74c750af41 100644
--- a/htdocs/js/invalid_form_scroll.js
+++ b/htdocs/js/invalid_form_scroll.js
@@ -4,8 +4,8 @@
*/
$(document).ready(function bindInvalidFormListeners() {
- // This will make sure that the flag indicating whether we scrolled
- // to an invalid element when the form is submitted is reset
+ // This will make sure that the flag indicating whether we scrolled
+ // to an invalid element when the form is submitted is reset
document.getElementsByName('fire_away')[0].addEventListener(
'click',
function() {
@@ -13,26 +13,26 @@ $(document).ready(function bindInvalidFormListeners() {
}
);
- // Override default event handler for invalid input elements
- // This will make sure that the invalid element appears at the top
- // of the page.
+ // Override default event handler for invalid input elements
+ // This will make sure that the invalid element appears at the top
+ // of the page.
let elements = document.querySelectorAll('input,select,textarea');
let navbarHeader = document.getElementsByClassName('navbar-header');
for (let i = elements.length; i--;) {
elements[i].addEventListener('invalid', function() {
- // Only make the uppermost invalid element visible when the
- // form is submitted
+ // Only make the uppermost invalid element visible when the
+ // form is submitted
if (!bindInvalidFormListeners.scrollingDone) {
this.scrollIntoView(true);
- // scrollingIntoView is not enough: the navigation bar will appear
- // over the invalid element and hide it.
- // We have to scroll an additional number of pixels down so that
- // the elements becomes visible.
+ // scrollingIntoView is not enough: the navigation bar will appear
+ // over the invalid element and hide it.
+ // We have to scroll an additional number of pixels down so that
+ // the elements becomes visible.
if (navbarHeader) {
window.scrollBy(0, -$(navbarHeader).height() - 10);
}
- // Only scroll once
+ // Only scroll once
bindInvalidFormListeners.scrollingDone = true;
}
});
diff --git a/htdocs/js/jquery.fileupload.js b/htdocs/js/jquery.fileupload.js
index 4d95b85a8d9..b631cab4ae9 100644
--- a/htdocs/js/jquery.fileupload.js
+++ b/htdocs/js/jquery.fileupload.js
@@ -26,8 +26,8 @@
$(element).change(function() {
let filename = $(this).val().split('\\').pop();
let placeHolder = $(this)
- .parent().parent().parent()
- .find('.file-caption-name');
+ .parent().parent().parent()
+ .find('.file-caption-name');
$(placeHolder).html(filename);
});
});
diff --git a/jslib/fetchDataStream.js b/jslib/fetchDataStream.js
index 6cf94a5cf83..c86a6e57779 100644
--- a/jslib/fetchDataStream.js
+++ b/jslib/fetchDataStream.js
@@ -10,35 +10,35 @@
* indicating whether the end of the stream has been reached.
*/
async function processLines(data, rowcb, endstreamcb) {
- const utf8Decoder = new TextDecoder('utf-8');
- let row = [];
- let colStart = -1;
- let rowStart = 0;
- for (let i = 0; i < data.length; i++) {
- switch (data[i]) {
- case 0x1e: // end of column
- const rowdata = data.slice(colStart+1, i);
- const encoded = utf8Decoder.decode(rowdata);
- colStart = i;
- row.push(encoded);
- continue;
- case 0x1f: // end of row
- const rowdata2 = data.slice(colStart+1, i);
- const encoded2 = utf8Decoder.decode(rowdata2);
- row.push(encoded2);
+ const utf8Decoder = new TextDecoder('utf-8');
+ let row = [];
+ let colStart = -1;
+ let rowStart = 0;
+ for (let i = 0; i < data.length; i++) {
+ switch (data[i]) {
+ case 0x1e: // end of column
+ const rowdata = data.slice(colStart+1, i);
+ const encoded = utf8Decoder.decode(rowdata);
+ colStart = i;
+ row.push(encoded);
+ continue;
+ case 0x1f: // end of row
+ const rowdata2 = data.slice(colStart+1, i);
+ const encoded2 = utf8Decoder.decode(rowdata2);
+ row.push(encoded2);
- rowcb(row);
+ rowcb(row);
- rowStart = i+1;
- colStart = i;
- row = [];
- continue;
- case 0x04: // end of stream
- endstreamcb(row);
- return {remainder: [], eos: true};
- }
+ rowStart = i+1;
+ colStart = i;
+ row = [];
+ continue;
+ case 0x04: // end of stream
+ endstreamcb(row);
+ return {remainder: [], eos: true};
}
- return {remainder: data.slice(rowStart), eos: false};
+ }
+ return {remainder: data.slice(rowStart), eos: false};
}
/**
@@ -55,44 +55,44 @@ async function processLines(data, rowcb, endstreamcb) {
* @param {string} method - the HTTP method to use for the request
*/
async function fetchDataStream(dataURL, rowcb, chunkcb, endstreamcb, method) {
- const response = await fetch(
- dataURL,
- {
- method: method || 'get',
- credentials: 'same-origin',
- },
- );
+ const response = await fetch(
+ dataURL,
+ {
+ method: method || 'get',
+ credentials: 'same-origin',
+ },
+ );
- const reader = response.body.getReader();
+ const reader = response.body.getReader();
- 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;
- });
- }
+ 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 8adcc594149..de2fb12f593 100644
--- a/jsx/Card.js
+++ b/jsx/Card.js
@@ -54,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 (
@@ -65,8 +65,8 @@ 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.state.hasError ?
- Something went wrong rendering this panel.
+ Something went wrong rendering this panel.
Please open a bug report.
-
- : this.props.children}
+
+ : 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 ? '' : (
-
+
Download Table as CSV
- )
+ )
}
{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 (
-
- {this.props.label}
- {requiredHTML}
-
+
{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 = (
-
- {this.props.label}
- {requiredHTML}
-
- );
+ if (this.props.label) {
inputClass = 'col-sm-9';
}
return (
- {label}
+ {this.props.label && (
+
+ )}
{emptyOptionHTML}
{optionList}
@@ -660,7 +639,6 @@ SelectElement.propTypes = {
required: PropTypes.bool,
emptyOption: PropTypes.bool,
autoSelect: PropTypes.bool,
- hasError: PropTypes.bool,
errorMessage: PropTypes.string,
onUserInput: PropTypes.func,
noMargins: PropTypes.bool,
@@ -680,8 +658,7 @@ SelectElement.defaultProps = {
sortByValue: true,
emptyOption: true,
autoSelect: true,
- hasError: false,
- errorMessage: 'The field is required!',
+ errorMessage: null,
onUserInput: function() {
console.warn('onUserInput() callback is not set');
},
@@ -810,15 +787,9 @@ export class TagsElement extends Component {
* @return {JSX} - React markup for the component
*/
render() {
- let disabled = this.props.disabled ? 'disabled' : null;
- let requiredHTML = null;
let emptyOptionHTML = null;
let errorMessage = null;
let elementClass = 'row form-group';
- // Add required asterix
- if (this.props.required) {
- requiredHTML = * ;
- }
// Add empty option
if (this.props.emptyOption) {
@@ -843,7 +814,7 @@ export class TagsElement extends Component {
list={this.props.id + '_list'}
className="form-control"
value={this.props.value || ''}
- disabled={disabled}
+ disabled={this.props.disabled}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
@@ -865,7 +836,7 @@ export class TagsElement extends Component {
className="form-control"
id={this.props.id}
value={this.props.value}
- disabled={disabled}
+ disabled={this.props.disabled}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
>
@@ -884,7 +855,7 @@ export class TagsElement extends Component {
id={this.props.id}
className="form-control"
value={this.props.value || ''}
- disabled={disabled}
+ disabled={this.props.disabled}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>;
@@ -892,7 +863,7 @@ export class TagsElement extends Component {
// iterate through added Tags items and render them
// with deletion button
- let items = this.props.items.map(function(item) {
+ let items = this.props.items.map((item) => {
let itmTxt;
// in event that the passed item is a key of options,
// render option value
@@ -903,28 +874,25 @@ export class TagsElement extends Component {
itmTxt = item;
}
return (
-
- {itmTxt}
+
+ {itmTxt}
-
-
+
+
);
- }, this);
+ });
return (
-
- {this.props.label}
- {requiredHTML}
-
+
{items}
{input}
@@ -934,7 +902,7 @@ export class TagsElement extends Component {
id={this.props.id + 'Add'}
type="button"
onClick={this.handleAdd}
- >
+ >
{this.props.btnLabel}
@@ -978,11 +946,10 @@ TagsElement.defaultProps = {
required: false,
disabled: false,
emptyOption: true,
- hasError: false,
allowDupl: false,
useSearch: false,
strictSearch: false, // only accept items specified in options
- errorMessage: '',
+ errorMessage: null,
pendingValKey: '',
btnLabel: 'Add Tag',
onUserInput: function() {
@@ -1025,21 +992,9 @@ export class TextareaElement 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;
-
- // Add required asterix
- if (required) {
- requiredHTML =
* ;
- }
-
return (
-
- {this.props.label}
- {requiredHTML}
-
+
@@ -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 = (
-
- {this.props.label}
- {requiredHTML}
-
- );
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 = (
-
- {this.props.label}
- {requiredHTML}
-
- );
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 = (
-
- {this.props.label}
- {requiredHTML}
-
- );
- }
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 =
- {this.props.label}
- {requiredHTML}
- ;
- 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 =
- {this.props.label}
- {requiredHTML}
- ;
- 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 =
- {this.props.label}
- {requiredHTML}
- ;
- 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 =
- {this.props.label}
- {requiredHTML}
- ;
- 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 (
-
- {this.props.label}
-
+
{fileName}
@@ -2069,25 +1928,16 @@ export class FileElement extends Component {
);
}
- let labelHTML;
- let classSz;
- if (this.props.label) {
- labelHTML =
- {this.props.label}
- {requiredHTML}
- ;
- 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 = (
-
- {this.props.label}
-
- );
- }
return (
- {label}
+ {this.props.label && (
+
+ )}
{this.props.text}
@@ -2277,9 +2119,7 @@ export class LinkElement extends Component {
render() {
return (
-
- {this.props.label}
-
+
{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 (
-
+
@@ -2396,7 +2234,7 @@ CheckboxElement.defaultProps = {
id: null,
disabled: false,
required: false,
- errorMessage: '',
+ errorMessage: null,
offset: 'col-sm-offset-3',
class: 'checkbox-inline',
elementClass: 'checkbox-inline col-sm-offset-3',
@@ -2488,30 +2326,30 @@ export class CTA extends Component {
*
* @return {JSX} - React markup for the component
*/
- render() {
- return (
-
- {this.props.label}
-
- );
- }
- }
-
- CTA.propTypes = {
- label: PropTypes.string,
- buttonClass: PropTypes.string,
- onUserInput: PropTypes.func,
- };
-
- CTA.defaultProps = {
- buttonClass: 'btn btn-primary',
- onUserInput: function() {
- console.warn('onUserInput() callback is not set');
- },
- };
+ render() {
+ return (
+
+ {this.props.label}
+
+ );
+ }
+}
+
+CTA.propTypes = {
+ label: PropTypes.string,
+ buttonClass: PropTypes.string,
+ onUserInput: PropTypes.func,
+};
+
+CTA.defaultProps = {
+ buttonClass: 'btn btn-primary',
+ onUserInput: function() {
+ console.warn('onUserInput() callback is not set');
+ },
+};
/**
* Generic form element.
@@ -2538,58 +2376,58 @@ export class LorisElement extends Component {
let elementHtml =
;
switch (elementProps.type) {
- case 'text':
- elementHtml = ( );
- break;
- case 'email':
- elementHtml = ( );
- break;
- case 'password':
- elementHtml = ( );
- break;
- case 'tags':
- elementHtml = ( );
- break;
- case 'select':
- elementHtml = ( );
- break;
- case 'search':
- elementHtml = ( );
- break;
- case 'date':
- elementHtml = ( );
- break;
- case 'time':
- elementHtml = ( );
- break;
- case 'numeric':
- elementHtml = ( );
- break;
- case 'textarea':
- elementHtml = ( );
- break;
- case 'file':
- elementHtml = ( );
- break;
- case 'static':
- elementHtml = ( );
- break;
- case 'header':
- elementHtml = ( );
- break;
- case 'link':
- elementHtml = ( );
- break;
- case 'advcheckbox':
- elementHtml = ( );
- break;
- default:
- console.warn(
- 'Element of type ' +
+ case 'text':
+ elementHtml = ( );
+ break;
+ case 'email':
+ elementHtml = ( );
+ break;
+ case 'password':
+ elementHtml = ( );
+ break;
+ case 'tags':
+ elementHtml = ( );
+ break;
+ case 'select':
+ elementHtml = ( );
+ break;
+ case 'search':
+ elementHtml = ( );
+ break;
+ case 'date':
+ elementHtml = ( );
+ break;
+ case 'time':
+ elementHtml = ( );
+ break;
+ case 'numeric':
+ elementHtml = ( );
+ break;
+ case 'textarea':
+ elementHtml = ( );
+ break;
+ case 'file':
+ elementHtml = ( );
+ break;
+ case 'static':
+ elementHtml = ( );
+ break;
+ case 'header':
+ elementHtml = ( );
+ break;
+ case 'link':
+ elementHtml = ( );
+ break;
+ case 'advcheckbox':
+ elementHtml = ( );
+ break;
+ default:
+ console.warn(
+ 'Element of type ' +
elementProps.type +
' is not currently implemented!'
- );
- break;
+ );
+ break;
}
return elementHtml;
@@ -2637,8 +2475,6 @@ export class RadioElement extends React.Component {
*/
generateLayout() {
let layout = [];
- let disabled = this.props.disabled ? 'disabled' : null;
- let required = this.props.required ? 'required' : null;
const styleRow = {
display: 'flex',
@@ -2673,7 +2509,7 @@ export class RadioElement extends React.Component {
const checked = this.props.checked === key;
content.push(
+ style={styleColumn}>
{this.props.options[key]}
@@ -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 (
+ htmlFor={this.props.id}>
{this.props.label}
{errorMessage}
{requiredHTML}
@@ -2852,8 +2686,8 @@ export class SliderElement extends React.Component {
value={this.props.value}
min={this.props.minValue}
max={this.props.maxValue}
- required={required}
- disabled={disabled}
+ required={this.props.required}
+ disabled={this.props.disabled}
onChange={this.handleChange}
style={{width: '100%'}}
/>
@@ -2871,8 +2705,8 @@ export class SliderElement extends React.Component {
value={this.props.value}
min={this.props.minValue}
max={this.props.maxValue}
- required={required}
- disabled={disabled}
+ required={this.props.required}
+ disabled={this.props.disabled}
onChange={this.handleChange}
style={{
width: '50px',
@@ -2905,7 +2739,7 @@ SliderElement.defaultProps = {
maxWidth: 'auto',
disabled: false,
required: false,
- errorMessage: '',
+ errorMessage: null,
elementClass: 'row form-group',
onUserInput: function() {
console.warn('onUserInput() callback is not set');
diff --git a/jsx/InfoPanel.tsx b/jsx/InfoPanel.tsx
index c9627d12972..7d72eb0c1eb 100644
--- a/jsx/InfoPanel.tsx
+++ b/jsx/InfoPanel.tsx
@@ -4,32 +4,31 @@ import {ReactElement, ReactNode} from 'react';
* Display a message in an information panel.
*
* @param {object} props - React props
-
* @returns {ReactNode} - the InfoPanel
*/
function InfoPanel(props: {children: ReactNode}): ReactElement {
- return (
-
-
-
-
-
{props.children}
-
- );
+ return (
+
+
+
+
+
{props.children}
+
+ );
}
export default InfoPanel;
diff --git a/jsx/Loader.js b/jsx/Loader.js
deleted file mode 100644
index 6706b4908b7..00000000000
--- a/jsx/Loader.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * This file contains the React component for Loader
- *
- * @author Henri Rabalais
- * @version 1.0.0
- */
-import PropTypes from 'prop-types';
-
-/**
- * Loader is a React component which shows a spinner wheel while
- * something is loading.
- *
- * @param {array} props - The React props
- * @return {HTMLElement} - Loader React component
- */
-function Loader(props) {
- 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 (
<>
+ className="btn btn-default dropdown-toggle col-xs-12"
+ onClick={this.toggleDropdown}>
-
- {selectLabel}
-
+
+ {selectLabel}
+
diff --git a/jsx/PaginationLinks.js b/jsx/PaginationLinks.js
index 3deead5e29f..229bd7f989e 100644
--- a/jsx/PaginationLinks.js
+++ b/jsx/PaginationLinks.js
@@ -91,7 +91,7 @@ class PaginationLinks extends Component {
lastShownPage = 1;
}
- // If there is only 1 page, don't display pagination links
+ // If there is only 1 page, don't display pagination links
if (startPage === lastShownPage) {
return
;
}
@@ -124,7 +124,7 @@ class PaginationLinks extends Component {
return (
- {pageLinks}
+ {pageLinks}
);
}
diff --git a/jsx/Panel.js b/jsx/Panel.js
index 2daebc32e4e..e89f9fd4d49 100644
--- a/jsx/Panel.js
+++ b/jsx/Panel.js
@@ -46,8 +46,8 @@ const Panel = (props) => {
for (const [index, view] of props.views.entries()) {
views.push(
viewClicked(index)}
- className={index === activeView ? 'active' : null}>
+ onClick={() => viewClicked(index)}
+ className={index === activeView ? 'active' : null}>
{view['title']}
@@ -55,9 +55,9 @@ const Panel = (props) => {
);
content.push(
+ id={`${index}_panel_content_${props.id}`}
+ className={index === activeView ?
+ `${index}_panel_content` : `${index}_panel_content hidden`}>
{view['content']}
);
@@ -65,12 +65,12 @@ const Panel = (props) => {
panelViews = (
+ className='btn btn-default btn-xs dropdown-toggle'
+ data-toggle='dropdown'>
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 = (
-
- {props.label}
- {required
- ? (* )
- : null}
-
- );
- }
-
+ 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}) => (
+
+ {label}
+ {required && * }
+
+);
+
+export default InputLabel;
diff --git a/modules/acknowledgements/jsx/acknowledgementsIndex.js b/modules/acknowledgements/jsx/acknowledgementsIndex.js
index 01920b30643..6051ba96c53 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';
/**
@@ -161,27 +161,27 @@ class AcknowledgementsIndex extends Component {
credentials: 'same-origin',
body: formObject,
})
- .then((resp) => {
- if (resp.ok && resp.status === 200) {
- swal.fire(
- 'Success!',
- 'Acknowledgement added.',
- 'success'
- ).then((result) => {
- if (result.value) {
- this.closeModalForm();
- this.fetchData();
- }
- });
- } else {
- resp.text().then((message) => {
- swal.fire('Error!', message, 'error');
- });
- }
- })
- .catch((error) => {
- console.error(error);
- });
+ .then((resp) => {
+ if (resp.ok && resp.status === 200) {
+ swal.fire(
+ 'Success!',
+ 'Acknowledgement added.',
+ 'success'
+ ).then((result) => {
+ if (result.value) {
+ this.closeModalForm();
+ this.fetchData();
+ }
+ });
+ } else {
+ resp.text().then((message) => {
+ swal.fire('Error!', message, 'error');
+ });
+ }
+ })
+ .catch((error) => {
+ console.error(error);
+ });
}
/**
@@ -220,16 +220,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;
}
@@ -386,10 +386,10 @@ class AcknowledgementsIndex extends Component {
return ;
}
- /**
- * XXX: Currently, the order of these fields MUST match the order of the
- * queried columns in _setupVariables() in acknowledgements.class.inc
- */
+ /**
+ * XXX: Currently, the order of these fields MUST match the order of the
+ * queried columns in _setupVariables() in acknowledgements.class.inc
+ */
const options = this.state.data.fieldOptions;
const fields = [
{label: 'Ordering', show: true},
diff --git a/modules/api/docs/LorisRESTAPI_v0.0.4-dev.md b/modules/api/docs/LorisRESTAPI_v0.0.4-dev.md
index e445316e5b5..6edea3e19a0 100644
--- a/modules/api/docs/LorisRESTAPI_v0.0.4-dev.md
+++ b/modules/api/docs/LorisRESTAPI_v0.0.4-dev.md
@@ -189,11 +189,13 @@ Will return a JSON object of the form:
"FullName" : "Long Name",
"Subgroup" : "Subgroup Name",
"DoubleDataEntryEnabled" : boolean
+ "DoubleDataEntryVisits" : array
},
"Instrument2" : {
"FullName" : "Long Name",
"Subgroup" : "Subgroup Name",
"DoubleDataEntryEnabled" : boolean
+ "DoubleDataEntryVisits" : array
},
...
}
diff --git a/modules/api/php/endpoints/project/instruments.class.inc b/modules/api/php/endpoints/project/instruments.class.inc
index 14eacd3be10..1b2d891b641 100644
--- a/modules/api/php/endpoints/project/instruments.class.inc
+++ b/modules/api/php/endpoints/project/instruments.class.inc
@@ -42,10 +42,13 @@ class Instruments extends Endpoint implements \LORIS\Middleware\ETagCalculator
/**
* Contructor
*
- * @param \Project $project The requested project
+ * @param \Project $project The requested project
+ * @param string $apiversion The version of the API being used
*/
- public function __construct(\Project $project)
- {
+ public function __construct(
+ \Project $project,
+ private string $apiversion = 'v0.0.3'
+ ) {
$this->_project = $project;
}
@@ -146,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 c7baf1f10a8..105483a0ac7 100644
--- a/modules/api/php/endpoints/project/project.class.inc
+++ b/modules/api/php/endpoints/project/project.class.inc
@@ -100,6 +100,8 @@ class Project extends Endpoint implements \LORIS\Middleware\ETagCalculator
}
}
+ $apiversion = $request->getAttribute("LORIS-API-Version") ?? "unknown";
+
// Delegate to sub-endpoints
$subendpoint = array_shift($pathparts);
switch ($subendpoint) {
@@ -110,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/models/projectinstrumentsrow.class.inc b/modules/api/php/models/projectinstrumentsrow.class.inc
index 58bd8634d51..76be7c809f7 100644
--- a/modules/api/php/models/projectinstrumentsrow.class.inc
+++ b/modules/api/php/models/projectinstrumentsrow.class.inc
@@ -27,25 +27,26 @@ class ProjectInstrumentsRow implements \LORIS\Data\DataInstance
private $_fullname;
private $_subgroupname;
private $_isDDE;
+ private $_ddeVisits;
/**
* Create a new ProjectImagesRow.
*
- * @param array $row An array of image properties
+ * @param array $row An array of image properties
+ * @param string $apiversion The version of the API being used
*/
- public function __construct(array $row)
- {
- $shortname = $row['shortname'] ?? null;
- $ddeinstruments = array_keys(\Utility::getAllDDEInstruments());
+ public function __construct(
+ array $row,
+ private string $apiversion = 'v0.0.3'
+ ) {
+ $this->_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;
}
/**
@@ -88,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.
*
@@ -95,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/views/project/instruments.class.inc b/modules/api/php/views/project/instruments.class.inc
index c117d463404..49a3ceb0c8f 100644
--- a/modules/api/php/views/project/instruments.class.inc
+++ b/modules/api/php/views/project/instruments.class.inc
@@ -45,9 +45,13 @@ class Instruments
*
* @param \Project $project The requested project
* @param ProjectInstrumentsRow[] $instruments An array of ProjectInstrumentsRow
+ * @param string $apiversion The version of the API being used
*/
- public function __construct(\Project $project, array $instruments)
- {
+ public function __construct(
+ \Project $project,
+ array $instruments,
+ private string $apiversion = 'v0.0.3'
+ ) {
$this->_project = $project;
$this->_instruments = $instruments;
}
@@ -70,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/battery_manager/jsx/batteryManagerForm.js b/modules/battery_manager/jsx/batteryManagerForm.js
index c41342c18dd..7e7761f4928 100644
--- a/modules/battery_manager/jsx/batteryManagerForm.js
+++ b/modules/battery_manager/jsx/batteryManagerForm.js
@@ -1,11 +1,11 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
- ButtonElement,
- FormElement,
- StaticElement,
- SelectElement,
- NumericElement,
+ ButtonElement,
+ FormElement,
+ StaticElement,
+ SelectElement,
+ NumericElement,
} from 'jsx/Form';
/**
@@ -43,7 +43,7 @@ class BatteryManagerForm extends Component {
entry.
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/test.class.inc b/modules/battery_manager/php/test.class.inc
index 2c948025fca..f83fa27abb3 100644
--- a/modules/battery_manager/php/test.class.inc
+++ b/modules/battery_manager/php/test.class.inc
@@ -90,17 +90,19 @@ class Test implements
public function toSQL() : array
{
return [
- '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,
- '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 2e18205844a..b3c17c3b0d9 100644
--- a/modules/battery_manager/php/testendpoint.class.inc
+++ b/modules/battery_manager/php/testendpoint.class.inc
@@ -219,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, []);
@@ -235,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 77af508917c..6b2ba925349 100644
--- a/modules/battery_manager/php/testoptionsendpoint.class.inc
+++ b/modules/battery_manager/php/testoptionsendpoint.class.inc
@@ -64,15 +64,16 @@ class TestOptionsEndpoint extends \NDB_Page
$this->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 777e52e347c..2ee3944850e 100644
--- a/modules/battery_manager/php/testprovisioner.class.inc
+++ b/modules/battery_manager/php/testprovisioner.class.inc
@@ -51,6 +51,7 @@ class TestProvisioner extends \LORIS\Data\Provisioners\DBRowProvisioner
b.Visit_label as visitLabel,
p.CenterID as centerId,
b.firstVisit,
+ b.DoubleDataEntryEnabled,
b.instr_order as instrumentOrder,
b.Active as active
FROM test_battery b
diff --git a/modules/battery_manager/test/BatteryManagerTest.php b/modules/battery_manager/test/BatteryManagerTest.php
index ce7e9327206..ac2775621bf 100644
--- a/modules/battery_manager/test/BatteryManagerTest.php
+++ b/modules/battery_manager/test/BatteryManagerTest.php
@@ -138,7 +138,7 @@ function testLoadsWithPermissionEdit()
);
$this->safeClick(
WebDriverBy::cssSelector(
- "#dynamictable > tbody > tr:nth-child(1) > td:nth-child(13) > button"
+ "#dynamictable > tbody > tr:nth-child(1) > td:nth-child(14) > button"
)
);
$bodyText = $this->safeFindElement(
@@ -165,55 +165,50 @@ function testEditform()
$this->safeGet($this->url . "/battery_manager/");
$this->safeClick(
WebDriverBy::cssSelector(
- "#dynamictable > tbody > tr > td:nth-child(13) > button"
+ "#dynamictable > tbody > tr > td:nth-child(14) > button"
)
);
$this->safeClick(
WebDriverBy::cssSelector(
- "#lorisworkspace > div>div:nth-child(2)>div>div:nth-child(2)>form>".
- " div > div:nth-child(2) > div > div > select > option:nth-child(2)"
- )
+ "#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(
- "#lorisworkspace > div> div:nth-child(2) > div > div:nth-child(2)".
- " > form > div > div:nth-child(3) > div > div > input"
+ "div:nth-child(3) > div:nth-child(1) >".
+ " div:nth-child(2) > input:nth-child(1)",
+ 1
)
)->clear()->sendKeys('0');
+
$this->safeFindElement(
WebDriverBy::cssSelector(
- "#lorisworkspace>div> div:nth-child(2) > div > div:nth-child(2) ".
- "> form > div > div:nth-child(4) > div > div > input"
- )
+ "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(
- "#lorisworkspace>div>div:nth-child(2)>div>div:nth-child(2)>form>".
- "div>div:nth-child(5) > div > div > select > option:nth-child(7)"
- )
+ "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(
- "#lorisworkspace > div > div:nth-child(2) > div > div:nth-child(2)>".
- "form>div>div:nth-child(6) > div > div >select> option:nth-child(2)"
- )
- );
- $this->safeClick(
- WebDriverBy::cssSelector(
- "#lorisworkspace > div >div:nth-child(2) > div > div:nth-child(2) >".
- "form>div>div:nth-child(7) >div > div > select > option:nth-child(5)"
- )
- );
-
- $this->safeClick(
- WebDriverBy::cssSelector(
- "#lorisworkspace>div>div:nth-child(2)>div>div:nth-child(2)>form>".
- " div > div:nth-child(11) > div > div > button"
- )
+ "div.col-sm-9:nth-child(1) > button:nth-child(1)"
+ ),
+ 1
);
$bodyText = $this->safeFindElement(
- WebDriverBy::cssSelector("#swal2-title")
+ WebDriverBy::cssSelector("#swal2-title"),
+ 1
)->getText();
$this->assertStringContainsString(
"Submission successful!",
@@ -238,37 +233,47 @@ function testAddNew()
);
$this->safeClick(
WebDriverBy::cssSelector(
- "#lorisworkspace > div > div:nth-child(2) > div > div:nth-child(2) ".
- "> form > div > div:nth-child(2)>div>div>select>option:nth-child(2)"
- )
+ "#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(
- "#lorisworkspace > div > div:nth-child(2) > div ".
- "> div:nth-child(2)>form>div>div:nth-child(3) > div > div > input"
+ "div:nth-child(3) > div:nth-child(1) > ".
+ "div:nth-child(2) > input:nth-child(1)",
+ 1
)
)->clear()->sendKeys('0');
+
$this->safeFindElement(
WebDriverBy::cssSelector(
- "#lorisworkspace > div > div:nth-child(2) > div >".
- " div:nth-child(2)>form > div > div:nth-child(4) > div > div >input"
- )
+ "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(
- "#lorisworkspace > div > div:nth-child(2)>div>div:nth-child(2)>form".
- " > div > div:nth-child(5) > div > div >select > option:nth-child(7)"
- )
+ "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(
- "#lorisworkspace > div >div:nth-child(2)>div>div:nth-child(2)>form ".
- "> div > div:nth-child(11) > div > div > button"
- )
+ "div.col-sm-9:nth-child(1) > button:nth-child(1)"
+ ),
+ 1
);
$bodyText = $this->safeFindElement(
- WebDriverBy::cssSelector("#swal2-title")
+ WebDriverBy::cssSelector("#swal2-title"),
+ 1
)->getText();
$this->assertStringContainsString(
"Submission successful!",
@@ -286,7 +291,7 @@ function testActivebtn()
$this->safeGet($this->url . "/battery_manager/");
$this->safeClick(
WebDriverBy::cssSelector(
- "#dynamictable > tbody > tr:nth-child(1) > td:nth-child(12) > button"
+ "#dynamictable > tbody > tr:nth-child(1) > td:nth-child(13) > button"
)
);
$bodyText = $this->safeFindElement(
@@ -312,7 +317,7 @@ function testFilter()
self::$display,
self::$clearFilter,
'AOSI',
- '4 rows'
+ '2 rows'
);
$this->_filterTest(
self::$minimumAge,
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/provisioners/incompleteprovisioner.class.inc b/modules/behavioural_qc/php/provisioners/incompleteprovisioner.class.inc
index 57ab07a50cd..f22f3096846 100644
--- a/modules/behavioural_qc/php/provisioners/incompleteprovisioner.class.inc
+++ b/modules/behavioural_qc/php/provisioners/incompleteprovisioner.class.inc
@@ -22,22 +22,6 @@ class IncompleteProvisioner extends \LORIS\Data\Provisioners\DBObjectProvisioner
*/
function __construct()
{
- $config =& \NDB_Config::singleton();
- $ddeInstruments = $config->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 (t.test_name IN ($ddeInstruments) OR
- f.commentid NOT LIKE 'DDE_%')
- ";
- }
parent::__construct(
"
SELECT DISTINCT
@@ -59,13 +43,20 @@ class IncompleteProvisioner extends \LORIS\Data\Provisioners\DBObjectProvisioner
JOIN candidate c ON (c.candid = s.candid)
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/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 {
Sync Volumes
Reset View
+ className='control'
+ value={this.state.panelSize}
+ onChange={this.handleChange}>
Auto
{Object.keys(options).map(function(option) {
return (
diff --git a/modules/bvl_feedback/jsx/react.behavioural_feedback_panel.js b/modules/bvl_feedback/jsx/react.behavioural_feedback_panel.js
index e2c18f942c6..d2334371da5 100644
--- a/modules/bvl_feedback/jsx/react.behavioural_feedback_panel.js
+++ b/modules/bvl_feedback/jsx/react.behavioural_feedback_panel.js
@@ -380,7 +380,7 @@ class FeedbackPanelRow extends Component {
value={entry.Comment}
date={entry.Date}
/>
- : null}
+ : null}
);
});
@@ -392,9 +392,9 @@ class FeedbackPanelRow extends Component {
dropdown = (Close );
commentButton = (
+ className='glyphicon glyphicon-comment'
+ onClick={this.props.commentToggle}
+ />
);
}
@@ -515,8 +515,8 @@ class CommentEntryForm extends Component {
{
this.state.entryID < 0 ?
- Add a comment: :
- Update comment:
+ Add a comment: :
+ Update comment:
}
);
+ }
+
+ return (
+ {consent.Label}
+ {value}
+ {date}
+ );
}
export default ConsentWidget;
diff --git a/modules/candidate_parameters/jsx/DiagnosisEvolution.js b/modules/candidate_parameters/jsx/DiagnosisEvolution.js
index a0a24261290..28b1311c4c7 100644
--- a/modules/candidate_parameters/jsx/DiagnosisEvolution.js
+++ b/modules/candidate_parameters/jsx/DiagnosisEvolution.js
@@ -37,7 +37,7 @@ class DiagnosisEvolution extends Component {
*/
componentDidMount() {
this.fetchData()
- .then(() => this.setState({isLoaded: true}));
+ .then(() => this.setState({isLoaded: true}));
}
/**
@@ -62,41 +62,41 @@ class DiagnosisEvolution extends Component {
const dxEvolution = this.state.data.diagnosisEvolution;
let formattedDxEvolution = [];
try {
- dxEvolution.map((record) => {
- let formattedDiagnosis = [];
- Object.entries(JSON.parse(record.Diagnosis)).map((entry, index) => {
- const [fieldName, dx] = entry;
- formattedDiagnosis.push(
-
{fieldName}: {dx}
- );
- });
- let sourceFields = [];
- record.sourceField.split(',').map((field, index) => {
- sourceFields.push(
{field}
);
- });
- const confirmed = record.Confirmed === 'Y' ?
-
+ dxEvolution.map((record) => {
+ let formattedDiagnosis = [];
+ Object.entries(JSON.parse(record.Diagnosis)).map((entry, index) => {
+ const [fieldName, dx] = entry;
+ formattedDiagnosis.push(
+
{fieldName}: {dx}
+ );
+ });
+ let sourceFields = [];
+ record.sourceField.split(',').map((field, index) => {
+ sourceFields.push(
{field}
);
+ });
+ const confirmed = record.Confirmed === 'Y' ?
+
✔
-
:
-
+
:
+
✗
-
;
- formattedDxEvolution.push(
- [
- record.TrajectoryName,
- record.Project,
- record.OrderNumber,
- record.visitLabel,
- record.instrumentName,
- sourceFields,
- formattedDiagnosis,
- confirmed,
- record.LastUpdate,
- ]
- );
- });
+ ;
+ formattedDxEvolution.push(
+ [
+ record.TrajectoryName,
+ record.Project,
+ record.OrderNumber,
+ record.visitLabel,
+ record.instrumentName,
+ sourceFields,
+ formattedDiagnosis,
+ confirmed,
+ record.LastUpdate,
+ ]
+ );
+ });
} catch (error) {
- console.error('Error parsing JSON:', error);
+ console.error('Error parsing JSON:', error);
}
return formattedDxEvolution;
}
@@ -144,24 +144,24 @@ class DiagnosisEvolution extends Component {
*/
render() {
if (this.state.error) {
- return
An error occured while loading the page. ;
+ return
An error occured while loading the page. ;
}
if (!this.state.isLoaded) {
- return
;
+ return
;
}
const latestConfirmedProjectDiagnosisIDs =
this.state.data.latestConfirmedProjectDiagnosis.length > 0 ?
- this.state.data.latestConfirmedProjectDiagnosis.map((item) => {
- return item.DxEvolutionID;
- }) : null;
+ this.state.data.latestConfirmedProjectDiagnosis.map((item) => {
+ return item.DxEvolutionID;
+ }) : null;
const latestProjectDiagnosisIDs =
this.state.data.latestProjectDiagnosis.length > 0 ?
- this.state.data.latestProjectDiagnosis.map((item) => {
- return item.DxEvolutionID;
- }) : null;
+ this.state.data.latestProjectDiagnosis.map((item) => {
+ return item.DxEvolutionID;
+ }) : null;
// Unset diagnosis in latest project diagnosis list if it also exists
// in the confirmed latest project diagnosis list
@@ -169,8 +169,8 @@ class DiagnosisEvolution extends Component {
if (latestConfirmedProjectDiagnosisIDs != null) {
latestProjectDiagnosisIDs.map((dxEvolutionID, index) => {
if (latestConfirmedProjectDiagnosisIDs.find(
- (element) => (element === dxEvolutionID)
- ) != undefined
+ (element) => (element === dxEvolutionID)
+ ) != undefined
) {
latestProjectDiagnosis.splice(index, 1);
}
diff --git a/modules/candidate_parameters/jsx/ParticipantStatus.js b/modules/candidate_parameters/jsx/ParticipantStatus.js
index 5ec4a683858..ff3a1a1779f 100644
--- a/modules/candidate_parameters/jsx/ParticipantStatus.js
+++ b/modules/candidate_parameters/jsx/ParticipantStatus.js
@@ -140,103 +140,103 @@ class ParticipantStatus extends Component {
updateButton =
;
}
- let required = this.state.Data.required;
- let subOptions = {};
- let suboptionsRequired = false;
- let participantStatus = (
- this.state.formData.participantStatus ?
- this.state.formData.participantStatus :
- this.state.Data.participantStatus
- );
+ let required = this.state.Data.required;
+ let subOptions = {};
+ let suboptionsRequired = false;
+ let participantStatus = (
+ this.state.formData.participantStatus ?
+ this.state.formData.participantStatus :
+ this.state.Data.participantStatus
+ );
- if (participantStatus && required.indexOf(participantStatus) > -1) {
- subOptions = this.state.Data.parentIDs[participantStatus];
- suboptionsRequired = true;
- }
+ if (participantStatus && required.indexOf(participantStatus) > -1) {
+ subOptions = this.state.Data.parentIDs[participantStatus];
+ suboptionsRequired = true;
+ }
- let commentsRequired = false;
- let statusOpts = this.state.Data.statusOptions;
- if (
- statusOpts &&
+ let commentsRequired = false;
+ let statusOpts = this.state.Data.statusOptions;
+ if (
+ statusOpts &&
statusOpts[participantStatus] !== 'Active' &&
statusOpts[participantStatus] !== 'Complete'
- ) {
- commentsRequired = true;
- }
+ ) {
+ commentsRequired = true;
+ }
- let formattedHistory = [];
- for (let statusKey in this.state.Data.history) {
- if (this.state.Data.history.hasOwnProperty(statusKey)) {
- let line = '';
- for (let field in this.state.Data.history[statusKey]) {
- if (this.state.Data.history[statusKey]
- .hasOwnProperty(field)
- ) {
- let current = this.state.Data.history[statusKey][field];
- if (current !== null) {
- switch (field) {
- case 'data_entry_date':
- line += '[';
- line += current;
- line += '] ';
- break;
- case 'entry_staff':
- line += current;
- line += ' ';
- break;
- case 'status':
- line += ' Status: ';
- line += current;
- line += ' ';
- break;
- case 'suboption':
- line += 'Details: ';
- line += current;
- line += ' ';
- break;
- case 'reason_specify':
- line += 'Comments: ';
- line += current;
- line += ' ';
- break;
- default:
- }
- }
- }
- }
- formattedHistory.push(
{line}
);
+ let formattedHistory = [];
+ for (let statusKey in this.state.Data.history) {
+ if (this.state.Data.history.hasOwnProperty(statusKey)) {
+ let line = '';
+ for (let field in this.state.Data.history[statusKey]) {
+ if (this.state.Data.history[statusKey]
+ .hasOwnProperty(field)
+ ) {
+ let current = this.state.Data.history[statusKey][field];
+ if (current !== null) {
+ switch (field) {
+ case 'data_entry_date':
+ line += '[';
+ line += current;
+ line += '] ';
+ break;
+ case 'entry_staff':
+ line += current;
+ line += ' ';
+ break;
+ case 'status':
+ line += ' Status: ';
+ line += current;
+ line += ' ';
+ break;
+ case 'suboption':
+ line += 'Details: ';
+ line += current;
+ line += ' ';
+ break;
+ case 'reason_specify':
+ line += 'Comments: ';
+ line += current;
+ line += ' ';
+ break;
+ default:
+ }
}
+ }
}
+ formattedHistory.push(
{line}
);
+ }
+ }
- let alertMessage = '';
- let alertClass = 'alert text-center hide';
- if (this.state.updateResult) {
- if (this.state.updateResult === 'success') {
- alertClass = 'alert alert-success text-center';
- alertMessage = 'Update Successful!';
- } else if (this.state.updateResult === 'error') {
- let errorMessage = this.state.errorMessage;
- alertClass = 'alert alert-danger text-center';
- alertMessage = errorMessage ?
- errorMessage :
- 'Failed to update!';
- }
- }
+ let alertMessage = '';
+ let alertClass = 'alert text-center hide';
+ if (this.state.updateResult) {
+ if (this.state.updateResult === 'success') {
+ alertClass = 'alert alert-success text-center';
+ alertMessage = 'Update Successful!';
+ } else if (this.state.updateResult === 'error') {
+ let errorMessage = this.state.errorMessage;
+ alertClass = 'alert alert-danger text-center';
+ alertMessage = errorMessage ?
+ errorMessage :
+ 'Failed to update!';
+ }
+ }
- return (
-
-
- {alertMessage}
-
-
-
-
-
+
+ {alertMessage}
+
+
+
+
+
-
+
-
+
- {updateButton}
- {formattedHistory}
-
-
- );
- }
+ />
+ {updateButton}
+ {formattedHistory}
+
+
+ );
+ }
/**
* Handles form submission
diff --git a/modules/candidate_parameters/jsx/ProbandInfo.js b/modules/candidate_parameters/jsx/ProbandInfo.js
index 460838b144a..d50a28537bd 100644
--- a/modules/candidate_parameters/jsx/ProbandInfo.js
+++ b/modules/candidate_parameters/jsx/ProbandInfo.js
@@ -218,56 +218,56 @@ class ProbandInfo extends Component {
let value = this.state.formData[paramTypeID];
switch (extraParameters[key2].Type.substring(0, 3)) {
- case 'enu': {
- let types = extraParameters[key2].Type.substring(5);
- types = types.slice(0, -1);
- types = types.replace(/'/g, '');
- types = types.split(',');
- let selectOptions = {};
- for (let key3 in types) {
- if (types.hasOwnProperty(key3)) {
- selectOptions[types[key3]] = types[key3];
- }
+ case 'enu': {
+ let types = extraParameters[key2].Type.substring(5);
+ types = types.slice(0, -1);
+ types = types.replace(/'/g, '');
+ types = types.split(',');
+ let selectOptions = {};
+ for (let key3 in types) {
+ if (types.hasOwnProperty(key3)) {
+ selectOptions[types[key3]] = types[key3];
}
-
- extraParameterFields.push(
-
- );
- break;
}
- case 'dat':
- extraParameterFields.push(
-
+
+ extraParameterFields.push(
+
+ );
+ break;
+ }
+ case 'dat':
+ extraParameterFields.push(
+
);
- break;
- default:
- extraParameterFields.push(
-
+ break;
+ default:
+ extraParameterFields.push(
+
);
}
}
diff --git a/modules/candidate_profile/jsx/CandidateInfo.js b/modules/candidate_profile/jsx/CandidateInfo.js
index 05f196a5c10..831193da45a 100644
--- a/modules/candidate_profile/jsx/CandidateInfo.js
+++ b/modules/candidate_profile/jsx/CandidateInfo.js
@@ -6,201 +6,201 @@ import PropTypes from 'prop-types';
* CandidateInfo table providing an overview of the candidate.
*/
export class CandidateInfo extends Component {
- /**
- * Construct the object.
- *
- * @param {array} props - The React props
- */
- constructor(props) {
- super(props);
+ /**
+ * Construct the object.
+ *
+ * @param {array} props - The React props
+ */
+ constructor(props) {
+ super(props);
- this.calcAge = this.calcAge.bind(this);
- this.getCohorts = this.getCohorts.bind(this);
- this.getVisitList = this.getVisitList.bind(this);
- }
+ this.calcAge = this.calcAge.bind(this);
+ this.getCohorts = this.getCohorts.bind(this);
+ this.getVisitList = this.getVisitList.bind(this);
+ }
- /**
- * Calculate the age (as of today) based on the date of birth
- * passed as an argument. If the age is less than or equal to
- * 3 years old, it will return a string describing the age in
- * months. Otherwise, it will return an age in years.
- *
- * @param {string} dob - The date of birth in format YYYY-MM-DD
- * @return {string} - A human readable string of the age.
- */
- calcAge(dob) {
- const dobdate = new Date(dob);
- const now = new Date();
- const years = now.getFullYear()-dobdate.getFullYear();
- const months = years*12 + now.getMonth() - dobdate.getMonth();
+ /**
+ * Calculate the age (as of today) based on the date of birth
+ * passed as an argument. If the age is less than or equal to
+ * 3 years old, it will return a string describing the age in
+ * months. Otherwise, it will return an age in years.
+ *
+ * @param {string} dob - The date of birth in format YYYY-MM-DD
+ * @return {string} - A human readable string of the age.
+ */
+ calcAge(dob) {
+ const dobdate = new Date(dob);
+ const now = new Date();
+ const years = now.getFullYear()-dobdate.getFullYear();
+ const months = years*12 + now.getMonth() - dobdate.getMonth();
- if (months <= 36) {
- return months + ' months old';
- }
- return years + ' years old';
+ if (months <= 36) {
+ return months + ' months old';
}
+ return years + ' years old';
+ }
- /**
- * Return a list of the unique cohorts contained in the
- * visits passed.
- *
- * @param {array} visits - An array of visits in the format of
- * the LORIS API
- * @return {array} - The unique list of cohorts as a string.
- */
- getCohorts(visits) {
- let mapped = [...new Set(visits.map( (visit) => {
- return visit.Meta.Battery;
- }))];
- return mapped;
- }
+ /**
+ * Return a list of the unique cohorts contained in the
+ * visits passed.
+ *
+ * @param {array} visits - An array of visits in the format of
+ * the LORIS API
+ * @return {array} - The unique list of cohorts as a string.
+ */
+ getCohorts(visits) {
+ let mapped = [...new Set(visits.map( (visit) => {
+ return visit.Meta.Battery;
+ }))];
+ return mapped;
+ }
- /**
- * Converts the list of visits passed as a parameter to a React element
- * that can be rendered. The React element is a comma separated list
- * of visits, each of which link to the timepoint.
- *
- * The instrument_list is currently used as the 'timepoint' because
- * that's where you end up when going via Access Profile, but eventually
- * it would be a good idea to have a non-modality specific visit dashboard
- * similar to the candidate dashboard.
- *
- * @param {array} visits - List of visits in the format returned by the LORIS API.
- * @return {object} - A React element containing a comma separated list of links.
- */
- getVisitList(visits) {
- let visitlinks = visits.map( (visit) => {
- const sessionID = this.props.VisitMap[visit.Meta.Visit];
- const candID = this.props.Candidate.Meta.CandID;
- return {
+ const sessionID = this.props.VisitMap[visit.Meta.Visit];
+ const candID = this.props.Candidate.Meta.CandID;
+ return {visit.Meta.Visit} ;
- });
- return
- {
- // Equivalent of .join(', ') that doesn't convert the React
- // element into the string [object Object].
- // See https://stackoverflow.com/questions/33577448/is-there-a-way-to-do-array-join-in-react
- visitlinks.reduce(
- (acc, el) => {
- if (acc === null) {
- return [el];
- }
- return [acc, ', ', el];
- },
- null,
- )
+ key={visit.Meta.Visit}
+ >{visit.Meta.Visit};
+ });
+ return
+ {
+ // Equivalent of .join(', ') that doesn't convert the React
+ // element into the string [object Object].
+ // See https://stackoverflow.com/questions/33577448/is-there-a-way-to-do-array-join-in-react
+ visitlinks.reduce(
+ (acc, el) => {
+ if (acc === null) {
+ return [el];
}
-
;
- }
+ return [acc, ', ', el];
+ },
+ null,
+ )
+ }
+
;
+ }
- /**
- * Render the React component
- *
- * @return {object} - The rendered react component
- */
- render() {
- const cohorts = this.getCohorts(this.props.Visits);
- const subprojlabel = cohorts.length == 1 ? 'Cohort'
- : 'Cohorts';
+ /**
+ * Render the React component
+ *
+ * @return {object} - The rendered react component
+ */
+ render() {
+ const cohorts = this.getCohorts(this.props.Visits);
+ const subprojlabel = cohorts.length == 1 ? 'Cohort'
+ : 'Cohorts';
- const data = [
- {
- label: 'PSCID',
- value: this.props.Candidate.Meta.PSCID,
- },
- {
- label: 'DCCID',
- value: this.props.Candidate.Meta.CandID,
- },
- {
- label: 'Date of Birth',
- value: this.props.Candidate.Meta.DoB,
- valueWhitespace: 'nowrap',
- },
- {
- label: 'Age',
- value: this.calcAge(this.props.Candidate.Meta.DoB),
- },
- {
- label: 'Sex',
- value: this.props.Candidate.Meta.Sex,
- },
- {
- label: 'Project',
- value: this.props.Candidate.Meta.Project,
- },
- {
- label: subprojlabel,
- value: cohorts.join(', '),
- },
- {
- label: 'Site',
- value: this.props.Candidate.Meta.Site,
- },
- {
- label: 'Visits',
- value: this.getVisitList(this.props.Visits),
- width: '12em',
- },
- ];
+ const data = [
+ {
+ label: 'PSCID',
+ value: this.props.Candidate.Meta.PSCID,
+ },
+ {
+ label: 'DCCID',
+ value: this.props.Candidate.Meta.CandID,
+ },
+ {
+ label: 'Date of Birth',
+ value: this.props.Candidate.Meta.DoB,
+ valueWhitespace: 'nowrap',
+ },
+ {
+ label: 'Age',
+ value: this.calcAge(this.props.Candidate.Meta.DoB),
+ },
+ {
+ label: 'Sex',
+ value: this.props.Candidate.Meta.Sex,
+ },
+ {
+ label: 'Project',
+ value: this.props.Candidate.Meta.Project,
+ },
+ {
+ label: subprojlabel,
+ value: cohorts.join(', '),
+ },
+ {
+ label: 'Site',
+ value: this.props.Candidate.Meta.Site,
+ },
+ {
+ label: 'Visits',
+ value: this.getVisitList(this.props.Visits),
+ width: '12em',
+ },
+ ];
- const renderTerm = (label, value, info) => {
- const cardStyle = {
- width: info.width || '6em',
- padding: '1em',
- marginLeft: '1ex',
- marginRight: '1ex',
- };
- let valueStyle = {};
- if (info.valueWhitespace) {
- valueStyle.whiteSpace = info.valueWhitespace;
- }
+ const renderTerm = (label, value, info) => {
+ const cardStyle = {
+ width: info.width || '6em',
+ padding: '1em',
+ marginLeft: '1ex',
+ marginRight: '1ex',
+ };
+ let valueStyle = {};
+ if (info.valueWhitespace) {
+ valueStyle.whiteSpace = info.valueWhitespace;
+ }
- return (
-
-
{label}
- {value}
-
- );
- };
- const cardInfo = data.map(
- (info) => renderTerm(info.label, info.value, info)
- );
+ return (
+
+
{label}
+ {value}
+
+ );
+ };
+ const cardInfo = data.map(
+ (info) => renderTerm(info.label, info.value, info)
+ );
- let extrainfo;
- if (this.props.ExtraCandidateInfo.length > 0) {
- // We give extra width for generic terms that we don't
- // know anything about their size, so we err on the
- // side of caution.
- extrainfo = (
-
-
-
- {this.props.ExtraCandidateInfo.map(
- (obj) => renderTerm(
- obj.term,
- obj.value,
- {width: '12em'}
- )
- )}
-
-
- );
- }
- return (
-
-
- {cardInfo}
-
- {extrainfo}
-
- );
+ let extrainfo;
+ if (this.props.ExtraCandidateInfo.length > 0) {
+ // We give extra width for generic terms that we don't
+ // know anything about their size, so we err on the
+ // side of caution.
+ extrainfo = (
+
+
+
+ {this.props.ExtraCandidateInfo.map(
+ (obj) => renderTerm(
+ obj.term,
+ obj.value,
+ {width: '12em'}
+ )
+ )}
+
+
+ );
}
+ return (
+
+
+ {cardInfo}
+
+ {extrainfo}
+
+ );
+ }
}
diff --git a/modules/candidate_profile/test/candidate_profileTest.php b/modules/candidate_profile/test/candidate_profileTest.php
index fc06dc22932..ca0fb25a849 100644
--- a/modules/candidate_profile/test/candidate_profileTest.php
+++ b/modules/candidate_profile/test/candidate_profileTest.php
@@ -12,6 +12,7 @@
* @link https://github.com/aces/Loris
*/
use Facebook\WebDriver\WebDriverBy;
+
require_once __DIR__
. "/../../../test/integrationtests/LorisIntegrationTestWithCandidate.class.inc";
/**
@@ -92,45 +93,23 @@ function testCandidateProfileWithoutProjectPermissions()
$this->resetUserProject();
}
/**
- * Test that the page instrument link works on card 2 section
+ * Test that the page instrument link works on card section
*
* @return void
*/
- function testCandidateProfileInstrumentLink1()
+ function testCandidateProfileInstrumentLink()
{
- $this->safeGet($this->url . "/candidate_profile/900000/");
- $this->safeFindElement(
+ $this->setupPermissions(['superuser']);
+
+ $this->safeGet($this->url . "/candidate_profile/115788/");
+ $this->safeClick(
WebDriverBy::cssSelector(
- "#card2 > div:nth-child(1) >".
- "div:nth-child(1)>div:nth-child(1)>div:nth-child(1)>div:nth-".
- "child(1)> div:nth-child(2) > h4:nth-child(1) > a:nth-child(1)"
+ '#card0 > div > div > div >'.
+ ' dl > div:nth-child(9) > dd > div > a:nth-child(1)'
)
- )->click();
- $bodyText
- = $this->safeFindElement(WebDriverBy::cssSelector("body"))
- ->getText();
- $this->assertStringContainsString(
- "Behavioural Battery of Instruments",
- $bodyText
);
- }
- /**
- * Test that the page instrument link works on card 1 section
- *
- * @return void
- */
- function testCandidateProfileInstrumentLink2()
- {
- $this->safeGet($this->url . "/candidate_profile/900000/");
- $this->safeFindElement(
- WebDriverBy::cssSelector(
- "#card0 > div:nth-child(1) >".
- "div:nth-child(1)>div:nth-child(1)>dl:nth-child(1)>div:nth-".
- "child(9)>dd:nth-child(2) > div:nth-child(1) > a:nth-child(1)"
- )
- )->click();
- $bodyText
- = $this->safeFindElement(WebDriverBy::cssSelector("body"))
+
+ $bodyText = $this->safeFindElement(WebDriverBy::cssSelector("body"))
->getText();
$this->assertStringContainsString(
"Behavioural Battery of Instruments",
diff --git a/modules/configuration/jsx/CohortRelations.js b/modules/configuration/jsx/CohortRelations.js
index 52a4ee99dcc..a5564a19254 100644
--- a/modules/configuration/jsx/CohortRelations.js
+++ b/modules/configuration/jsx/CohortRelations.js
@@ -32,13 +32,13 @@ class CohortRelations extends Component {
});
return (
-
- );
+
+ );
}
}
diff --git a/modules/configuration/jsx/DiagnosisEvolution.js b/modules/configuration/jsx/DiagnosisEvolution.js
index 76b0a7ccc57..c71de4e3e62 100644
--- a/modules/configuration/jsx/DiagnosisEvolution.js
+++ b/modules/configuration/jsx/DiagnosisEvolution.js
@@ -19,566 +19,566 @@ import {
* Candidate diagnosis evolution component
*/
class DiagnosisEvolution extends Component {
- /**
- * @constructor
- * @param {object} props - React Component properties
- */
- constructor(props) {
- super(props);
-
- this.state = {
- data: [],
- formData: {
- new: {
- DxEvolutionID: 'new',
- Name: null,
- ProjectID: null,
- instrumentName: null,
- sourceField: null,
- visitLabel: null,
- pendingSourceField: null,
- },
- },
- errorMessage: {},
- error: false,
- isLoaded: false,
- currentTab: 'new',
- };
-
- this.fetchData = this.fetchData.bind(this);
- this.setFormData = this.setFormData.bind(this);
- this.validate = this.validate.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleReset = this.handleReset.bind(this);
- this.confirmDelete = this.confirmDelete.bind(this);
- this.handleDelete = this.handleDelete.bind(this);
- this.addSourceField = this.addSourceField.bind(this);
- this.removeSourceField = this.removeSourceField.bind(this);
+ /**
+ * @constructor
+ * @param {object} props - React Component properties
+ */
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ data: [],
+ formData: {
+ new: {
+ DxEvolutionID: 'new',
+ Name: null,
+ ProjectID: null,
+ instrumentName: null,
+ sourceField: null,
+ visitLabel: null,
+ pendingSourceField: null,
+ },
+ },
+ errorMessage: {},
+ error: false,
+ isLoaded: false,
+ currentTab: 'new',
+ };
+
+ this.fetchData = this.fetchData.bind(this);
+ this.setFormData = this.setFormData.bind(this);
+ this.validate = this.validate.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleReset = this.handleReset.bind(this);
+ this.confirmDelete = this.confirmDelete.bind(this);
+ this.handleDelete = this.handleDelete.bind(this);
+ this.addSourceField = this.addSourceField.bind(this);
+ this.removeSourceField = this.removeSourceField.bind(this);
+ }
+
+ /**
+ * Called by React when the component has been rendered on the page.
+ */
+ componentDidMount() {
+ this.fetchData()
+ .then(() => this.setState({isLoaded: true}));
+ }
+
+ /**
+ * Fetch data
+ *
+ * @return {Promise}
+ */
+ fetchData() {
+ return fetch(this.props.dataURL, {credentials: 'same-origin'})
+ .then((resp) => resp.json())
+ .then((data) => this.setState({
+ data: data,
+ formData: JSON.parse(JSON.stringify({
+ ...this.state.formData,
+ ...data,
+ })),
+ }))
+ .catch((error) => {
+ this.setState({error: true});
+ console.error(error);
+ });
+ }
+
+ /**
+ * Set form data
+ *
+ * @param {string} formElement
+ * @param {*} value
+ */
+ setFormData(formElement, value) {
+ const tabID = this.state.currentTab;
+ let formData = this.state.formData;
+
+ if (tabID == 'new') {
+ let tabData = {
+ ...formData.new,
+ [formElement]: value,
+ };
+ formData.new = tabData;
+ } else {
+ let tabData = {
+ ...formData.diagnosisTracks[tabID],
+ [formElement]: value,
+ };
+ formData.diagnosisTracks[tabID] = tabData;
}
- /**
- * Called by React when the component has been rendered on the page.
- */
- componentDidMount() {
- this.fetchData()
- .then(() => this.setState({isLoaded: true}));
+ let errorMessage = {};
+ errorMessage[tabID] = this.state.errorMessage[tabID] ?
+ this.state.errorMessage[tabID] :
+ {
+ Name: null,
+ ProjectID: null,
+ visitLabel: null,
+ instrumentName: null,
+ sourceField: null,
+ orderNumber: null,
+ };
+ if (!!(errorMessage[tabID][formElement])
+ && (!!value || value !== '' || !!value.length)
+ ) {
+ errorMessage[tabID][formElement] = null;
}
- /**
- * Fetch data
- *
- * @return {Promise}
- */
- fetchData() {
- return fetch(this.props.dataURL, {credentials: 'same-origin'})
- .then((resp) => resp.json())
- .then((data) => this.setState({
- data: data,
- formData: JSON.parse(JSON.stringify({
- ...this.state.formData,
- ...data,
- })),
- }))
- .catch((error) => {
- this.setState({error: true});
- console.error(error);
- });
+ this.setState({formData, errorMessage});
+ }
+
+ /**
+ * renders the diagnosis trajectory form
+ *
+ * @param {number} dxEvolutionID
+ * @return {JSX} React markup for the component
+ */
+ renderDiagnosisForm(dxEvolutionID) {
+ const id = typeof dxEvolutionID !== 'undefined' ?
+ dxEvolutionID : this.state.currentTab;
+ const trajectoryData = id == 'new' ?
+ this.state.formData.new :
+ this.state.formData.diagnosisTracks[id];
+ const deleteButton = id !== 'new' ?
+ (
+
+ ) : null;
+ const errorMessage = this.state.errorMessage[id] ?
+ this.state.errorMessage[id] :
+ {
+ Name: null,
+ ProjectID: null,
+ visitLabel: null,
+ instrumentName: null,
+ sourceField: null,
+ orderNumber: null,
+ };
+
+ return (
+
+
+
Diagnosis Evolution
+
+
+
+
+
+
+
+
+
+
+
+
+ {deleteButton}
+
+
+
+
+
+ );
+ }
+
+ /**
+ * Renders the React component.
+ *
+ * @return {JSX} - React markup for the component
+ */
+ render() {
+ if (this.state.error) {
+ return An error occured while loading the page. ;
}
- /**
- * Set form data
- *
- * @param {string} formElement
- * @param {*} value
- */
- setFormData(formElement, value) {
- const tabID = this.state.currentTab;
- let formData = this.state.formData;
-
- if (tabID == 'new') {
- let tabData = {
- ...formData.new,
- [formElement]: value,
- };
- formData.new = tabData;
- } else {
- let tabData = {
- ...formData.diagnosisTracks[tabID],
- [formElement]: value,
- };
- formData.diagnosisTracks[tabID] = tabData;
- }
-
- let errorMessage = {};
- errorMessage[tabID] = this.state.errorMessage[tabID] ?
- this.state.errorMessage[tabID] :
- {
- Name: null,
- ProjectID: null,
- visitLabel: null,
- instrumentName: null,
- sourceField: null,
- orderNumber: null,
- };
- if (!!(errorMessage[tabID][formElement])
- && (!!value || value !== '' || !!value.length)
- ) {
- errorMessage[tabID][formElement] = null;
- }
-
- this.setState({formData, errorMessage});
+ if (!this.state.isLoaded) {
+ return ;
}
- /**
- * renders the diagnosis trajectory form
- *
- * @param {number} dxEvolutionID
- * @return {JSX} React markup for the component
- */
- renderDiagnosisForm(dxEvolutionID) {
- const id = typeof dxEvolutionID !== 'undefined' ?
- dxEvolutionID : this.state.currentTab;
- const trajectoryData = id == 'new' ?
- this.state.formData.new :
- this.state.formData.diagnosisTracks[id];
- const deleteButton = id !== 'new' ?
- (
-
- ) : null;
- const errorMessage = this.state.errorMessage[id] ?
- this.state.errorMessage[id] :
- {
- Name: null,
- ProjectID: null,
- visitLabel: null,
- instrumentName: null,
- sourceField: null,
- orderNumber: null,
- };
-
- return (
-
-
-
Diagnosis Evolution
-
-
-
-
-
-
-
-
-
-
-
-
- {deleteButton}
-
-
-
-
-
- );
+ let tabList = [];
+ tabList.push({id: 'new', label: 'New Diagnosis Trajectory'});
+
+ let diagnosisTracks = [];
+ const trajectories = this.state.data.diagnosisTracks;
+ if (trajectories) {
+ Object.values(trajectories).map((trajectory) => {
+ const dxID = trajectory.DxEvolutionID;
+ const dxName = trajectory.Name;
+ diagnosisTracks.push(this.renderDiagnosisForm(dxID));
+ tabList.push({id: `${dxID}`, label: dxName});
+ });
}
- /**
- * Renders the React component.
- *
- * @return {JSX} - React markup for the component
- */
- render() {
- if (this.state.error) {
- return An error occured while loading the page. ;
- }
-
- if (!this.state.isLoaded) {
- return ;
- }
-
- let tabList = [];
- tabList.push({id: 'new', label: 'New Diagnosis Trajectory'});
-
- let diagnosisTracks = [];
- const trajectories = this.state.data.diagnosisTracks;
- if (trajectories) {
- Object.values(trajectories).map((trajectory) => {
- const dxID = trajectory.DxEvolutionID;
- const dxName = trajectory.Name;
- diagnosisTracks.push(this.renderDiagnosisForm(dxID));
- tabList.push({id: `${dxID}`, label: dxName});
- });
- }
-
- return (
-
-
+ return (
+
+
Use this page to manage the configuration of the study's
diagnosis trajectory.
-
-
this.setState({currentTab: tabId})}
- >
- {this.state.isLoaded && this.renderDiagnosisForm('new')}
- {diagnosisTracks}
-
-
- );
+
+
this.setState({currentTab: tabId})}
+ >
+ {this.state.isLoaded && this.renderDiagnosisForm('new')}
+ {diagnosisTracks}
+
+
+ );
+ }
+
+ /**
+ * Sets required field errors
+ *
+ * @param {object} formData - Form data
+ * @param {string} tabID - Relevant tab
+ * @return {boolean}
+ */
+ validate(formData, tabID) {
+ let isValid = true;
+
+ let errorMessage = this.state.errorMessage;
+ errorMessage[tabID] = {
+ Name: null,
+ ProjectID: null,
+ visitLabel: null,
+ instrumentName: null,
+ sourceField: null,
+ orderNumber: null,
+ };
+
+ if (!formData.Name) {
+ errorMessage[tabID]['Name'] = 'This field is required!';
+ isValid = false;
}
-
- /**
- * Sets required field errors
- *
- * @param {object} formData - Form data
- * @param {string} tabID - Relevant tab
- * @return {boolean}
- */
- validate(formData, tabID) {
- let isValid = true;
-
- let errorMessage = this.state.errorMessage;
- errorMessage[tabID] = {
- Name: null,
- ProjectID: null,
- visitLabel: null,
- instrumentName: null,
- sourceField: null,
- orderNumber: null,
- };
-
- if (!formData.Name) {
- errorMessage[tabID]['Name'] = 'This field is required!';
- isValid = false;
- }
- if (!formData.ProjectID) {
- errorMessage[tabID]['ProjectID'] = 'This field is required!' +
+ if (!formData.ProjectID) {
+ errorMessage[tabID]['ProjectID'] = 'This field is required!' +
' Entry must be included in provided list of options.';
- isValid = false;
- }
- if (!formData.visitLabel) {
- errorMessage[tabID]['visitLabel'] = 'This field is required!' +
+ isValid = false;
+ }
+ if (!formData.visitLabel) {
+ errorMessage[tabID]['visitLabel'] = 'This field is required!' +
' Entry must be included in provided list of options.';
- isValid = false;
- }
- if (!formData.instrumentName) {
- errorMessage[tabID]['instrumentName'] = 'This field is required!' +
+ isValid = false;
+ }
+ if (!formData.instrumentName) {
+ errorMessage[tabID]['instrumentName'] = 'This field is required!' +
' Entry must be included in provided list of options.';
- isValid = false;
- }
- if (!formData.sourceField || !formData.sourceField.length) {
- errorMessage[tabID]['sourceField'] = 'This field is required!' +
+ isValid = false;
+ }
+ if (!formData.sourceField || !formData.sourceField.length) {
+ errorMessage[tabID]['sourceField'] = 'This field is required!' +
' Please click "Add Field" before saving.';
- isValid = false;
- }
- if (!formData.orderNumber) {
- errorMessage[tabID]['orderNumber'] = 'This field is required!';
- isValid = false;
- }
-
- this.setState({errorMessage});
- return isValid;
+ isValid = false;
+ }
+ if (!formData.orderNumber) {
+ errorMessage[tabID]['orderNumber'] = 'This field is required!';
+ isValid = false;
}
+ this.setState({errorMessage});
+ return isValid;
+ }
+
+
+ /**
+ * Handles form submission
+ *
+ * @param {event} e - Form submission event
+ */
+ handleSubmit(e) {
+ e.preventDefault();
+
+ const tabID = this.state.currentTab;
+ let formData = tabID == 'new' ?
+ this.state.formData.new :
+ this.state.formData.diagnosisTracks[tabID];
+ let formObject = new FormData();
+ for (let key in formData) {
+ if (formData[key] !== '') {
+ formObject.append(key, formData[key]);
+ }
+ }
+ if (!this.validate(formData, tabID)) {
+ return;
+ }
- /**
- * Handles form submission
- *
- * @param {event} e - Form submission event
- */
- handleSubmit(e) {
- e.preventDefault();
-
- const tabID = this.state.currentTab;
- let formData = tabID == 'new' ?
- this.state.formData.new :
- this.state.formData.diagnosisTracks[tabID];
- let formObject = new FormData();
- for (let key in formData) {
- if (formData[key] !== '') {
- formObject.append(key, formData[key]);
- }
- }
- if (!this.validate(formData, tabID)) {
- return;
- }
-
- fetch(this.props.submitURL, {
- method: 'POST',
- cache: 'no-cache',
- credentials: 'same-origin',
- body: formObject,
- }).then((resp) => {
- if (resp.ok) {
- swal.fire({
- title: 'Submission Successful!',
- type: 'success',
- });
- window.location.href =
+ fetch(this.props.submitURL, {
+ method: 'POST',
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ body: formObject,
+ }).then((resp) => {
+ if (resp.ok) {
+ swal.fire({
+ title: 'Submission Successful!',
+ type: 'success',
+ });
+ window.location.href =
`${loris.BaseURL}/configuration/diagnosis_evolution`;
- } else {
- resp.json().then((msg) => {
- let status = resp.status == 409 ?
- 'Conflict!' : 'Error!';
- swal.fire({
- title: status,
- text: msg.error,
- type: 'error',
- });
- });
- }
- }).catch((error) => {
- console.warn(error);
+ } else {
+ resp.json().then((msg) => {
+ let status = resp.status == 409 ?
+ 'Conflict!' : 'Error!';
+ swal.fire({
+ title: status,
+ text: msg.error,
+ type: 'error',
+ });
});
- }
-
- /**
- * Handles form reset
- *
- * @param {event} e - Form submission event
- */
- handleReset(e) {
- e.preventDefault();
- const tabID = this.state.currentTab;
- let formData = this.state.formData;
- if (tabID === 'new') {
- let formDataNew = formData[tabID];
- for (let key in formDataNew) {
- if (key !== 'DxEvolutionID') {
- formDataNew[key] = null;
- }
- }
- formData[tabID] = formDataNew;
- } else {
- formData.diagnosisTracks[tabID]
- = JSON.parse(JSON.stringify(
- this.state.data.diagnosisTracks[tabID]
- ));
+ }
+ }).catch((error) => {
+ console.warn(error);
+ });
+ }
+
+ /**
+ * Handles form reset
+ *
+ * @param {event} e - Form submission event
+ */
+ handleReset(e) {
+ e.preventDefault();
+ const tabID = this.state.currentTab;
+ let formData = this.state.formData;
+ if (tabID === 'new') {
+ let formDataNew = formData[tabID];
+ for (let key in formDataNew) {
+ if (key !== 'DxEvolutionID') {
+ formDataNew[key] = null;
}
-
- let errorMessage = {};
- errorMessage[tabID] = {
- Name: null,
- ProjectID: null,
- visitLabel: null,
- instrumentName: null,
- sourceField: null,
- orderNumber: null,
- };
-
- this.setState({formData, errorMessage});
+ }
+ formData[tabID] = formDataNew;
+ } else {
+ formData.diagnosisTracks[tabID]
+ = JSON.parse(JSON.stringify(
+ this.state.data.diagnosisTracks[tabID]
+ ));
}
- /**
- * Swal for user to confirm deletion
- *
- * @param {event} e - Form submission event
- */
- confirmDelete(e) {
- e.preventDefault();
-
+ let errorMessage = {};
+ errorMessage[tabID] = {
+ Name: null,
+ ProjectID: null,
+ visitLabel: null,
+ instrumentName: null,
+ sourceField: null,
+ orderNumber: null,
+ };
+
+ this.setState({formData, errorMessage});
+ }
+
+ /**
+ * Swal for user to confirm deletion
+ *
+ * @param {event} e - Form submission event
+ */
+ confirmDelete(e) {
+ e.preventDefault();
+
+ swal.fire({
+ title: 'Are you sure you want to delete this diagnosis trajectory?',
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonText: 'Delete',
+ cancelButtonText: 'Cancel',
+ }).then((result) => {
+ if (result.value) {
+ this.handleDelete();
+ }
+ });
+ }
+
+ /**
+ * Handles diagnosis delete
+ */
+ handleDelete() {
+ const tabID = this.state.currentTab;
+
+ let diagnosisTracks = this.state.formData.diagnosisTracks;
+ let ID = diagnosisTracks[tabID]['DxEvolutionID'];
+
+ fetch(this.props.submitURL + '/?ID='+ ID, {
+ method: 'DELETE',
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ }).then((resp) => {
+ if (resp.ok) {
swal.fire({
- title: 'Are you sure you want to delete this diagnosis trajectory?',
- type: 'warning',
- showCancelButton: true,
- confirmButtonText: 'Delete',
- cancelButtonText: 'Cancel',
- }).then((result) => {
- if (result.value) {
- this.handleDelete();
- }
+ title: 'Deletion Successful!',
+ type: 'success',
});
- }
-
- /**
- * Handles diagnosis delete
- */
- handleDelete() {
- const tabID = this.state.currentTab;
-
- let diagnosisTracks = this.state.formData.diagnosisTracks;
- let ID = diagnosisTracks[tabID]['DxEvolutionID'];
-
- fetch(this.props.submitURL + '/?ID='+ ID, {
- method: 'DELETE',
- cache: 'no-cache',
- credentials: 'same-origin',
- }).then((resp) => {
- if (resp.ok) {
- swal.fire({
- title: 'Deletion Successful!',
- type: 'success',
- });
- window.location.href =
+ window.location.href =
`${loris.BaseURL}/configuration/diagnosis_evolution`;
- } else {
- resp.json().then((msg) => {
- let status = resp.status == 409 ?
- 'Conflict!' : 'Error!';
- swal.fire({
- title: status,
- text: msg.error,
- type: 'error',
- });
- });
- }
- }).catch((error) => {
- console.warn(error);
+ } else {
+ resp.json().then((msg) => {
+ let status = resp.status == 409 ?
+ 'Conflict!' : 'Error!';
+ swal.fire({
+ title: status,
+ text: msg.error,
+ type: 'error',
+ });
});
- }
-
- /**
- * Add source field
- *
- * @param {*} formElement
- * @param {string} value
- * @param {*} pendingValKey
- * @param {*} id
- */
- addSourceField(formElement, value, pendingValKey) {
- const tabID = this.state.currentTab;
- let formData = this.state.formData;
-
- if (tabID == 'new') {
- let listItems = formData.new[formElement] || [];
- listItems.push(value);
- formData.new[formElement] = listItems;
- formData.new[pendingValKey] = null;
- } else {
- let listItems =
+ }
+ }).catch((error) => {
+ console.warn(error);
+ });
+ }
+
+ /**
+ * Add source field
+ *
+ * @param {*} formElement
+ * @param {string} value
+ * @param {*} pendingValKey
+ * @param {*} id
+ */
+ addSourceField(formElement, value, pendingValKey) {
+ const tabID = this.state.currentTab;
+ let formData = this.state.formData;
+
+ if (tabID == 'new') {
+ let listItems = formData.new[formElement] || [];
+ listItems.push(value);
+ formData.new[formElement] = listItems;
+ formData.new[pendingValKey] = null;
+ } else {
+ let listItems =
formData.diagnosisTracks[tabID][formElement] || [];
- listItems.push(value);
- formData.diagnosisTracks[tabID][formElement] = listItems;
- formData.diagnosisTracks[tabID][pendingValKey] = null;
- }
+ listItems.push(value);
+ formData.diagnosisTracks[tabID][formElement] = listItems;
+ formData.diagnosisTracks[tabID][pendingValKey] = null;
+ }
- let errorMessage = {};
- errorMessage[tabID] = this.state.errorMessage[tabID] ?
- this.state.errorMessage[tabID] :
- {
- Name: null,
- ProjectID: null,
- visitLabel: null,
- instrumentName: null,
- sourceField: null,
- orderNumber: null,
- };
- if (!!(errorMessage[tabID][formElement])
+ let errorMessage = {};
+ errorMessage[tabID] = this.state.errorMessage[tabID] ?
+ this.state.errorMessage[tabID] :
+ {
+ Name: null,
+ ProjectID: null,
+ visitLabel: null,
+ instrumentName: null,
+ sourceField: null,
+ orderNumber: null,
+ };
+ if (!!(errorMessage[tabID][formElement])
&& (!!value || value !== '' || !!value.length)
- ) {
- errorMessage[tabID][formElement] = null;
- }
-
- this.setState({formData, errorMessage});
+ ) {
+ errorMessage[tabID][formElement] = null;
}
- /**
- * Remove source field
- *
- * @param {*} formElement
- * @param {string} value
- * @param {*} pendingValKey
- */
- removeSourceField(formElement, value) {
- const tabID = this.state.currentTab;
- let formData = this.state.formData;
-
- if (tabID == 'new') {
- let listItems = formData.new[formElement];
- let index = listItems.indexOf(value);
- if (index > -1) {
- listItems.splice(index, 1);
- }
- formData.new[formElement] = listItems;
- } else {
- let listItems =
+ this.setState({formData, errorMessage});
+ }
+
+ /**
+ * Remove source field
+ *
+ * @param {*} formElement
+ * @param {string} value
+ * @param {*} pendingValKey
+ */
+ removeSourceField(formElement, value) {
+ const tabID = this.state.currentTab;
+ let formData = this.state.formData;
+
+ if (tabID == 'new') {
+ let listItems = formData.new[formElement];
+ let index = listItems.indexOf(value);
+ if (index > -1) {
+ listItems.splice(index, 1);
+ }
+ formData.new[formElement] = listItems;
+ } else {
+ let listItems =
formData.diagnosisTracks[tabID][formElement];
- let index = listItems.indexOf(value);
- if (index > -1) {
- listItems.splice(index, 1);
- }
- formData.diagnosisTracks[tabID][formElement] = listItems;
- }
- this.setState({formData: formData});
+ let index = listItems.indexOf(value);
+ if (index > -1) {
+ listItems.splice(index, 1);
+ }
+ formData.diagnosisTracks[tabID][formElement] = listItems;
}
+ this.setState({formData: formData});
+ }
}
DiagnosisEvolution.propTypes = {
@@ -589,13 +589,13 @@ DiagnosisEvolution.propTypes = {
};
window.addEventListener('load', () => {
- const root = createRoot(
- document.getElementById('lorisworkspace')
- );
- root.render(
-
- );
+ const root = createRoot(
+ document.getElementById('lorisworkspace')
+ );
+ root.render(
+
+ );
});
diff --git a/modules/configuration/jsx/configuration_helper.js b/modules/configuration/jsx/configuration_helper.js
index 45c6aacc3e8..010d406bea7 100644
--- a/modules/configuration/jsx/configuration_helper.js
+++ b/modules/configuration/jsx/configuration_helper.js
@@ -1,130 +1,130 @@
import swal from 'sweetalert2';
$(function() {
- 'use strict';
+ 'use strict';
- $('div').tooltip();
+ $('div').tooltip();
- let count = 0;
- $('.add').on('click', function(e) {
- e.preventDefault();
+ let count = 0;
+ $('.add').on('click', function(e) {
+ e.preventDefault();
- count = count + 1;
+ count = count + 1;
- // Field that will be copied
- let currentField = $(this).parent().find('.entry:first-child');
+ // Field that will be copied
+ let currentField = $(this).parent().find('.entry:first-child');
- let id = $(currentField).parent().attr('id');
- let name = 'add-' + id + '-' + count;
+ let id = $(currentField).parent().attr('id');
+ let name = 'add-' + id + '-' + count;
- // Setup the new form field
- let newField = currentField.clone();
- newField.find('.form-control').attr('name', name);
- $(newField).find('.btn-remove')
- .addClass('remove-new')
- .removeClass('btn-remove');
- resetForm(newField);
+ // Setup the new form field
+ let newField = currentField.clone();
+ newField.find('.form-control').attr('name', name);
+ $(newField).find('.btn-remove')
+ .addClass('remove-new')
+ .removeClass('btn-remove');
+ resetForm(newField);
- newField.appendTo($(this).parent().children(':first'));
- });
+ newField.appendTo($(this).parent().children(':first'));
+ });
- $('body').on('click', '.remove-new', function() {
- if ($(this).parent().parent().parent().children().length > 1) {
- $(this).parent().parent().remove();
- } else {
- resetForm($(this).parent().parent());
- }
- });
+ $('body').on('click', '.remove-new', function() {
+ if ($(this).parent().parent().parent().children().length > 1) {
+ $(this).parent().parent().remove();
+ } else {
+ resetForm($(this).parent().parent());
+ }
+ });
- $('.btn-remove').on('click', function(e) {
- e.preventDefault();
+ $('.btn-remove').on('click', function(e) {
+ e.preventDefault();
- let selectedOption = $(this).parent().parent().children()
- .prop('value');
+ let selectedOption = $(this).parent().parent().children()
+ .prop('value');
- let fieldName = $(this)
- .parent().parent().parent().parent().parent().children()
- .attr('data-original-title');
+ let fieldName = $(this)
+ .parent().parent().parent().parent().parent().children()
+ .attr('data-original-title');
- swal.fire({
- text: 'Please confirm you want to delete the option "' +
+ swal.fire({
+ text: 'Please confirm you want to delete the option "' +
selectedOption
+ '" of the field "' + fieldName + '".',
- type: 'warning',
- showCancelButton: true,
- confirmButtonText: 'Confirm',
- cancelButtonText: 'Cancel',
- }).then((result) => {
- if (result.value) {
- let id = $(this).attr('name');
- let button = this;
-
- $.ajax({
- type: 'post',
- url: loris.BaseURL + '/configuration/ajax/process.php',
- data: {remove: id},
- success: function() {
- if ($(button)
- .parent().parent().parent().children()
- .length > 1
- ) {
- $(button).parent().parent().remove();
- } else {
- let parentID = $(button)
- .parent().parent().parent()
- .attr('id');
- let name = 'add-' + parentID;
-
- resetForm($(button).parent().parent());
- $(button)
- .parent().parent().children('.form-control')
- .attr('name', name);
- $(button)
- .addClass('remove-new')
- .removeClass('btn-remove');
- }
- },
- error: function(xhr, desc, err) {
- console.error(xhr);
- console.error('Details: ' + desc + '\nError:' + err);
- },
- });
- }
- });
- });
-
- // On form submit, process the changes through an AJAX call
- $('form').on('submit', function(e) {
- e.preventDefault();
-
- let form = $(this).serialize();
-
- // Clear previous feedback
- $('.submit-area > label').remove();
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonText: 'Confirm',
+ cancelButtonText: 'Cancel',
+ }).then((result) => {
+ if (result.value) {
+ let id = $(this).attr('name');
+ let button = this;
$.ajax({
- type: 'post',
- url: loris.BaseURL + '/configuration/ajax/process.php',
- data: form,
- success: function() {
- let html = 'Submitted ';
- $(html)
- .hide()
- .appendTo('.submit-area')
- .fadeIn(500).delay(1000).fadeOut(500);
- location.reload();
- },
- error: function(xhr, desc, err) {
- let html = '' + xhr.responseText + ' ';
- $(html).hide().appendTo('.submit-area').fadeIn(500).delay(1000);
- },
+ type: 'post',
+ url: loris.BaseURL + '/configuration/ajax/process.php',
+ data: {remove: id},
+ success: function() {
+ if ($(button)
+ .parent().parent().parent().children()
+ .length > 1
+ ) {
+ $(button).parent().parent().remove();
+ } else {
+ let parentID = $(button)
+ .parent().parent().parent()
+ .attr('id');
+ let name = 'add-' + parentID;
+
+ resetForm($(button).parent().parent());
+ $(button)
+ .parent().parent().children('.form-control')
+ .attr('name', name);
+ $(button)
+ .addClass('remove-new')
+ .removeClass('btn-remove');
+ }
+ },
+ error: function(xhr, desc, err) {
+ console.error(xhr);
+ console.error('Details: ' + desc + '\nError:' + err);
+ },
});
+ }
});
-
- // On form reset, to delete the elements added with the "Add field" button that were not submitted.
- $('form').on('reset', function(e) {
- $('.tab-pane.active').find('select[name^="add-"]').parent().remove();
+ });
+
+ // On form submit, process the changes through an AJAX call
+ $('form').on('submit', function(e) {
+ e.preventDefault();
+
+ let form = $(this).serialize();
+
+ // Clear previous feedback
+ $('.submit-area > label').remove();
+
+ $.ajax({
+ type: 'post',
+ url: loris.BaseURL + '/configuration/ajax/process.php',
+ data: form,
+ success: function() {
+ let html = 'Submitted ';
+ $(html)
+ .hide()
+ .appendTo('.submit-area')
+ .fadeIn(500).delay(1000).fadeOut(500);
+ location.reload();
+ },
+ error: function(xhr, desc, err) {
+ let html = '' + xhr.responseText + ' ';
+ $(html).hide().appendTo('.submit-area').fadeIn(500).delay(1000);
+ },
});
+ });
+
+ // On form reset, to delete the elements added with the "Add field" button that were not submitted.
+ $('form').on('reset', function(e) {
+ $('.tab-pane.active').find('select[name^="add-"]').parent().remove();
+ });
});
/*
@@ -142,11 +142,11 @@ function validate(form) {
* @param {Element} form A DOM form element
*/
function resetForm(form) {
- 'use strict';
+ 'use strict';
- $(form).find(
- 'input:text, input:password, input:file, select, textarea'
- ).val('');
- $(form).find('input:radio, input:checkbox')
- .removeAttr('checked').removeAttr('selected');
+ $(form).find(
+ 'input:text, input:password, input:file, select, textarea'
+ ).val('');
+ $(form).find('input:radio, input:checkbox')
+ .removeAttr('checked').removeAttr('selected');
}
diff --git a/modules/configuration/php/cohort.class.inc b/modules/configuration/php/cohort.class.inc
index a9d29a71ee7..d79835185df 100644
--- a/modules/configuration/php/cohort.class.inc
+++ b/modules/configuration/php/cohort.class.inc
@@ -63,6 +63,11 @@ class Cohort extends \NDB_Form
'1' => 'Yes',
'0' => 'No',
];
+ $this->tpl_data['useDoBOptions']
+ = [
+ '1' => 'Yes',
+ '0' => 'No',
+ ];
$this->tpl_data['WindowDifferenceOptions']
= [
diff --git a/modules/conflict_resolver/jsx/CandidateConflictsWidget.js b/modules/conflict_resolver/jsx/CandidateConflictsWidget.js
index 667f306a8d8..36f1abf1a03 100644
--- a/modules/conflict_resolver/jsx/CandidateConflictsWidget.js
+++ b/modules/conflict_resolver/jsx/CandidateConflictsWidget.js
@@ -11,68 +11,68 @@ import PropTypes from 'prop-types';
* @return {object}
*/
function CandidateConflictsWidget(props) {
- const visits = getVisits(props.Conflicts);
- const instruments = getInstruments(props.Conflicts);
+ const visits = getVisits(props.Conflicts);
+ const instruments = getInstruments(props.Conflicts);
- useEffect(() => {
- c3.generate({
- bindto: '#conflictschart',
- data: {
- columns: getDataBreakdown(visits, instruments, props.Conflicts),
- type: 'bar',
- onclick: function(d, el) {
- // If the user clicked on a bar in the chart, redirect to
- // the specific instrument/visit for this candid.
- window.location = props.BaseURL + '/conflict_resolver/'
+ useEffect(() => {
+ c3.generate({
+ bindto: '#conflictschart',
+ data: {
+ columns: getDataBreakdown(visits, instruments, props.Conflicts),
+ type: 'bar',
+ onclick: function(d, el) {
+ // If the user clicked on a bar in the chart, redirect to
+ // the specific instrument/visit for this candid.
+ window.location = props.BaseURL + '/conflict_resolver/'
+ '?visitLabel=' + visits[d.index]
+ '&instrument=' + d.id
+ '&candidateID=' + props.Candidate.Meta.CandID;
- },
- },
- axis: {
- x: {
- type: 'category',
- categories: visits,
- label: {
- text: 'Visit',
- position: 'outer-center',
- },
- },
- y: {
- label: {
- position: 'outer-middle',
- text: 'Number of Conflicts',
- },
- },
- },
- legend: {
- item: {
- onclick: function(id) {
- // If the user clicked on the legend, redirect to the
- // conflict resolver for that instrument across all
- // visits
- window.location = props.BaseURL + '/conflict_resolver/'
+ },
+ },
+ axis: {
+ x: {
+ type: 'category',
+ categories: visits,
+ label: {
+ text: 'Visit',
+ position: 'outer-center',
+ },
+ },
+ y: {
+ label: {
+ position: 'outer-middle',
+ text: 'Number of Conflicts',
+ },
+ },
+ },
+ legend: {
+ item: {
+ onclick: function(id) {
+ // If the user clicked on the legend, redirect to the
+ // conflict resolver for that instrument across all
+ // visits
+ window.location = props.BaseURL + '/conflict_resolver/'
+ '?instrument=' + id
+ '&candidateID=' + props.Candidate.Meta.CandID;
- },
- },
- },
- });
+ },
+ },
+ },
});
+ });
- return
-
-
-
- {'Click on instrument in legend to visit conflict resolver '
+ return
+
+
+
+ {'Click on instrument in legend to visit conflict resolver '
+ 'for that instrument across all visits.'}
-
-
- {'Click on bar in graph to visit conflict resolver '
+
+
+ {'Click on bar in graph to visit conflict resolver '
+ 'for that visit and instrument combination.'}
-
-
-
;
+
+
+
;
}
CandidateConflictsWidget.propTypes = {
Conflicts: PropTypes.array,
@@ -87,11 +87,11 @@ CandidateConflictsWidget.propTypes = {
* @return {array}
*/
function getVisits(data) {
- let visits = {};
- for (const row of Object.values(data)) {
- visits[row.Visit_label] = true;
- }
- return Object.keys(visits);
+ let visits = {};
+ for (const row of Object.values(data)) {
+ visits[row.Visit_label] = true;
+ }
+ return Object.keys(visits);
}
/**
@@ -101,11 +101,11 @@ function getVisits(data) {
* @return {array}
*/
function getInstruments(data) {
- let visits = {};
- for (const row of Object.values(data)) {
- visits[row.Test_name] = true;
- }
- return Object.keys(visits);
+ let visits = {};
+ for (const row of Object.values(data)) {
+ visits[row.Test_name] = true;
+ }
+ return Object.keys(visits);
}
/**
@@ -118,36 +118,36 @@ function getInstruments(data) {
* @return {array} - an array suitable for an C3 data key
*/
function getDataBreakdown(visits, instruments, conflicts) {
- let odata = {};
- // The data needs to be in the format:
- // ['instrument1', v1val, v2val, v3val],
- // ['instrument2', v1val, v2val, v3val]
- // etc.
- // First we convert the conflicts from the format returned
- // from the DB of [VisitLabel, TestName, Count] (sparsely
- // populated if count is 0) into an object so we can easily
- // look up the value, then we go through the list of instruments
- // and populate an array to return to C3.
- for (let i = 0; i < conflicts.length; i++) {
- const conflict = conflicts[i];
- if (!odata[conflict.Test_name]) {
- odata[conflict.Test_name] = {};
- }
- odata[conflict.Test_name][conflict.Visit_label] = conflict.Conflicts;
+ let odata = {};
+ // The data needs to be in the format:
+ // ['instrument1', v1val, v2val, v3val],
+ // ['instrument2', v1val, v2val, v3val]
+ // etc.
+ // First we convert the conflicts from the format returned
+ // from the DB of [VisitLabel, TestName, Count] (sparsely
+ // populated if count is 0) into an object so we can easily
+ // look up the value, then we go through the list of instruments
+ // and populate an array to return to C3.
+ for (let i = 0; i < conflicts.length; i++) {
+ const conflict = conflicts[i];
+ if (!odata[conflict.Test_name]) {
+ odata[conflict.Test_name] = {};
}
+ odata[conflict.Test_name][conflict.Visit_label] = conflict.Conflicts;
+ }
- let data = [];
+ let data = [];
- for (let i = 0; i < instruments.length; i++) {
- const tn = instruments[i];
- let row = [tn];
- for (let j = 0; j < visits.length; j++) {
- const visit = visits[j];
- row.push(Number(odata[tn][visit]));
- }
- data.push(row);
+ for (let i = 0; i < instruments.length; i++) {
+ const tn = instruments[i];
+ let row = [tn];
+ for (let j = 0; j < visits.length; j++) {
+ const visit = visits[j];
+ row.push(Number(odata[tn][visit]));
}
- return data;
+ data.push(row);
+ }
+ return data;
}
export default CandidateConflictsWidget;
diff --git a/modules/conflict_resolver/jsx/conflict_resolver.js b/modules/conflict_resolver/jsx/conflict_resolver.js
index 25bf262fe8e..8b353b7aafb 100644
--- a/modules/conflict_resolver/jsx/conflict_resolver.js
+++ b/modules/conflict_resolver/jsx/conflict_resolver.js
@@ -45,16 +45,16 @@ class ConflictResolver extends Component {
let filtertable;
switch (this.state.activeTab) {
- case 'unresolved':
- filtertable = (
-
- );
- break;
- case 'resolved':
- filtertable = (
-
- );
- break;
+ case 'unresolved':
+ filtertable = (
+
+ );
+ break;
+ case 'resolved':
+ filtertable = (
+
+ );
+ break;
}
return (
{
- return resp.ok ? {} : resp.json();
- })
- .then((json) => {
- if (json.error) {
- throw json.error;
- }
- this.setState({success: true, error: false, emptyOption: false, value});
- })
- .catch((error) => {
- swal('Error!', error, 'error');
- this.setState({error: true, success: false, emptyOption: true});
- });
+ .then((resp) => {
+ return resp.ok ? {} : resp.json();
+ })
+ .then((json) => {
+ if (json.error) {
+ throw json.error;
+ }
+ this.setState({success: true, error: null, emptyOption: false, value});
+ })
+ .catch((error) => {
+ swal('Error!', error, 'error');
+ this.setState({error, success: false, emptyOption: true});
+ });
}
/**
@@ -95,8 +95,7 @@ class FixConflictForm extends Component {
onUserInput={this.resolveConflict}
options={this.props.options}
emptyOption={emptyOption}
- hasError={error}
- errorMessage={''}
+ errorMessage={error}
noMargins={true}
/>
diff --git a/modules/conflict_resolver/jsx/resolved_filterabledatatable.js b/modules/conflict_resolver/jsx/resolved_filterabledatatable.js
index 1b69cf038ec..01f4d9eb5a0 100644
--- a/modules/conflict_resolver/jsx/resolved_filterabledatatable.js
+++ b/modules/conflict_resolver/jsx/resolved_filterabledatatable.js
@@ -45,7 +45,7 @@ class ResolvedFilterableDataTable extends Component {
*/
formatColumn(column, cell, rowData, rowHeaders) {
return (
- {cell}
+ {cell}
);
}
@@ -75,9 +75,9 @@ class ResolvedFilterableDataTable extends Component {
*/
fetchData() {
return fetch(
- loris.BaseURL.concat('/conflict_resolver/resolved'),
- {credentials: 'same-origin'}
- )
+ loris.BaseURL.concat('/conflict_resolver/resolved'),
+ {credentials: 'same-origin'}
+ )
.then((resp) => resp.json())
.then((json) => {
if (json.error) {
@@ -132,6 +132,11 @@ class ResolvedFilterableDataTable extends Component {
type: 'select',
options: options.project,
}},
+ {label: 'Cohort', show: true, filter: {
+ name: 'Cohort',
+ type: 'select',
+ options: options.cohort,
+ }},
{label: 'Site', show: true, filter: {
name: 'Site',
type: 'select',
diff --git a/modules/conflict_resolver/jsx/unresolved_filterabledatatable.js b/modules/conflict_resolver/jsx/unresolved_filterabledatatable.js
index e93b2f66220..16872dc571c 100644
--- a/modules/conflict_resolver/jsx/unresolved_filterabledatatable.js
+++ b/modules/conflict_resolver/jsx/unresolved_filterabledatatable.js
@@ -46,17 +46,17 @@ class UnresolvedFilterableDataTable extends Component {
*/
formatColumn(column, cell, rowData, rowHeaders) {
switch (column) {
- case 'Correct Answer':
- const options = {
- 1: rowData['Value 1'],
- 2: rowData['Value 2'],
- };
- return (
-
- );
+ case 'Correct Answer':
+ const options = {
+ 1: rowData['Value 1'],
+ 2: rowData['Value 2'],
+ };
+ return (
+
+ );
}
return (
{cell}
@@ -144,6 +144,11 @@ class UnresolvedFilterableDataTable extends Component {
type: 'select',
options: options.project,
}},
+ {label: 'Cohort', show: true, filter: {
+ name: 'cohort',
+ type: 'select',
+ options: options.cohort,
+ }},
{label: 'Site', show: true, filter: {
name: 'Site',
type: 'select',
diff --git a/modules/conflict_resolver/php/endpoints/resolved.class.inc b/modules/conflict_resolver/php/endpoints/resolved.class.inc
index ee00ca9a8e7..6b88e490b33 100644
--- a/modules/conflict_resolver/php/endpoints/resolved.class.inc
+++ b/modules/conflict_resolver/php/endpoints/resolved.class.inc
@@ -88,12 +88,14 @@ class Resolved extends Endpoint implements ETagCalculator
$sites = array_values(\Utility::getSiteList(false));
$visitlabels = array_values(\Utility::getVisitList());
$projects = array_values(\Utility::getProjectList());
+ $cohorts = array_values(\Utility::getCohortList());
return [
'site' => array_combine($sites, $sites),
'instrument' => \NDB_BVL_Instrument::getInstrumentNamesList($loris),
'visitLabel' => array_combine($visitlabels, $visitlabels),
'project' => array_combine($projects, $projects),
+ 'cohort' => array_combine($cohorts, $cohorts),
];
}
diff --git a/modules/conflict_resolver/php/endpoints/unresolved.class.inc b/modules/conflict_resolver/php/endpoints/unresolved.class.inc
index 74b6e39e85d..b9da6fa95c5 100644
--- a/modules/conflict_resolver/php/endpoints/unresolved.class.inc
+++ b/modules/conflict_resolver/php/endpoints/unresolved.class.inc
@@ -96,12 +96,14 @@ class Unresolved extends Endpoint implements ETagCalculator
$sites = array_values(\Utility::getSiteList(false));
$visitlabels = array_values(\Utility::getVisitList());
$projects = array_values(\Utility::getProjectList());
+ $cohorts = array_values(\Utility::getCohortList());
return [
'site' => array_combine($sites, $sites),
'instrument' => \NDB_BVL_Instrument::getInstrumentNamesList($loris),
'visitLabel' => array_combine($visitlabels, $visitlabels),
'project' => array_combine($projects, $projects),
+ 'cohort' => array_combine($cohorts, $cohorts),
];
}
diff --git a/modules/conflict_resolver/php/models/resolveddto.class.inc b/modules/conflict_resolver/php/models/resolveddto.class.inc
index 4dbdd50bf56..9451d3480a5 100644
--- a/modules/conflict_resolver/php/models/resolveddto.class.inc
+++ b/modules/conflict_resolver/php/models/resolveddto.class.inc
@@ -37,6 +37,7 @@ class ResolvedDTO implements DataInstance, SiteHaver
protected $resolvedid;
protected $projectid;
protected $project;
+ protected $cohort;
protected $centerid;
protected $site;
protected $candid;
@@ -63,6 +64,7 @@ class ResolvedDTO implements DataInstance, SiteHaver
return [
'ResolvedID' => $this->resolvedid,
'Project' => $this->project,
+ 'Cohort' => $this->cohort,
'Site' => $this->site,
'CandID' => $this->candid,
'PSCID' => $this->pscid,
diff --git a/modules/conflict_resolver/php/models/unresolveddto.class.inc b/modules/conflict_resolver/php/models/unresolveddto.class.inc
index 493a610fe9c..25a500ac67f 100644
--- a/modules/conflict_resolver/php/models/unresolveddto.class.inc
+++ b/modules/conflict_resolver/php/models/unresolveddto.class.inc
@@ -37,6 +37,7 @@ class UnresolvedDTO implements DataInstance, SiteHaver
protected $conflictid;
protected $projectid;
protected $project;
+ protected $cohort;
protected $centerid;
protected $site;
protected $candid;
@@ -58,6 +59,7 @@ class UnresolvedDTO implements DataInstance, SiteHaver
return [
'ConflictID' => $this->conflictid,
'Project' => $this->project,
+ 'Cohort' => $this->cohort,
'Site' => $this->site,
'CandID' => $this->candid,
'PSCID' => $this->pscid,
diff --git a/modules/conflict_resolver/php/provisioners/resolvedprovisioner.class.inc b/modules/conflict_resolver/php/provisioners/resolvedprovisioner.class.inc
index e5719d13341..f087019b922 100644
--- a/modules/conflict_resolver/php/provisioners/resolvedprovisioner.class.inc
+++ b/modules/conflict_resolver/php/provisioners/resolvedprovisioner.class.inc
@@ -39,12 +39,30 @@ class ResolvedProvisioner extends \LORIS\Data\Provisioners\DBObjectProvisioner
candidate.PSCID as pscid,
session.Visit_label as visitlabel,
Project.Name as project,
- conflicts_resolved.FieldName as question,
- conflicts_resolved.OldValue1 as value1,
- conflicts_resolved.OldValue2 as value2,
- CASE
- WHEN conflicts_resolved.NewValue = 1
- THEN conflicts_resolved.OldValue1
+ conflicts_resolved.FieldName as question,
+ CASE
+ WHEN conflicts_resolved.FieldName <> "Examiner"
+ THEN conflicts_resolved.OldValue1
+ ELSE
+ CONCAT(conflicts_resolved.OldValue1, " - ",
+ (SELECT full_name FROM examiners WHERE examinerID = conflicts_resolved.OldValue1))
+ END as value1,
+ CASE
+ WHEN conflicts_resolved.FieldName <> "Examiner"
+ THEN conflicts_resolved.OldValue2
+ ELSE CONCAT(conflicts_resolved.OldValue2, " - ",
+ (SELECT full_name FROM examiners WHERE examinerID = conflicts_resolved.OldValue2))
+ END as value2,
+ CASE
+ WHEN conflicts_resolved.NewValue = 1
+ AND conflicts_resolved.FieldName <> "Examiner"
+ THEN conflicts_resolved.OldValue1
+ WHEN conflicts_resolved.NewValue = 1 AND conflicts_resolved.FieldName = "Examiner"
+ THEN CONCAT(conflicts_resolved.OldValue1, " - ",
+ (SELECT full_name FROM examiners WHERE examinerID = conflicts_resolved.OldValue1))
+ WHEN conflicts_resolved.NewValue <> 1 AND conflicts_resolved.FieldName = "Examiner"
+ THEN CONCAT(conflicts_resolved.OldValue2, " - ",
+ (SELECT full_name FROM examiners WHERE examinerID = conflicts_resolved.OldValue2))
ELSE conflicts_resolved.OldValue2
END AS correctanswer,
conflicts_resolved.ResolutionTimestamp as resolutiontimestamp,
@@ -53,7 +71,8 @@ class ResolvedProvisioner extends \LORIS\Data\Provisioners\DBObjectProvisioner
conflicts_resolved.UserID as resolver,
psc.name as site,
session.CenterID as centerid,
- Project.ProjectID as projectid
+ Project.ProjectID as projectid,
+ cohort.title as cohort
FROM
conflicts_resolved
LEFT JOIN flag ON (conflicts_resolved.CommentId1=flag.CommentID)
@@ -61,6 +80,7 @@ class ResolvedProvisioner extends \LORIS\Data\Provisioners\DBObjectProvisioner
LEFT JOIN candidate ON (candidate.CandID=session.CandID)
LEFT JOIN Project ON (session.ProjectID=Project.ProjectID )
LEFT JOIN psc ON (session.CenterID = psc.CenterID)
+ LEFT JOIN cohort ON (session.CohortID=cohort.CohortID)
WHERE session.Active="Y" AND candidate.Active ="Y"
',
[],
diff --git a/modules/conflict_resolver/php/provisioners/unresolvedprovisioner.class.inc b/modules/conflict_resolver/php/provisioners/unresolvedprovisioner.class.inc
index c5a3dd01d52..6f2cbce7cf3 100644
--- a/modules/conflict_resolver/php/provisioners/unresolvedprovisioner.class.inc
+++ b/modules/conflict_resolver/php/provisioners/unresolvedprovisioner.class.inc
@@ -40,11 +40,22 @@ class UnresolvedProvisioner extends \LORIS\Data\Provisioners\DBObjectProvisioner
session.Visit_label as visitlabel,
Project.Name as project,
conflicts_unresolved.FieldName as question,
- conflicts_unresolved.Value1 as value1,
- conflicts_unresolved.Value2 as value2,
+ CASE
+ WHEN conflicts_unresolved.FieldName = "Examiner"
+ THEN CONCAT(conflicts_unresolved.Value1, " - ",
+ (SELECT full_name FROM examiners WHERE examinerID = conflicts_unresolved.Value1))
+ ELSE conflicts_unresolved.Value1
+ END AS value1,
+ CASE
+ WHEN conflicts_unresolved.FieldName = "Examiner"
+ THEN CONCAT(conflicts_unresolved.Value2, " - ",
+ (SELECT full_name FROM examiners WHERE examinerID = conflicts_unresolved.Value2))
+ ELSE conflicts_unresolved.Value2
+ END AS value2,
psc.name as site,
session.CenterID as centerid,
- Project.ProjectID as projectid
+ Project.ProjectID as projectid,
+ cohort.title as cohort
FROM
conflicts_unresolved
LEFT JOIN flag ON (conflicts_unresolved.CommentId1=flag.CommentID)
@@ -52,6 +63,7 @@ class UnresolvedProvisioner extends \LORIS\Data\Provisioners\DBObjectProvisioner
LEFT JOIN candidate ON (candidate.CandID=session.CandID)
LEFT JOIN Project ON (session.ProjectID=Project.ProjectID)
LEFT JOIN psc ON (session.CenterID = psc.CenterID)
+ LEFT JOIN cohort ON (cohort.CohortID=session.CohortID)
WHERE session.Active="Y" AND candidate.Active ="Y"
',
[],
diff --git a/modules/create_timepoint/jsx/createTimepointIndex.js b/modules/create_timepoint/jsx/createTimepointIndex.js
index 2f3bacf869c..1d2b218f003 100644
--- a/modules/create_timepoint/jsx/createTimepointIndex.js
+++ b/modules/create_timepoint/jsx/createTimepointIndex.js
@@ -5,10 +5,10 @@ import Panel from 'Panel';
import Loader from 'Loader';
import swal from 'sweetalert2';
import {
- FormElement,
- SelectElement,
- StaticElement,
- ButtonElement,
+ FormElement,
+ SelectElement,
+ StaticElement,
+ ButtonElement,
} from 'jsx/Form';
/**
@@ -187,7 +187,7 @@ class CreateTimepoint extends React.Component {
const errorMessage = `No cohorts defined for project: ${
this.state.form.options.project[
this.state.form.value.project
- ]}`;
+ ]}`;
state.messages = [errorMessage];
swal.fire(errorMessage, '', 'error');
state.form.options.cohort = {};
@@ -216,20 +216,20 @@ class CreateTimepoint extends React.Component {
handleVisitLabel() {
const state = Object.assign({}, this.state);
if (state.storage.visit[
- state.form.value.project
- ] !== undefined) {
+ state.form.value.project
+ ] !== undefined) {
if (Array.isArray(state.storage.visit[
state.form.value.project][state.form.value.cohort])
) {
const errorMessage = `No visit labels defined for
combination of project: ${
- this.state.form.options.project[
- this.state.form.value.project
- ]
- } and cohort: ${
- this.state.form.options.cohort[
- this.state.form.value.cohort
- ]}`;
+ this.state.form.options.project[
+ this.state.form.value.project
+ ]
+} and cohort: ${
+ this.state.form.options.cohort[
+ this.state.form.value.cohort
+ ]}`;
state.messages = [errorMessage];
swal.fire(errorMessage, '', 'error');
state.form.options.visit = {};
diff --git a/modules/dashboard/jsx/welcome.js b/modules/dashboard/jsx/welcome.js
index 13ea9300c53..23773a80333 100644
--- a/modules/dashboard/jsx/welcome.js
+++ b/modules/dashboard/jsx/welcome.js
@@ -1,13 +1,13 @@
import DOMPurify from 'dompurify';
window.addEventListener('load', () => {
- fetch(loris.BaseURL + '/dashboard/projectdescription').then( (resp) => {
- if (!resp.ok) {
- throw new Error('Could not get project description');
- }
- return resp.json();
- }).then( (json) => {
- const el = document.getElementById('project-description');
- el.innerHTML = DOMPurify.sanitize(json.Description);
- }).catch( (e) => console.error(e));
+ fetch(loris.BaseURL + '/dashboard/projectdescription').then( (resp) => {
+ if (!resp.ok) {
+ throw new Error('Could not get project description');
+ }
+ return resp.json();
+ }).then( (json) => {
+ const el = document.getElementById('project-description');
+ el.innerHTML = DOMPurify.sanitize(json.Description);
+ }).catch( (e) => console.error(e));
});
diff --git a/modules/data_release/jsx/addPermissionForm.js b/modules/data_release/jsx/addPermissionForm.js
index d4a5aa370de..859541e82ec 100644
--- a/modules/data_release/jsx/addPermissionForm.js
+++ b/modules/data_release/jsx/addPermissionForm.js
@@ -25,7 +25,6 @@ class AddPermissionForm extends Component {
data: {},
fieldOptions: {},
formData: {},
- hasError: {},
errorMessage: {},
isLoaded: false,
loadedData: 0,
@@ -59,8 +58,7 @@ class AddPermissionForm extends Component {
* Called by React when the component has been rendered on the page.
*/
componentDidMount() {
- this.fetchData()
- .then(() => this.setState({isLoaded: true}));
+ this.fetchData().then(() => this.setState({isLoaded: true}));
}
/**
@@ -96,7 +94,6 @@ class AddPermissionForm extends Component {
options={this.state.fieldOptions.users}
onUserInput={this.setFormData}
ref='userid'
- hasError={this.state.hasError.Username}
errorMessage={this.state.errorMessage.Username}
required={true}
value={this.state.formData.userid}
@@ -108,10 +105,10 @@ class AddPermissionForm extends Component {
options={this.state.fieldOptions.filenames}
onUserInput={this.setFormData}
ref='data_release_id'
- hasError={this.state.hasError.Filename}
errorMessage={this.state.errorMessage.Filename}
required={false}
value={this.state.formData.data_release_id}
+ autoSelect={false}
/>
OR
@@ -181,7 +178,6 @@ class AddPermissionForm extends Component {
}).then(function() {
window.location.assign('/data_release');
});
- this.props.fetchData();
} else {
let msg = response.statusText ?
response.statusText :
@@ -208,16 +204,9 @@ class AddPermissionForm extends Component {
Version: undefined,
};
- let hasError = {
- Username: false,
- Filename: false,
- Version: false,
- };
-
// make sure a user was selected
if (!formData.userid) {
errorMessage.Username = 'You must select a user!';
- hasError.Username = true;
isValid = false;
}
@@ -227,12 +216,10 @@ class AddPermissionForm extends Component {
let msg = 'You must select a file OR a version to grant permission on!';
errorMessage.Filename = msg;
errorMessage.Version = msg;
- hasError.Filename = true;
- hasError.Version = true;
isValid = false;
}
- this.setState({errorMessage, hasError});
+ this.setState({errorMessage});
return isValid;
}
}
diff --git a/modules/data_release/jsx/dataReleaseIndex.js b/modules/data_release/jsx/dataReleaseIndex.js
index 5c9ac15d7b4..efc1b7f8904 100644
--- a/modules/data_release/jsx/dataReleaseIndex.js
+++ b/modules/data_release/jsx/dataReleaseIndex.js
@@ -99,26 +99,26 @@ class DataReleaseIndex extends Component {
// Set class to 'bg-danger' if file is hidden.
let result = {cell} ;
switch (column) {
- case 'File Name':
- if (this.props.hasPermission('superuser')
+ case 'File Name':
+ if (this.props.hasPermission('superuser')
|| this.props.hasPermission('data_release_view')
|| this.props.hasPermission('data_release_upload')
|| this.props.hasPermission('data_release_edit_file_access')) {
- const downloadURL = loris.BaseURL
+ const downloadURL = loris.BaseURL
+ '/data_release/files/'
+ encodeURIComponent(row['Data Release ID']);
- result = (
-
-
- {cell}
-
-
- );
- }
- break;
+ result = (
+
+
+ {cell}
+
+
+ );
+ }
+ break;
}
return result;
}
@@ -157,6 +157,11 @@ class DataReleaseIndex extends Component {
name: 'uploadDate',
type: 'text',
}},
+ {label: 'Project Name', show: true, filter: {
+ name: 'Project',
+ type: 'select',
+ options: this.state.data.fieldOptions.projects,
+ }},
{label: 'Data Release ID', show: false,
},
];
@@ -178,9 +183,10 @@ class DataReleaseIndex extends Component {
+ '/data_release/files'
}
action={loris.BaseURL + '/data_release/files'}
+ projects={this.state.data.fieldOptions.projects}
/>
- );
+ );
// Add Permission modal window
const addPermissionForm = (
@@ -224,7 +230,7 @@ class DataReleaseIndex extends Component {
show={this.state.show.managePermissionsForm}
onClose={() => this.hide('managePermissionsForm')}
/>
- );
+ );
const actions = [
{
diff --git a/modules/data_release/jsx/managePermissionsForm.js b/modules/data_release/jsx/managePermissionsForm.js
index 958a81095ae..d98602a8856 100644
--- a/modules/data_release/jsx/managePermissionsForm.js
+++ b/modules/data_release/jsx/managePermissionsForm.js
@@ -7,6 +7,7 @@ import {
FormElement,
CheckboxElement,
StaticElement,
+ SearchableDropdown,
} from 'jsx/Form';
/**
@@ -24,11 +25,12 @@ class ManagePermissionsForm extends Component {
this.state = {
data: {},
- hasError: {},
+ originalData: {},
errorMessage: {},
isLoaded: false,
};
+ this.setFormData = this.setFormData.bind(this);
this.fetchData = this.fetchData.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
@@ -38,7 +40,7 @@ class ManagePermissionsForm extends Component {
*/
componentDidMount() {
this.fetchData()
- .then(() => this.setState({isLoaded: true}));
+ .then(() => this.setState({isLoaded: true}));
}
/**
@@ -48,12 +50,12 @@ class ManagePermissionsForm extends Component {
*/
fetchData() {
return fetch(this.props.DataURL, {credentials: 'same-origin'})
- .then((resp) => resp.json())
- .then((data) => this.setState({data}))
- .catch( (error) => {
- this.setState({error: 'An error occurred when loading the form!'});
- console.error(error);
- });
+ .then((resp) => resp.json())
+ .then((data) => this.setState({data, originalData: data}))
+ .catch( (error) => {
+ this.setState({error: 'An error occurred when loading the form!'});
+ console.error(error);
+ });
}
/**
@@ -88,26 +90,72 @@ class ManagePermissionsForm extends Component {
onSubmit={this.handleSubmit}
>
- {Object.entries(data).map(([userId, user]) => {
- const versions = Object.values(options.versions).map((version) =>
-
-
- this.setFormData(userId, version, permission)
- }
- />
-
- );
-
- return
+ {this.state.user &&
+
+
+ this.setFormData(
+ 'versionsByUser', {
+ userId: this.state.user, version, checked,
+ }
+ )
+ }
+ />
+
+ )}
+ />
+ }
+
+ {this.state.version &&
+ {
+ if (user.versions.includes(this.state.version)) {
+ return
+ {versions}
}
- />;
- })};
+ value={
+ data[user.id].versions.includes(this.state.version)
+ }
+ onUserInput={(_, checked) =>
+ this.setFormData(
+ 'usersByVersion',
+ {
+ userId: user.id,
+ checked,
+ version: this.state.version,
+ }
+ )
+ }
+ />
+ ;
+ }
+ }
+ )}
+ />
+ }
);
@@ -116,19 +164,25 @@ class ManagePermissionsForm extends Component {
/**
* Store the value of the element in this.state.data
*
- * @param {string} userId
- * @param {string} version
- * @param {boolean} permission
+ * @param {string} formElement - name of the selected element
+ * @param {string} value - selected value for corresponding form element
*/
- setFormData(userId, version, permission) {
+ setFormData(formElement, value) {
let {data} = JSON.parse(JSON.stringify(this.state));
- if (permission) {
- data[userId].versions = [...data[userId].versions, version];
+ if (formElement === 'versionsByUser' || formElement === 'usersByVersion') {
+ let {checked, version, userId} = value;
+ if (checked) {
+ data[userId].versions = [...data[userId].versions, version];
+ } else {
+ data[userId].versions = data[userId].versions
+ .filter((e) => e !== version);
+ }
+ this.setState({data});
} else {
- data[userId].versions = data[userId].versions
- .filter((e) => e !== version);
+ this.setState({[formElement]: (value === '' ? null : value)});
+ if (formElement != 'user') this.setState({user: null});
+ if (formElement != 'version') this.setState({version: null});
}
- this.setState({data});
}
/**
@@ -148,23 +202,23 @@ class ManagePermissionsForm extends Component {
body: formObj,
cache: 'no-cache',
})
- .then((response) => {
- if (response.ok) {
- swal.fire({
- text: 'Permission Update Success!',
- title: '',
- type: 'success',
- });
- this.props.fetchData();
- return Promise.resolve();
- } else {
- let msg = response.statusText ?
- response.statusText : 'Submission Error!';
- swal.fire(msg, '', 'error');
- console.error(msg);
- return Promise.reject();
- }
- });
+ .then((response) => {
+ if (response.ok) {
+ swal.fire({
+ text: 'Permission Update Success!',
+ title: '',
+ type: 'success',
+ });
+ this.props.fetchData();
+ return Promise.resolve();
+ } else {
+ let msg = response.statusText ?
+ response.statusText : 'Submission Error!';
+ swal.fire(msg, '', 'error');
+ console.error(msg);
+ return Promise.reject();
+ }
+ });
}
}
diff --git a/modules/data_release/jsx/uploadFileForm.js b/modules/data_release/jsx/uploadFileForm.js
index 68043b02a02..7abe8610f23 100644
--- a/modules/data_release/jsx/uploadFileForm.js
+++ b/modules/data_release/jsx/uploadFileForm.js
@@ -8,6 +8,7 @@ import {
FileElement,
TextboxElement,
ButtonElement,
+ SelectElement,
} from 'jsx/Form';
/**
@@ -28,7 +29,6 @@ class UploadFileForm extends Component {
formData: {},
uploadResult: null,
errorMessage: {},
- hasError: {},
isLoaded: false,
uploadProgress: -1,
};
@@ -79,6 +79,14 @@ class UploadFileForm extends Component {
required={false}
value={this.state.formData.version}
/>
+
@@ -118,15 +126,15 @@ class UploadFileForm extends Component {
Filesize: undefined,
};
- let hasError = {
- Filename: undefined,
- Filesize: undefined,
- };
-
if (!formData.file) {
errorMessage.Filename = 'You must select a file to upload';
- hasError.Filename = true;
- this.setState({errorMessage, hasError});
+ this.setState({errorMessage});
+ return;
+ }
+
+ if (!formData.project) {
+ errorMessage.Project = 'You must select a project';
+ this.setState({errorMessage});
return;
}
@@ -138,14 +146,13 @@ class UploadFileForm extends Component {
+ maxSizeAllowed
+ ')';
errorMessage['Filesize'] = msg;
- hasError['Filesize'] = true;
swal.fire({
title: 'Error',
text: msg,
type: 'error',
showCancelButton: true,
});
- this.setState({errorMessage, hasError});
+ this.setState({errorMessage});
return;
}
@@ -175,30 +182,30 @@ class UploadFileForm extends Component {
cache: 'no-cache',
}).then(async (response) => {
if (response.status === 409) {
- swal.fire({
- title: 'Are you sure?',
- text: 'A file with this name already exists!\n '
+ swal.fire({
+ title: 'Are you sure?',
+ text: 'A file with this name already exists!\n '
+ 'Would you like to overwrite existing file?\n '
+ 'Note that the version associated with '
+ 'the file will also be overwritten.',
- type: 'warning',
- showCancelButton: true,
- confirmButtonText: 'Yes, I am sure!',
- cancelButtonText: 'No, cancel it!',
- }).then((isConfirm) => {
- if (isConfirm && isConfirm.value) {
- this.uploadFile(true);
- }
- });
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonText: 'Yes, I am sure!',
+ cancelButtonText: 'No, cancel it!',
+ }).then((isConfirm) => {
+ if (isConfirm && isConfirm.value) {
+ this.uploadFile(true);
+ }
+ });
} else if (!response.ok) {
const body = await response.json();
let msg;
if (body && body.error) {
- msg = body.error;
+ msg = body.error;
} else if (response.statusText) {
- msg = response.statusText;
+ msg = response.statusText;
} else {
- msg = 'Upload error!';
+ msg = 'Upload error!';
}
this.setState({
errorMessage: msg,
@@ -207,13 +214,13 @@ class UploadFileForm extends Component {
swal.fire(msg, '', 'error');
console.error(msg);
} else {
- swal.fire({
- text: 'Upload Successful!',
- title: '',
- type: 'success',
- }).then(function() {
- window.location.assign('/data_release');
- });
+ swal.fire({
+ text: 'Upload Successful!',
+ title: '',
+ type: 'success',
+ }).then(function() {
+ window.location.assign('/data_release');
+ });
}
}).catch( (error) => {
let msg = error.message ? error.message : 'Upload error!';
@@ -230,6 +237,7 @@ class UploadFileForm extends Component {
UploadFileForm.propTypes = {
DataURL: PropTypes.string.isRequired,
action: PropTypes.string.isRequired,
+ projects: PropTypes.array.isRequired,
};
export default UploadFileForm;
diff --git a/modules/data_release/php/data_release.class.inc b/modules/data_release/php/data_release.class.inc
index 6f8a814c5c1..47ffded4583 100644
--- a/modules/data_release/php/data_release.class.inc
+++ b/modules/data_release/php/data_release.class.inc
@@ -59,8 +59,12 @@ class Data_Release extends \DataFrameworkMenu
function getUsersList(\Database $DB)
{
return $DB->pselectColWithIndexKey(
- "SELECT ID, UserID FROM users",
- [],
+ "SELECT DISTINCT ID, LOWER(users.UserID) as UserID FROM users
+ JOIN user_project_rel upr ON upr.UserID = users.ID
+ WHERE upr.ProjectID IN
+ (SELECT ProjectID FROM user_project_rel WHERE UserID = :userID)
+ ORDER BY LOWER(users.UserID)",
+ [':userID' => \User::singleton()->getID()],
"ID"
);
}
@@ -75,8 +79,10 @@ class Data_Release extends \DataFrameworkMenu
function getFilesList(\Database $DB)
{
$result = $DB->pselectWithIndexKey(
- "SELECT id, file_name, version FROM data_release",
- [],
+ "SELECT id, file_name, version, ProjectID FROM data_release
+ WHERE ProjectID IS NULL OR ProjectID IN
+ (SELECT ProjectID FROM user_project_rel WHERE UserID = :userID)",
+ [':userID' => \User::singleton()->getID()],
"id"
);
@@ -99,9 +105,12 @@ class Data_Release extends \DataFrameworkMenu
*/
function getVersionsList(\Database $DB)
{
+ $user =& \User::singleton();
$versions = $DB->pselectCol(
- "SELECT DISTINCT version FROM data_release",
- [],
+ "SELECT DISTINCT version FROM data_release
+ WHERE ProjectID IS NULL OR ProjectID IN
+ (SELECT ProjectID FROM user_project_rel WHERE UserID = :userID)",
+ ['userID' => $user->getID()],
"version"
);
@@ -125,8 +134,10 @@ class Data_Release extends \DataFrameworkMenu
function getVersionFiles(\Database $db)
{
$result = $db->pselect(
- "SELECT version, id FROM data_release",
- []
+ "SELECT version, id FROM data_release
+ WHERE ProjectID IS NULL OR ProjectID IN
+ (SELECT ProjectID FROM user_project_rel WHERE UserID = :userID)",
+ [':userID' => \User::singleton()->getID()]
);
$versionFiles = [];
@@ -151,16 +162,23 @@ class Data_Release extends \DataFrameworkMenu
$result = $db->pselect(
"SELECT u.ID as userId,
u.UserID as userName,
- drp.data_release_id fileId
+ drp.data_release_id fileId,
+ dr.ProjectID as ProjectID
FROM users u
LEFT JOIN data_release_permissions drp ON (u.ID=drp.userid)
- LEFT JOIN data_release dr ON (drp.data_release_id=dr.id)",
- []
+ LEFT JOIN data_release dr ON (drp.data_release_id=dr.id)
+ JOIN user_project_rel upr ON upr.UserID = u.ID
+ WHERE upr.ProjectID IN
+ (SELECT ProjectID FROM user_project_rel WHERE UserID = :userID)",
+ [':userID' => \User::singleton()->getID()]
);
+ error_log(print_r($result, true));
+
$userFiles = [];
foreach ($result as $row) {
$userFiles[$row['userId']]['name'] = $row['userName'];
+ $userFiles[$row['userId']]['id'] = $row['userId'];
if (empty($userFiles[$row['userId']]['files'])) {
$userFiles[$row['userId']]['files'] = [];
}
@@ -206,11 +224,13 @@ class Data_Release extends \DataFrameworkMenu
*/
protected function getFieldOptions() : array
{
- $db = $this->loris->getDatabaseConnection();
+ $db = $this->loris->getDatabaseConnection();
+ $projects = \Utility::getProjectList();
return [
'users' => $this->getUsersList($db),
'versions' => $this->getVersionsList($db),
'filenames' => $this->getFilesList($db),
+ 'projects' => array_combine($projects, $projects)
];
}
diff --git a/modules/data_release/php/datareleaseprovisioner.class.inc b/modules/data_release/php/datareleaseprovisioner.class.inc
index 636f2b5c36f..f3eab13e526 100644
--- a/modules/data_release/php/datareleaseprovisioner.class.inc
+++ b/modules/data_release/php/datareleaseprovisioner.class.inc
@@ -44,8 +44,10 @@ class DataReleaseProvisioner extends \LORIS\Data\Provisioners\DBRowProvisioner
file_name AS fileName,
IF(version is null or version ='','Unversioned', version) AS version,
upload_date AS uploadDate,
+ p.Name as ProjectName,
dr.id as dataReleaseID
- FROM data_release dr";
+ FROM data_release dr
+ LEFT JOIN Project p on p.ProjectID = dr.ProjectID";
if (!$user->hasPermission("superuser")) {
$query .= "
@@ -54,7 +56,11 @@ class DataReleaseProvisioner extends \LORIS\Data\Provisioners\DBRowProvisioner
ON
(dr.id=drp.data_release_id)
WHERE
- drp.UserID=".$user->getID();
+ drp.UserID=".$user->getID()." AND (
+ dr.ProjectID IS NULL OR dr.ProjectID IN
+ (SELECT ProjectID FROM user_project_rel
+ WHERE UserID = ".$user->getID().")
+ )";
}
$query .= " ORDER BY uploadDate";
diff --git a/modules/data_release/php/files.class.inc b/modules/data_release/php/files.class.inc
index 4ac7cc31e36..b9067d51d37 100644
--- a/modules/data_release/php/files.class.inc
+++ b/modules/data_release/php/files.class.inc
@@ -116,6 +116,7 @@ class Files extends \NDB_Page
$request,
$posted['version'],
$overwrite,
+ $posted['project'],
);
}
@@ -180,15 +181,17 @@ class Files extends \NDB_Page
* Moves a to the appropriate place on the filesystem and inserts into
* the database, returning an appropriate HTTP response.
*
- * @param \LORIS\FilesUploadHandler $files The FilesUploadHandler which moves
- * the file and generates the
- * response.
- * @param \User $user The user uploading the file.
- * @param string $fileName The file name being uploaded.
- * @param ServerRequestInterface $request The incoming request.
- * @param ?string $version The user submitted file version.
- * @param bool $overwrite Flag to indicate if existing file
- * should be overwritten.
+ * @param \LORIS\FilesUploadHandler $files The FilesUploadHandler which
+ * moves the file and generates
+ * the response.
+ * @param \User $user The user uploading the file.
+ * @param string $fileName The file name being uploaded.
+ * @param ServerRequestInterface $request The incoming request.
+ * @param ?string $version The user submitted file version.
+ * @param bool $overwrite Flag to indicate if existing
+ * file should be overwritten.
+ * @param ?string $projectName The name of the project the file
+ * belongs to
*
* @return ResponseInterface
*/
@@ -198,13 +201,20 @@ class Files extends \NDB_Page
string $fileName,
ServerRequestInterface $request,
?string $version,
- bool $overwrite
+ bool $overwrite,
+ ?string $projectName
) : ResponseInterface {
$DB = $this->loris->getDatabaseConnection();
if ($version !== null) {
$version = strtolower($version);
}
+ // Get ProjectID
+ $ProjectID = $DB->pselectOne(
+ "SELECT ProjectID FROM Project WHERE Name=:project",
+ ['project' => $projectName]
+ );
+
$upload_date = date('Y-m-d');
if ($overwrite) {
// update file in data_release table.
@@ -213,6 +223,7 @@ class Files extends \NDB_Page
[
'version' => $version,
'upload_date' => $upload_date,
+ 'ProjectID' => $ProjectID,
],
['file_name' => $fileName]
);
@@ -224,6 +235,7 @@ class Files extends \NDB_Page
'file_name' => $fileName,
'version' => $version,
'upload_date' => $upload_date,
+ 'ProjectID' => $ProjectID,
]
);
diff --git a/modules/datadict/jsx/dataDictIndex.js b/modules/datadict/jsx/dataDictIndex.js
index 9116f80ab92..4849c2f9998 100644
--- a/modules/datadict/jsx/dataDictIndex.js
+++ b/modules/datadict/jsx/dataDictIndex.js
@@ -42,31 +42,31 @@ class DataDictIndex extends Component {
* Called by React when the component has been rendered on the page.
*/
componentDidMount() {
- // Load the field options. This comes from a separate request than the
- // table data stream. Once the fieldOptions are loaded, we set isLoaded
- // to true so that the page is displayed with the data that's been
- // retrieved.
- fetch(this.props.fieldsURL, {credentials: 'same-origin'})
- .then((resp) => resp.json())
- .then((data) => this.setState({fieldOptions: data, isLoaded: true}))
- .catch((error) => {
- this.setState({error: true});
- console.error(error);
- });
+ // Load the field options. This comes from a separate request than the
+ // table data stream. Once the fieldOptions are loaded, we set isLoaded
+ // to true so that the page is displayed with the data that's been
+ // retrieved.
+ fetch(this.props.fieldsURL, {credentials: 'same-origin'})
+ .then((resp) => resp.json())
+ .then((data) => this.setState({fieldOptions: data, isLoaded: true}))
+ .catch((error) => {
+ this.setState({error: true});
+ console.error(error);
+ });
this.fetchData();
}
/**
* Retrive data from the provided URL and save it in state
*/
- fetchData() {
- fetchDataStream(this.props.dataURL,
- (row) => this.state.data.push(row),
- (end) => {
- this.setState({isLoading: !end, data: this.state.data});
- },
- () => {},
- );
+ fetchData() {
+ fetchDataStream(this.props.dataURL,
+ (row) => this.state.data.push(row),
+ (end) => {
+ this.setState({isLoading: !end, data: this.state.data});
+ },
+ () => {},
+ );
}
/**
@@ -129,7 +129,7 @@ class DataDictIndex extends Component {
contentEditable="true"
className="description"
onBlur={updateDict(rowData)}>
- {cell}
+ {cell}
);
}
@@ -143,7 +143,7 @@ class DataDictIndex extends Component {
*/
render() {
if (this.state.error) {
- return
An error occured while loading the page. ;
+ return
An error occured while loading the page. ;
}
// Waiting for async data to load
@@ -153,61 +153,61 @@ class DataDictIndex extends Component {
const options = this.state.fieldOptions;
let fields = [
- {
- label: 'Source From',
- show: true,
- filter: {
- name: 'Source From',
- type: 'multiselect',
- options: options.sourceFrom,
- },
+ {
+ label: 'Source From',
+ show: true,
+ filter: {
+ name: 'Source From',
+ type: 'multiselect',
+ options: options.sourceFrom,
},
- {
- label: 'Name',
- show: true,
- filter: {
- name: 'Name',
- type: 'text',
- },
+ },
+ {
+ label: 'Name',
+ show: true,
+ filter: {
+ name: 'Name',
+ type: 'text',
},
- {
- label: 'Source Field',
- show: true,
- filter: {
- name: 'Source Field',
- type: 'text',
- },
+ },
+ {
+ label: 'Source Field',
+ show: true,
+ filter: {
+ name: 'Source Field',
+ type: 'text',
},
- {
- label: 'Description',
- show: true,
- filter: {
- name: 'Description',
- type: 'text',
- },
+ },
+ {
+ label: 'Description',
+ show: true,
+ filter: {
+ name: 'Description',
+ type: 'text',
},
- {
- label: 'Description Status',
- show: true,
- filter: {
- name: 'DescriptionStatus',
- type: 'select',
- options: {
- 'empty': 'Empty',
- 'modified': 'Modified',
- 'unchanged': 'Unchanged',
- },
- },
+ },
+ {
+ label: 'Description Status',
+ show: true,
+ filter: {
+ name: 'DescriptionStatus',
+ type: 'select',
+ options: {
+ 'empty': 'Empty',
+ 'modified': 'Modified',
+ 'unchanged': 'Unchanged',
+ },
},
+ },
];
return (
-
+
);
}
}
diff --git a/modules/dataquery/jsx/calcpayload.tsx b/modules/dataquery/jsx/calcpayload.tsx
index 0229cf71103..5bcf7380f96 100644
--- a/modules/dataquery/jsx/calcpayload.tsx
+++ b/modules/dataquery/jsx/calcpayload.tsx
@@ -1,9 +1,9 @@
import {QueryTerm, QueryGroup} from './querydef';
import {
- APIQueryObject,
- APIQueryField,
- APIQueryGroupField,
- APIQueryCriteriaGroup,
+ APIQueryObject,
+ APIQueryField,
+ APIQueryGroupField,
+ APIQueryCriteriaGroup,
} from './types';
/**
* Calculates the payload to submit to the search endpoint
@@ -14,38 +14,38 @@ import {
* @returns {APIQueryObject} - The query to send to the API
*/
export function calcPayload(
- fields: APIQueryField[],
- filters: QueryGroup
+ fields: APIQueryField[],
+ filters: QueryGroup
): APIQueryObject {
- const payload: APIQueryObject = {
- type: 'candidates',
- fields: fields.map((val: APIQueryField) => {
- const fieldpayload: APIQueryField = {
- module: val.module,
- category: val.category,
- field: val.field,
- };
- if (val.visits) {
- fieldpayload.visits = val.visits;
- }
- return fieldpayload;
- },
- ),
+ const payload: APIQueryObject = {
+ type: 'candidates',
+ fields: fields.map((val: APIQueryField) => {
+ const fieldpayload: APIQueryField = {
+ module: val.module,
+ category: val.category,
+ field: val.field,
+ };
+ if (val.visits) {
+ fieldpayload.visits = val.visits;
+ }
+ return fieldpayload;
+ },
+ ),
+ };
+ if (filters.group.length > 0) {
+ payload.criteria = {
+ operator: filters.operator,
+ group: filters.group.map( (val) => {
+ if (val instanceof QueryTerm) {
+ return val as APIQueryGroupField;
+ } else if (val instanceof QueryGroup) {
+ return val as APIQueryCriteriaGroup;
+ } else {
+ throw new Error('Invalid query');
+ }
+ }),
};
- if (filters.group.length > 0) {
- payload.criteria = {
- operator: filters.operator,
- group: filters.group.map( (val) => {
- if (val instanceof QueryTerm) {
- return val as APIQueryGroupField;
- } else if (val instanceof QueryGroup) {
- return val as APIQueryCriteriaGroup;
- } else {
- throw new Error('Invalid query');
- }
- }),
- };
- }
- return payload;
+ }
+ return payload;
}
diff --git a/modules/dataquery/jsx/components/expansionpanels.tsx b/modules/dataquery/jsx/components/expansionpanels.tsx
index 960b8b90755..1c0dfd4536f 100644
--- a/modules/dataquery/jsx/components/expansionpanels.tsx
+++ b/modules/dataquery/jsx/components/expansionpanels.tsx
@@ -21,7 +21,7 @@ const ExpansionPanels = (props: {
}) => {
return (
+ style={{margin: '0 auto', maxWidth: '900px'}}>
{ props.panels.map((panel, index) => (
string,
}) {
- const groups: SelectGroup[] = [];
- const placeholder = props.placeholder || 'Select a category';
- for (const [module, subcategories]
- of Object.entries(props.groups)) {
- const options: SelectOption[] = [];
- for (const [value, desc]
- of Object.entries(subcategories) as unknown as [string, string]) {
- options.push({
- value: value,
- label: desc,
- module: module,
- });
- }
-
- let label = module;
- if (props.mapGroupName) {
- label = props.mapGroupName(module);
- }
- groups.push({
- label: label,
- options: options,
+ const groups: SelectGroup[] = [];
+ const placeholder = props.placeholder || 'Select a category';
+ for (const [module, subcategories]
+ of Object.entries(props.groups)) {
+ const options: SelectOption[] = [];
+ for (const [value, desc]
+ of Object.entries(subcategories) as unknown as [string, string]) {
+ options.push({
+ value: value,
+ label: desc,
+ module: module,
});
}
- /**
- * Callback to call when the selection changes.
- *
- * @param {object} e - The click event callback
- * @param {string} e.module - The module
- * @param {string} e.value - The value
- * @returns {void}
- */
- const selected = (e: SingleValue) => {
- // The callback should be (e: SelectOption) but typescript
- // is convinced that it's a SingleValue.
- // console.log(e) confirms that it has the same structure
- // as SelectOption, so just convert it and explicitly
- // cast it unsafely to make the compiler happy.
- const val: SelectOption = e as unknown as SelectOption;
- props.onChange(val.module, val.value);
- };
- return (
-
-
) => {
+ // The callback should be (e: SelectOption) but typescript
+ // is convinced that it's a SingleValue.
+ // console.log(e) confirms that it has the same structure
+ // as SelectOption, so just convert it and explicitly
+ // cast it unsafely to make the compiler happy.
+ const val: SelectOption = e as unknown as SelectOption;
+ props.onChange(val.module, val.value);
+ };
+ return (
+
+ ({...base, zIndex: 9999})}
- }
- placeholder={placeholder}
- />
-
- );
+ }
+ placeholder={placeholder}
+ />
+
+ );
}
export default FilterableSelectGroup;
diff --git a/modules/dataquery/jsx/criteriaterm.tsx b/modules/dataquery/jsx/criteriaterm.tsx
index 8a90309b849..9fe6b2d457a 100644
--- a/modules/dataquery/jsx/criteriaterm.tsx
+++ b/modules/dataquery/jsx/criteriaterm.tsx
@@ -11,25 +11,25 @@ import {FieldDictionary, FullDictionary} from './types';
* @returns {string} - The frontend value to display for op
*/
function op2str(op: string): string {
- switch (op) {
- case 'lt': return '<';
- case 'lte': return '≤';
- case 'eq': return '=';
- case 'neq': return '≠';
- case 'gte': return '≥';
- case 'gt': return '>';
- case 'in': return 'in';
- case 'startsWith': return 'starts with';
- case 'contains': return 'contains';
- case 'endsWith': return 'ends with';
- case 'isnotnull': return 'has data';
- case 'isnull': return 'has no data';
- case 'exists': return 'exists';
- case 'notexists': return 'does not exist';
- case 'numberof': return 'number of';
- default: console.error('Unhandle operator');
- return '';
- }
+ switch (op) {
+ case 'lt': return '<';
+ case 'lte': return '≤';
+ case 'eq': return '=';
+ case 'neq': return '≠';
+ case 'gte': return '≥';
+ case 'gt': return '>';
+ case 'in': return 'in';
+ case 'startsWith': return 'starts with';
+ case 'contains': return 'contains';
+ case 'endsWith': return 'ends with';
+ case 'isnotnull': return 'has data';
+ case 'isnull': return 'has no data';
+ case 'exists': return 'exists';
+ case 'notexists': return 'does not exist';
+ case 'numberof': return 'number of';
+ default: console.error('Unhandle operator');
+ return '';
+ }
}
/**
@@ -41,14 +41,14 @@ function op2str(op: string): string {
* @returns {FieldDictionary} - The dictionary for this term
*/
function getDictionary(
- term: QueryTerm,
- dict: FullDictionary
+ term: QueryTerm,
+ dict: FullDictionary
): FieldDictionary|null {
- if (!dict || !dict[term.module] || !dict[term.module][term.category]
+ if (!dict || !dict[term.module] || !dict[term.module][term.category]
|| !dict[term.module][term.category][term.fieldname]) {
- return null;
- }
- return dict[term.module][term.category][term.fieldname];
+ return null;
+ }
+ return dict[term.module][term.category][term.fieldname];
}
/**
@@ -67,101 +67,101 @@ export function CriteriaTerm(props: {
mapModuleName: (module: string) => string,
mapCategoryName: (module: string, category: string) => string,
}) {
- const containerStyle: React.CSSProperties ={
- display: 'flex' as const,
- flexWrap: 'nowrap' as const,
- flexDirection: 'row' as const,
- justifyContent: 'space-evenly',
- width: '100%',
- alignItems: 'center',
- };
+ const containerStyle: React.CSSProperties ={
+ display: 'flex' as const,
+ flexWrap: 'nowrap' as const,
+ flexDirection: 'row' as const,
+ justifyContent: 'space-evenly',
+ width: '100%',
+ alignItems: 'center',
+ };
- const fieldStyle = {
- width: '33%',
- };
- const opStyle = {
- width: '33%',
- textAlign: 'center' as const,
- };
- const valueStyle = {
- width: '33%',
- display: 'flex',
- alignItems: 'center',
- };
+ const fieldStyle = {
+ width: '33%',
+ };
+ const opStyle = {
+ width: '33%',
+ textAlign: 'center' as const,
+ };
+ const valueStyle = {
+ width: '33%',
+ display: 'flex',
+ alignItems: 'center',
+ };
- let visits;
- if (props.term.visits) {
- visits = '';
- if (props.term.visits.length == 1) {
- visits += props.term.visits[0];
- } else {
- for (let i = 0; i < props.term.visits.length; i++) {
- visits += props.term.visits[i];
- if (i == props.term.visits.length-2) {
- visits += ' or ';
- } else if (i < props.term.visits.length-2) {
- visits += ', ';
- }
- }
+ let visits;
+ if (props.term.visits) {
+ visits = '';
+ if (props.term.visits.length == 1) {
+ visits += props.term.visits[0];
+ } else {
+ for (let i = 0; i < props.term.visits.length; i++) {
+ visits += props.term.visits[i];
+ if (i == props.term.visits.length-2) {
+ visits += ' or ';
+ } else if (i < props.term.visits.length-2) {
+ visits += ', ';
}
- visits =
- at visit
- {visits}
-
;
+ }
}
+ visits =
+ at visit
+ {visits}
+
;
+ }
- let value;
- if (props.term.op == Operators.IN) {
- const liststyle = {
- margin: 0,
- padding: 0,
- listStylePosition: 'inside' as const,
- listStyleType: 'disc',
- };
+ let value;
+ if (props.term.op == Operators.IN) {
+ const liststyle = {
+ margin: 0,
+ padding: 0,
+ listStylePosition: 'inside' as const,
+ listStyleType: 'disc',
+ };
- value =
- {(props.term.value as string[]).map(
- (val, idx) => {val}
- )}
- ;
- } else {
- value = {props.term.value} ;
- }
+ value =
+ {(props.term.value as string[]).map(
+ (val, idx) => {val}
+ )}
+ ;
+ } else {
+ value = {props.term.value} ;
+ }
- let cardinalityWarning;
- const dict = getDictionary(props.term, props.fulldictionary);
- if (!dict) {
- // This sometimes happens when first loading, before the dictionary
- // is retrieved, so we do not print an error.
- } else if (dict.cardinality == 'many') {
- cardinalityWarning = ;
- }
- return (
-
-
-
-
-
{op2str(props.term.op)}
-
-
{value}
-
{visits}
- {cardinalityWarning}
-
-
);
+ >;
+ }
+ return (
+
+
+
+
+
{op2str(props.term.op)}
+
+
{value}
+
{visits}
+ {cardinalityWarning}
+
+
);
}
diff --git a/modules/dataquery/jsx/definefields.tsx b/modules/dataquery/jsx/definefields.tsx
index b0a42771bbd..90c71789fdc 100644
--- a/modules/dataquery/jsx/definefields.tsx
+++ b/modules/dataquery/jsx/definefields.tsx
@@ -53,12 +53,12 @@ function QueryField(props: {
const value=props.value;
const scrollRef = useRef(null);
useEffect(() => {
- if (props.scrollTo == true && scrollRef.current !== null) {
- scrollRef.current.scrollIntoView({
- behavior: 'smooth',
- });
- props.resetScrollTo();
- }
+ if (props.scrollTo == true && scrollRef.current !== null) {
+ scrollRef.current.scrollIntoView({
+ behavior: 'smooth',
+ });
+ props.resetScrollTo();
+ }
}, [props.scrollTo]);
let visits;
@@ -73,41 +73,41 @@ function QueryField(props: {
* @returns {void}
*/
const selected = (newvisits: readonly VisitOption[]) => {
- props.onChangeVisitList(
- props.module,
- props.category,
- item,
- newvisits.map( (visit: VisitOption) => visit.value),
- );
+ props.onChangeVisitList(
+ props.module,
+ props.category,
+ item,
+ newvisits.map( (visit: VisitOption) => visit.value),
+ );
};
const selectOptions: string[] = value.visits || [];
if (props.selected && (typeof props.selected.visits !== 'undefined')) {
- selectedVisits = props.selected.visits;
+ selectedVisits = props.selected.visits;
} else {
- selectedVisits = selectOptions.filter((visit: string) => {
- return props.defaultVisits.includes(visit);
- });
+ selectedVisits = selectOptions.filter((visit: string) => {
+ return props.defaultVisits.includes(visit);
+ });
}
if (props.selected) {
- visits = e.stopPropagation()}>
-
Visits
-
{
- return {value: visit, label: visit};
- })
- }
- isMulti
- onChange={selected}
- placeholder='Select Visits'
- value={selectedVisits.map( (visit: string): VisitOption => {
- return {value: visit, label: visit};
- })
- }
- menuPortalTarget={document.body}
- styles={
- {menuPortal:
+ visits = e.stopPropagation()}>
+
Visits
+ {
+ return {value: visit, label: visit};
+ })
+ }
+ isMulti
+ onChange={selected}
+ placeholder='Select Visits'
+ value={selectedVisits.map( (visit: string): VisitOption => {
+ return {value: visit, label: visit};
+ })
+ }
+ menuPortalTarget={document.body}
+ styles={
+ {menuPortal:
/**
* Adds appropriate zIndex to the react select's base CSS
*
@@ -115,34 +115,34 @@ function QueryField(props: {
* @returns {object} New CSS with z-index added
*/
(base) => ({...base, zIndex: 9999}),
- }
- }
- closeMenuOnSelect={false}
- />
- ;
+ }
+ }
+ closeMenuOnSelect={false}
+ />
+ ;
}
}
const download = value.type == 'URI' ?
: null;
return (
props.onFieldToggle(
- props.module,
- props.category,
- item,
- selectedVisits,
- )}>
-
- {item}
- {value.description} {download}
-
- {visits}
+ ref={scrollRef}
+ style={{
+ cursor: 'pointer',
+ display: 'flex',
+ justifyContent: 'space-between',
+ }}
+ onClick={() => props.onFieldToggle(
+ props.module,
+ props.category,
+ item,
+ selectedVisits,
+ )}>
+
+ {item}
+ {value.description} {download}
+
+ {visits}
);
}
@@ -202,73 +202,73 @@ function DefineFields(props: {
const [syncVisits, setSyncVisits] = useState(false);
const [zoomTo, setZoomTo] = useState(null);
useEffect(() => {
- if (!syncVisits) {
- return;
- }
- let modifiedvisits = false;
- props.selected.forEach( (field: APIQueryField) => {
- // Valid visits according to the dictionary
- const category = props.fulldictionary[field.module][field.category];
- const dict = category[field.field];
+ if (!syncVisits) {
+ return;
+ }
+ let modifiedvisits = false;
+ props.selected.forEach( (field: APIQueryField) => {
+ // Valid visits according to the dictionary
+ const category = props.fulldictionary[field.module][field.category];
+ const dict = category[field.field];
- if (dict.scope == 'candidate') {
- return;
- }
- if (typeof dict.visits !== 'undefined') {
- const newvisits = dict.visits.filter((visit) => {
- return props.defaultVisits.includes(visit);
- });
- field.visits = newvisits;
- modifiedvisits = true;
- }
- });
- if (modifiedvisits) {
- props.setSelected([...props.selected]);
+ if (dict.scope == 'candidate') {
+ return;
}
+ if (typeof dict.visits !== 'undefined') {
+ const newvisits = dict.visits.filter((visit) => {
+ return props.defaultVisits.includes(visit);
+ });
+ field.visits = newvisits;
+ modifiedvisits = true;
+ }
+ });
+ if (modifiedvisits) {
+ props.setSelected([...props.selected]);
+ }
}, [syncVisits, props.defaultVisits]);
const displayed: string[] = Object.keys(
props.displayedFields || {}
- ).filter((value) => {
- if (activeFilter === '') {
- // No filter set
- return true;
- }
+ ).filter((value) => {
+ if (activeFilter === '') {
+ // No filter set
+ return true;
+ }
- // Filter with a case insensitive comparison to either the description or
- // the field name displayed to the user
- const lowerFilter = activeFilter.toLowerCase();
- const desc = props.displayedFields[value].description;
- return (value.toLowerCase().includes(lowerFilter)
+ // Filter with a case insensitive comparison to either the description or
+ // the field name displayed to the user
+ const lowerFilter = activeFilter.toLowerCase();
+ const desc = props.displayedFields[value].description;
+ return (value.toLowerCase().includes(lowerFilter)
|| desc.toLowerCase().includes(lowerFilter));
});
const fields = displayed.map((item: string) => {
- /**
- * Return true if this element equals
- * the selected.
- *
- * @param {APIQueryField} element - The element
- * @returns {boolean} - true if equal
- */
- const equalField = (element: APIQueryField) => {
- return (element.module == props.module
+ /**
+ * Return true if this element equals
+ * the selected.
+ *
+ * @param {APIQueryField} element - The element
+ * @returns {boolean} - true if equal
+ */
+ const equalField = (element: APIQueryField) => {
+ return (element.module == props.module
&& element.category === props.category
&& element.field == item);
- };
- const selobj = props.selected.find(equalField);
- return setZoomTo(null)}
- key={item}
- item={item}
- value={props.displayedFields[item]}
- selected={selobj}
- module={props.module}
- category={props.category}
- onFieldToggle={props.onFieldToggle}
- onChangeVisitList={props.onChangeVisitList}
- defaultVisits={props.defaultVisits}
- />;
+ };
+ const selobj = props.selected.find(equalField);
+ return setZoomTo(null)}
+ key={item}
+ item={item}
+ value={props.displayedFields[item]}
+ selected={selobj}
+ module={props.module}
+ category={props.category}
+ onFieldToggle={props.onFieldToggle}
+ onChangeVisitList={props.onChangeVisitList}
+ defaultVisits={props.defaultVisits}
+ />;
});
/**
@@ -277,8 +277,8 @@ function DefineFields(props: {
* @param {React.ChangeEventHandler} e - The mouse event
*/
const setFilter = (e: React.FormEvent) => {
- const target = e.target as HTMLInputElement;
- setActiveFilter(target.value);
+ const target = e.target as HTMLInputElement;
+ setActiveFilter(target.value);
};
/**
@@ -287,24 +287,24 @@ function DefineFields(props: {
* @returns {void}
*/
const addAll = () => {
- const toAdd = displayed.map((item) => {
- const dict = props.displayedFields[item];
- const retObj: APIQueryField = {
- module: props.module,
- category: props.category,
- field: item,
- };
- // Only include defined visits which intersect
- // with the default ones, convert to the react-select
- // format used internally.
- if (dict.visits) {
- retObj['visits'] = dict.visits.filter((visit) => {
- return props.defaultVisits.includes(visit);
- });
- }
- return retObj;
- });
- props.onAddAll(toAdd);
+ const toAdd = displayed.map((item) => {
+ const dict = props.displayedFields[item];
+ const retObj: APIQueryField = {
+ module: props.module,
+ category: props.category,
+ field: item,
+ };
+ // Only include defined visits which intersect
+ // with the default ones, convert to the react-select
+ // format used internally.
+ if (dict.visits) {
+ retObj['visits'] = dict.visits.filter((visit) => {
+ return props.defaultVisits.includes(visit);
+ });
+ }
+ return retObj;
+ });
+ props.onAddAll(toAdd);
};
/**
* Removes all items from the currently selected category
@@ -312,40 +312,40 @@ function DefineFields(props: {
* @returns {void}
*/
const removeAll = () => {
- const toRemove = displayed.map((item) => {
- const dict = props.displayedFields[item];
- return {
- module: props.module,
- category: props.category,
- field: item,
- dictionary: dict,
- };
- });
- props.onRemoveAll(toRemove);
+ const toRemove = displayed.map((item) => {
+ const dict = props.displayedFields[item];
+ return {
+ module: props.module,
+ category: props.category,
+ field: item,
+ dictionary: dict,
+ };
+ });
+ props.onRemoveAll(toRemove);
};
let fieldList: React.ReactElement|null = null;
if (props.category) {
- // Put into a short variable name for line length
- const mCategories = props.allCategories.categories[props.module];
- const cname = mCategories[props.category];
- let defaultVisits;
- if (props.defaultVisits) {
- const allVisits = props.allVisits.map((el) => {
- return {value: el, label: el};
- });
- const selectedVisits = props.defaultVisits.map((el) => {
- return {value: el, label: el};
- });
- defaultVisits =
-
Default Visits
-
{
+ return {value: el, label: el};
+ });
+ const selectedVisits = props.defaultVisits.map((el) => {
+ return {value: el, label: el};
+ });
+ defaultVisits =
+
Default Visits
+
({...base, zIndex: 9999}),
- }
- }
- value={selectedVisits}
- closeMenuOnSelect={false}
- />
-
- setSyncVisits(value)
- } />
-
- ;
- }
+ }
+ }
+ value={selectedVisits}
+ closeMenuOnSelect={false}
+ />
+
+ setSyncVisits(value)
+ } />
+
+ ;
+ }
- fieldList = (
-
-
{cname} fields
-
- {defaultVisits}
-
-
-
-
-
-
-
-
+ fieldList = (
+
+
{cname} fields
+
+ {defaultVisits}
+
+
+
+
+
+
+
+
Add all
-
-
+
+
Remove all
-
-
-
-
-
{fields}
-
);
+
+
+
+
+
{fields}
+
);
}
return (
-
-
-
Available Fields
- props.allCategories.modules[key]}
- onChange={props.onCategoryChange}
- />
- {fieldList}
-
-
+
+
Available Fields
+ props.allCategories.modules[key]}
+ onChange={props.onCategoryChange}
+ />
+ {fieldList}
+
+
-
+
-
Selected Fields
-
- Clear
-
+
Selected Fields
+
+ Clear
+
{
- setZoomTo(item);
- props.onCategoryChange(module, category);
+ selected={props.selected}
+ removeField={props.removeField}
+ fulldictionary={props.fulldictionary}
+ setSelected={props.setSelected}
+ snapToView={
+ (module: string, category: string, item: string) => {
+ setZoomTo(item);
+ props.onCategoryChange(module, category);
}}
/>
+
-
-
);
+
);
}
/**
@@ -490,125 +490,125 @@ function SelectedFieldList(props: {
* @returns {void}
*/
const moveSelected = () => {
- if (draggingIdx=== null || droppingIdx === null) {
- return;
- }
- const newSelected: APIQueryField[] = props.selected;
+ if (draggingIdx=== null || droppingIdx === null) {
+ return;
+ }
+ const newSelected: APIQueryField[] = props.selected;
- const removed: APIQueryField = newSelected.splice(draggingIdx, 1)[0];
- const newIdx: number|null = (droppingIdx||0 <= draggingIdx||0)
- ? droppingIdx
- : (droppingIdx - 1);
+ const removed: APIQueryField = newSelected.splice(draggingIdx, 1)[0];
+ const newIdx: number|null = (droppingIdx||0 <= draggingIdx||0)
+ ? droppingIdx
+ : (droppingIdx - 1);
- if (newIdx == null) {
- return;
- }
- newSelected.splice(
- newIdx,
- 0,
- removed,
- );
- props.setSelected([...newSelected]);
- setDroppingIdx(null);
- setDraggingIdx(null);
+ if (newIdx == null) {
+ return;
+ }
+ newSelected.splice(
+ newIdx,
+ 0,
+ removed,
+ );
+ props.setSelected([...newSelected]);
+ setDroppingIdx(null);
+ setDraggingIdx(null);
};
const fields = props.selected.map((item, i) => {
- /**
- * Removes an item from the selected
- *
- * @param {APIQueryField} item - The field to remove
- * @returns {void}
- */
- const removeField = (item: APIQueryField) => {
- props.removeField(item.module, item.category, item.field);
+ /**
+ * Removes an item from the selected
+ *
+ * @param {APIQueryField} item - The field to remove
+ * @returns {void}
+ */
+ const removeField = (item: APIQueryField) => {
+ props.removeField(item.module, item.category, item.field);
+ };
+ const style: React.CSSProperties = {display: 'flex',
+ flexWrap: 'nowrap' as const,
+ cursor: 'grab',
+ justifyContent: 'space-between'};
+ if (removingIdx === i) {
+ style.textDecoration = 'line-through' as const;
+ }
+ if (droppingIdx === i) {
+ style.borderTop = 'thin solid black';
+ }
+ if (draggingIdx == i) {
+ style.background = '#f5f5f5';
+ }
+ let fieldvisits;
+ if (item.visits) {
+ const style = {
+ fontStyle: 'italic',
+ color: '#aaa',
+ fontSize: '0.7em',
+ marginLeft: 20,
};
- const style: React.CSSProperties = {display: 'flex',
- flexWrap: 'nowrap' as const,
- cursor: 'grab',
- justifyContent: 'space-between'};
- if (removingIdx === i) {
- style.textDecoration = 'line-through' as const;
- }
- if (droppingIdx === i) {
- style.borderTop = 'thin solid black';
- }
- if (draggingIdx == i) {
- style.background = '#f5f5f5';
- }
- let fieldvisits;
- if (item.visits) {
- const style = {
- fontStyle: 'italic',
- color: '#aaa',
- fontSize: '0.7em',
- marginLeft: 20,
- };
- fieldvisits = {item.visits.join(', ')} ;
- }
- return ( {
- props.snapToView(item.module, item.category, item.field);
- }}
- onDragStart={() => {
- setDraggingIdx(i);
- }}
+ fieldvisits =
{item.visits.join(', ')} ;
+ }
+ return (
{
+ props.snapToView(item.module, item.category, item.field);
+ }}
+ onDragStart={() => {
+ setDraggingIdx(i);
+ }}
- onDragEnd={() => {
- setDraggingIdx(null);
- setDroppingIdx(null);
- }}
+ onDragEnd={() => {
+ setDraggingIdx(null);
+ setDroppingIdx(null);
+ }}
- onDragEnter={() => {
- setDroppingIdx(i);
- }}
+ onDragEnter={() => {
+ setDroppingIdx(i);
+ }}
- onDragOver ={(e) => {
- e.stopPropagation();
- e.preventDefault();
- }}
- onDrop={() => moveSelected()}
- >
-
-
{item.field}
- {getDictionaryDescription(
- item.module,
- item.category,
- item.field,
- props.fulldictionary,
- )}
- {fieldvisits}
-
-
setRemovingIdx(i)}
- onMouseLeave={() => setRemovingIdx(null)}>
- {
- removeField(item);
- setRemovingIdx(null);
- }}
- style={{cursor: 'pointer'}} />
-
-
);
+ onDragOver ={(e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ onDrop={() => moveSelected()}
+ >
+
+
{item.field}
+ {getDictionaryDescription(
+ item.module,
+ item.category,
+ item.field,
+ props.fulldictionary,
+ )}
+ {fieldvisits}
+
+
setRemovingIdx(i)}
+ onMouseLeave={() => setRemovingIdx(null)}>
+ {
+ removeField(item);
+ setRemovingIdx(null);
+ }}
+ style={{cursor: 'pointer'}} />
+
+
);
});
if (draggingIdx !== null) {
- // Add a sink after the last element, so that we can drop on
- // the end
- const style: React.CSSProperties = {height: 50};
- const nItems = fields.length;
- if (droppingIdx === nItems) {
- style.borderTop = 'thin solid black' as const;
- }
- fields.push( setDroppingIdx(nItems) }
- onDragOver ={(e) => {
- e.stopPropagation();
- e.preventDefault();
- }}
- onDrop={() => moveSelected()}>
-
);
+ // Add a sink after the last element, so that we can drop on
+ // the end
+ const style: React.CSSProperties = {height: 50};
+ const nItems = fields.length;
+ if (droppingIdx === nItems) {
+ style.borderTop = 'thin solid black' as const;
+ }
+ fields.push( setDroppingIdx(nItems) }
+ onDragOver ={(e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ onDrop={() => moveSelected()}>
+
);
}
return {fields}
;
diff --git a/modules/dataquery/jsx/definefilters.addfiltermodal.tsx b/modules/dataquery/jsx/definefilters.addfiltermodal.tsx
index b24349826ad..42dff210300 100644
--- a/modules/dataquery/jsx/definefilters.addfiltermodal.tsx
+++ b/modules/dataquery/jsx/definefilters.addfiltermodal.tsx
@@ -4,18 +4,18 @@ import Modal from 'jsx/Modal';
import Select from 'react-select';
import swal from 'sweetalert2';
import {
- NumericElement,
- DateElement,
- TimeElement,
- SelectElement,
- TextboxElement,
+ NumericElement,
+ DateElement,
+ TimeElement,
+ SelectElement,
+ TextboxElement,
} from 'jsx/Form';
import {QueryTerm, QueryGroup} from './querydef';
import {
- Operators,
- FieldDictionary,
- DictionaryCategory,
- VisitOption,
+ Operators,
+ FieldDictionary,
+ DictionaryCategory,
+ VisitOption,
} from './types';
import {CategoriesAPIReturn} from './hooks/usedatadictionary';
@@ -33,27 +33,27 @@ function VisitList(props: {
options: string[],
onChange: (newvals: string[]) => void,
}) {
- const selectOptions: VisitOption[] = props.options.map(
- (vl) => {
- return {value: vl, label: vl};
- }
- );
+ const selectOptions: VisitOption[] = props.options.map(
+ (vl) => {
+ return {value: vl, label: vl};
+ }
+ );
- const selectedVisits = selectOptions.filter((opt) => {
- return props.selected.includes(opt.value);
- });
+ const selectedVisits = selectOptions.filter((opt) => {
+ return props.selected.includes(opt.value);
+ });
- return {
- props.onChange(
- newvals.map((valobj) => valobj.value)
- );
- }}
- placeholder='Select Visits'
- value={selectedVisits}
- menuPortalTarget={document.body}
- styles={{menuPortal:
+ return {
+ props.onChange(
+ newvals.map((valobj) => valobj.value)
+ );
+ }}
+ placeholder='Select Visits'
+ value={selectedVisits}
+ menuPortalTarget={document.body}
+ styles={{menuPortal:
/**
* Required for rendering properly on top of window.
*
@@ -61,9 +61,9 @@ function VisitList(props: {
* @returns {object} - The new CSS object
*/
(base) => ({...base, zIndex: 9999})}
- }
- closeMenuOnSelect={false}
- />;
+ }
+ closeMenuOnSelect={false}
+ />;
}
/**
@@ -92,186 +92,186 @@ function AddFilterModal(props: {
module: string,
category: string,
}) {
- let fieldSelect;
- let criteriaSelect;
- let visitSelect;
- let cardinalityWarning;
- const [fieldDictionary, setFieldDictionary]
+ let fieldSelect;
+ let criteriaSelect;
+ let visitSelect;
+ let cardinalityWarning;
+ const [fieldDictionary, setFieldDictionary]
= useState(null);
- const [fieldname, setFieldname] = useState(null);
- const [op, setOp] = useState(null);
- const [value, setValue] = useState('');
- const [selectedVisits, setSelectedVisits] = useState(null);
+ const [fieldname, setFieldname] = useState(null);
+ const [op, setOp] = useState(null);
+ const [value, setValue] = useState('');
+ const [selectedVisits, setSelectedVisits] = useState(null);
- if (props.displayedFields) {
- const options: { Fields: {[key: string]: string}} = {'Fields': {}};
- for (const [key, value] of Object.entries(props.displayedFields)) {
- options['Fields'][key] = value.description;
- }
- fieldSelect = {
- const dict = props.displayedFields[fieldname];
- setFieldDictionary(dict);
- setFieldname(fieldname);
- if (dict.visits) {
- setSelectedVisits(dict.visits);
- }
- }}
- placeholder="Select a field" />;
+ if (props.displayedFields) {
+ const options: { Fields: {[key: string]: string}} = {'Fields': {}};
+ for (const [key, value] of Object.entries(props.displayedFields)) {
+ options['Fields'][key] = value.description;
}
-
- if (fieldDictionary) {
- let valueSelect;
- if (op) {
- valueSelect = valueInput(fieldDictionary, op, value, setValue);
+ fieldSelect = {
+ const dict = props.displayedFields[fieldname];
+ setFieldDictionary(dict);
+ setFieldname(fieldname);
+ if (dict.visits) {
+ setSelectedVisits(dict.visits);
}
+ }}
+ placeholder="Select a field" />;
+ }
+
+ if (fieldDictionary) {
+ let valueSelect;
+ if (op) {
+ valueSelect = valueInput(fieldDictionary, op, value, setValue);
+ }
- criteriaSelect =
-
Criteria
-
-
- {
- setOp(operator as Operators);
- }}
- placeholder="Select an operator"
- />
-
-
{valueSelect}
-
-
;
+ criteriaSelect =
+
Criteria
+
+
+ {
+ setOp(operator as Operators);
+ }}
+ placeholder="Select an operator"
+ />
+
+
{valueSelect}
+
+
;
if (fieldDictionary.scope == 'session' && fieldDictionary.visits) {
- visitSelect = e.stopPropagation()}>
-
for at least one of the following visits
-
- ;
+ visitSelect = e.stopPropagation()}>
+
for at least one of the following visits
+
+ ;
}
if (fieldDictionary.cardinality == 'many') {
- cardinalityWarning =
-
-
This field may exist multiple times for a
+ cardinalityWarning =
+
+
This field may exist multiple times for a
single {fieldDictionary.scope}. Adding a criteria
based on it means that it must match for at least
one of the data points.
-
;
- }
+
;
}
+ }
- /**
- * Function that returns a promise on the modal's submission to check
- * if the input is valid.
- *
- * @returns {Promise} - Promise that resolves if input is valid and rejects otherwise
- */
- const submitPromise = () =>
- new Promise((resolve, reject) => {
- // Validate and reject if invalid
- if (!fieldname) {
- swal.fire({
- type: 'error',
- title: 'Invalid field',
- text: 'You must select a field for the criteria.',
- });
- reject();
- return;
- }
- if (!op) {
- swal.fire({
- type: 'error',
- title: 'Invalid operator',
- text: 'You must select an operator for the criteria.',
- });
- reject();
- return;
- }
+ /**
+ * Function that returns a promise on the modal's submission to check
+ * if the input is valid.
+ *
+ * @returns {Promise} - Promise that resolves if input is valid and rejects otherwise
+ */
+ const submitPromise = () =>
+ new Promise((resolve, reject) => {
+ // Validate and reject if invalid
+ if (!fieldname) {
+ swal.fire({
+ type: 'error',
+ title: 'Invalid field',
+ text: 'You must select a field for the criteria.',
+ });
+ reject();
+ return;
+ }
+ if (!op) {
+ swal.fire({
+ type: 'error',
+ title: 'Invalid operator',
+ text: 'You must select an operator for the criteria.',
+ });
+ reject();
+ return;
+ }
- if (!value) {
- if (op != 'isnotnull' && op != 'isnull'
+ if (!value) {
+ if (op != 'isnotnull' && op != 'isnull'
&& op != 'exists' && op != 'notexists') {
- swal.fire({
- type: 'error',
- title: 'Invalid value',
- text: 'You must enter a value to compare the ' +
+ swal.fire({
+ type: 'error',
+ title: 'Invalid value',
+ text: 'You must enter a value to compare the ' +
'field against.',
- });
- reject();
- return;
- }
- }
+ });
+ reject();
+ return;
+ }
+ }
- if (fieldDictionary && fieldDictionary.scope == 'session') {
- if (!selectedVisits || selectedVisits.length == 0) {
- swal.fire({
- type: 'error',
- title: 'Invalid visits',
- text: 'No visits selected for criteria.',
- });
- reject();
- return;
- }
- }
+ if (fieldDictionary && fieldDictionary.scope == 'session') {
+ if (!selectedVisits || selectedVisits.length == 0) {
+ swal.fire({
+ type: 'error',
+ title: 'Invalid visits',
+ text: 'No visits selected for criteria.',
+ });
+ reject();
+ return;
+ }
+ }
- // It's been validated, now save the data and resolve
- const crit: QueryTerm = new QueryTerm(
- props.module,
- props.category,
- fieldname,
- op,
- value,
- );
- if (fieldDictionary && fieldDictionary.scope == 'session') {
- crit.visits = selectedVisits || [];
- }
+ // It's been validated, now save the data and resolve
+ const crit: QueryTerm = new QueryTerm(
+ props.module,
+ props.category,
+ fieldname,
+ op,
+ value,
+ );
+ if (fieldDictionary && fieldDictionary.scope == 'session') {
+ crit.visits = selectedVisits || [];
+ }
- props.addQueryGroupItem(
- props.query,
- crit,
- );
+ props.addQueryGroupItem(
+ props.query,
+ crit,
+ );
- props.closeModal();
- resolve(null);
- }
+ props.closeModal();
+ resolve(null);
+ }
);
- return
-
-
Field
-
-
- props.categories.modules[key]}
- onChange={props.onCategoryChange} />
-
-
- {fieldSelect}
-
-
- {cardinalityWarning}
- {criteriaSelect}
- {visitSelect}
-
- ;
+ return
+
+
Field
+
+
+ props.categories.modules[key]}
+ onChange={props.onCategoryChange} />
+
+
+ {fieldSelect}
+
+
+ {cardinalityWarning}
+ {criteriaSelect}
+ {visitSelect}
+
+ ;
}
@@ -282,59 +282,59 @@ function AddFilterModal(props: {
* @returns {object} - list of options for this dictionary
*/
function getOperatorOptions(dict: FieldDictionary) {
- let options: {[operator: string]: string};
+ let options: {[operator: string]: string};
- if (dict.type == 'integer' || dict.type == 'date' ||
+ if (dict.type == 'integer' || dict.type == 'date' ||
dict.type == 'interval' || dict.type == 'time' ||
dict.type == 'decimal') {
- // Comparable data types
- options = {
- 'lt': '<',
- 'lte': '≤',
- 'eq': '=',
- 'neq': '≠',
- 'gte': '≥',
- 'gt': '>',
- };
- } else if (dict.type == 'enumeration') {
- // Enumerations are a dropdown. Comparable operators
- // are meaningless, but the options are a dropdown
- // and we might be looking for an option "in" any
- // of the selected choices.
- options = {
- 'eq': '=',
- 'neq': '≠',
- 'in': 'in',
- };
- } else if (dict.type == 'string' ||
+ // Comparable data types
+ options = {
+ 'lt': '<',
+ 'lte': '≤',
+ 'eq': '=',
+ 'neq': '≠',
+ 'gte': '≥',
+ 'gt': '>',
+ };
+ } else if (dict.type == 'enumeration') {
+ // Enumerations are a dropdown. Comparable operators
+ // are meaningless, but the options are a dropdown
+ // and we might be looking for an option "in" any
+ // of the selected choices.
+ options = {
+ 'eq': '=',
+ 'neq': '≠',
+ 'in': 'in',
+ };
+ } else if (dict.type == 'string' ||
dict.type == 'URI') {
- // We might be looking for a substring.
- options = {
- 'eq': '=',
- 'neq': '≠',
- 'startsWith': 'starts with',
- 'contains': 'contains',
- 'endsWith': 'ends with',
- };
- } else {
- // fall back to == and !=, valid for any type.
- options = {'eq': '=', 'neq': '≠'};
- }
+ // We might be looking for a substring.
+ options = {
+ 'eq': '=',
+ 'neq': '≠',
+ 'startsWith': 'starts with',
+ 'contains': 'contains',
+ 'endsWith': 'ends with',
+ };
+ } else {
+ // fall back to == and !=, valid for any type.
+ options = {'eq': '=', 'neq': '≠'};
+ }
- // Possible cardinalities are unique, single,
- // optional, or many. Unique and single don't
- // change the possible operators, optional or
- // 1-many cardinalities have a couple more
- // things you can check.
- if (dict.cardinality == 'optional') {
- options['isnotnull'] = 'has data';
- options['isnull'] = 'has no data';
- } else if (dict.cardinality == 'many') {
- options['exists'] = 'exists';
- options['notexists'] = 'does not exist';
- options['numberof'] = 'number of';
- }
- return options;
+ // Possible cardinalities are unique, single,
+ // optional, or many. Unique and single don't
+ // change the possible operators, optional or
+ // 1-many cardinalities have a couple more
+ // things you can check.
+ if (dict.cardinality == 'optional') {
+ options['isnotnull'] = 'has data';
+ options['isnull'] = 'has no data';
+ } else if (dict.cardinality == 'many') {
+ options['exists'] = 'exists';
+ options['notexists'] = 'does not exist';
+ options['numberof'] = 'number of';
+ }
+ return options;
}
@@ -349,101 +349,101 @@ function getOperatorOptions(dict: FieldDictionary) {
* @returns {React.ReactElement} - the react element
*/
function valueInput(fielddict: FieldDictionary,
- op: Operators,
- value: string|string[],
- setValue: (val: string) => void
+ op: Operators,
+ value: string|string[],
+ setValue: (val: string) => void
) {
- const vs: string = value as string;
- switch (op) {
- case 'exists':
- case 'notexists':
- case 'isnull':
- case 'isnotnull':
- return
;
- case 'numberof':
- return
setValue(value)} />;
- }
+ const vs: string = value as string;
+ switch (op) {
+ case 'exists':
+ case 'notexists':
+ case 'isnull':
+ case 'isnotnull':
+ return ;
+ case 'numberof':
+ return setValue(value)} />;
+ }
- switch (fielddict.type) {
- case 'date':
- return setValue(value)} />;
- case 'time':
- // There's no time element type in LORIS, so use the HTML5
- // one with bootstrap styling that matches the rest of our
- // elements
- return setValue(value)
- }
- />;
- case 'URI':
- // Should this be input type="url"?
- return setValue(value)}
- name='value'
- value={vs} />;
- case 'integer':
- return setValue(value)} />;
- case 'boolean':
- return {
- setValue(value);
- }}
- placeholder="Select a value"
- />;
- case 'enumeration':
- const opts: {[key: string]: string} = {};
- for (let i = 0;
- fielddict.options && i < fielddict.options.length;
- i++
- ) {
- const opt = fielddict.options[i];
- if (fielddict.labels) {
- opts[opt] = fielddict.labels[i];
- } else {
- opts[opt] = opt;
- }
- }
- if (op == 'in') {
- return setValue(value)}
- value={value}
- sortByValue={false}
- />;
- }
- return {
- setValue(value);
- }}
- placeholder="Select a value"
- />;
- default:
- return setValue(value)}
- name='value'
- value={vs} />;
- }
+ switch (fielddict.type) {
+ case 'date':
+ return setValue(value)} />;
+ case 'time':
+ // There's no time element type in LORIS, so use the HTML5
+ // one with bootstrap styling that matches the rest of our
+ // elements
+ return setValue(value)
+ }
+ />;
+ case 'URI':
+ // Should this be input type="url"?
+ return setValue(value)}
+ name='value'
+ value={vs} />;
+ case 'integer':
+ return setValue(value)} />;
+ case 'boolean':
+ return {
+ setValue(value);
+ }}
+ placeholder="Select a value"
+ />;
+ case 'enumeration':
+ const opts: {[key: string]: string} = {};
+ for (let i = 0;
+ fielddict.options && i < fielddict.options.length;
+ i++
+ ) {
+ const opt = fielddict.options[i];
+ if (fielddict.labels) {
+ opts[opt] = fielddict.labels[i];
+ } else {
+ opts[opt] = opt;
+ }
+ }
+ if (op == 'in') {
+ return setValue(value)}
+ value={value}
+ sortByValue={false}
+ />;
+ }
+ return {
+ setValue(value);
+ }}
+ placeholder="Select a value"
+ />;
+ default:
+ return setValue(value)}
+ name='value'
+ value={vs} />;
+ }
}
export default AddFilterModal;
diff --git a/modules/dataquery/jsx/definefilters.importcsvmodal.tsx b/modules/dataquery/jsx/definefilters.importcsvmodal.tsx
index 512f6bd837b..2246203ecb1 100644
--- a/modules/dataquery/jsx/definefilters.importcsvmodal.tsx
+++ b/modules/dataquery/jsx/definefilters.importcsvmodal.tsx
@@ -17,192 +17,192 @@ function ImportCSVModal(props: {
setQuery: (root: QueryGroup) => void,
closeModal: () => void,
}) {
- const [csvFile, setCSVFile] = useState(null);
- const [csvHeader, setCSVHeader] = useState(false);
- const [csvType, setCSVType] = useState('candidate');
- const [idType, setIdType] = useState('PSCID');
- /**
- * Promise for handling modal closing. Always accepts.
- *
- * @returns {Promise} - a stub promise
- */
- const submitPromise = () =>
- new Promise((resolve) => {
- resolve(null);
- }
+ const [csvFile, setCSVFile] = useState(null);
+ const [csvHeader, setCSVHeader] = useState(false);
+ const [csvType, setCSVType] = useState('candidate');
+ const [idType, setIdType] = useState('PSCID');
+ /**
+ * Promise for handling modal closing. Always accepts.
+ *
+ * @returns {Promise} - a stub promise
+ */
+ const submitPromise = () =>
+ new Promise((resolve) => {
+ resolve(null);
+ }
);
- const candIDRegex = new RegExp('^[1-9][0-9]{5}$');
+ const candIDRegex = new RegExp('^[1-9][0-9]{5}$');
- /**
- * Callback function for after papaparse has parsed the csv
- *
- * @param {any} value - the value from papaparse callback
- */
- const csvParsed = (value: Papa.ParseResult) => {
- if (value.errors && value.errors.length > 0) {
- console.error(value.errors);
- swal.fire({
- type: 'error',
- title: 'Invalid CSV',
- text: 'Could not parse CSV file',
- });
- }
+ /**
+ * Callback function for after papaparse has parsed the csv
+ *
+ * @param {any} value - the value from papaparse callback
+ */
+ const csvParsed = (value: Papa.ParseResult) => {
+ if (value.errors && value.errors.length > 0) {
+ console.error(value.errors);
+ swal.fire({
+ type: 'error',
+ title: 'Invalid CSV',
+ text: 'Could not parse CSV file',
+ });
+ }
- // If candidates: validate 1 column
- // If sessions: validate 2 columns
- const expectedLength = (csvType === 'session' ? 2 : 1);
- const startLine = csvHeader ? 1 : 0;
- for (let i = startLine; i < value.data.length; i++) {
- if (value.data[i].length != expectedLength) {
- swal.fire({
- type: 'error',
- title: 'Invalid CSV',
- text: 'Expected ' + expectedLength + ' columns in CSV.'
+ // If candidates: validate 1 column
+ // If sessions: validate 2 columns
+ const expectedLength = (csvType === 'session' ? 2 : 1);
+ const startLine = csvHeader ? 1 : 0;
+ for (let i = startLine; i < value.data.length; i++) {
+ if (value.data[i].length != expectedLength) {
+ swal.fire({
+ type: 'error',
+ title: 'Invalid CSV',
+ text: 'Expected ' + expectedLength + ' columns in CSV.'
+ ' Got ' + value.data[i].length + ' on line ' +
(i+1) + '.',
- });
- return;
- }
- if (idType === 'CandID') {
- if (candIDRegex.test(value.data[i][0]) !== true) {
- swal.fire({
- type: 'error',
- title: 'Invalid DCC ID',
- text: 'Invalid DCC ID (' + value.data[i][0]
+ });
+ return;
+ }
+ if (idType === 'CandID') {
+ if (candIDRegex.test(value.data[i][0]) !== true) {
+ swal.fire({
+ type: 'error',
+ title: 'Invalid DCC ID',
+ text: 'Invalid DCC ID (' + value.data[i][0]
+ ') on line '
+ (i+1) + '.',
- });
- }
- }
+ });
}
+ }
+ }
- // Now that it's been validated, build a new query
- const newQuery = new QueryGroup('or');
- for (let i = startLine; i < value.data.length; i++) {
- if (csvType === 'session') {
- const sessionGroup = new QueryGroup('and');
- sessionGroup.addTerm(
- new QueryTerm(
- 'candidate_parameters',
- 'Identifiers',
- idType,
- 'eq',
- value.data[i][0],
- ),
- );
- sessionGroup.addTerm(
- new QueryTerm(
- 'candidate_parameters',
- 'Meta',
- 'VisitLabel',
- 'eq',
- value.data[i][1],
- ),
- );
- newQuery.group.push(sessionGroup);
- } else {
- newQuery.addTerm(
- new QueryTerm(
- 'candidate_parameters',
- 'Identifiers',
- idType,
- 'eq',
- value.data[i][0],
- ),
- );
- }
- }
+ // Now that it's been validated, build a new query
+ const newQuery = new QueryGroup('or');
+ for (let i = startLine; i < value.data.length; i++) {
+ if (csvType === 'session') {
+ const sessionGroup = new QueryGroup('and');
+ sessionGroup.addTerm(
+ new QueryTerm(
+ 'candidate_parameters',
+ 'Identifiers',
+ idType,
+ 'eq',
+ value.data[i][0],
+ ),
+ );
+ sessionGroup.addTerm(
+ new QueryTerm(
+ 'candidate_parameters',
+ 'Meta',
+ 'VisitLabel',
+ 'eq',
+ value.data[i][1],
+ ),
+ );
+ newQuery.group.push(sessionGroup);
+ } else {
+ newQuery.addTerm(
+ new QueryTerm(
+ 'candidate_parameters',
+ 'Identifiers',
+ idType,
+ 'eq',
+ value.data[i][0],
+ ),
+ );
+ }
+ }
- props.setQuery(newQuery);
- props.closeModal();
- };
+ props.setQuery(newQuery);
+ props.closeModal();
+ };
- const dtstyle = {
- marginLeft: '1em',
- marginTop: '1em',
- };
+ const dtstyle = {
+ marginLeft: '1em',
+ marginTop: '1em',
+ };
- return
-
+ ;
}
export default ImportCSVModal;
diff --git a/modules/dataquery/jsx/definefilters.tsx b/modules/dataquery/jsx/definefilters.tsx
index bce5c2cc9ea..ddeb64baebd 100644
--- a/modules/dataquery/jsx/definefilters.tsx
+++ b/modules/dataquery/jsx/definefilters.tsx
@@ -60,342 +60,342 @@ function DefineFilters(props: {
addNewQueryGroup: (group: QueryGroup) => void,
removeQueryGroupItem: (group: QueryGroup, i: number) => QueryGroup,
}) : React.ReactElement {
- let displayquery: React.ReactNode = null;
- const [addModal, setAddModal] = useState(false);
- const [csvModal, setCSVModal] = useState(false);
- const [showAdvanced, setShowAdvanced] = useState(false);
- // The subgroup used for the "Add Filter" modal window
- // to add to. Default to top level unless click from a
- // query group, in which case the callback changes it
- // to that group.
- const [modalQueryGroup, setModalGroup] = useState(props.query);
- const [deleteItemIndex, setDeleteItemIndex] = useState(null);
- const [queryMatches, setQueryMatches] = useState(null);
- useEffect(() => {
- setQueryMatches(null);
- const payload = calcPayload(props.fields, props.query);
+ let displayquery: React.ReactNode = null;
+ const [addModal, setAddModal] = useState(false);
+ const [csvModal, setCSVModal] = useState(false);
+ const [showAdvanced, setShowAdvanced] = useState(false);
+ // The subgroup used for the "Add Filter" modal window
+ // to add to. Default to top level unless click from a
+ // query group, in which case the callback changes it
+ // to that group.
+ const [modalQueryGroup, setModalGroup] = useState(props.query);
+ const [deleteItemIndex, setDeleteItemIndex] = useState(null);
+ const [queryMatches, setQueryMatches] = useState(null);
+ useEffect(() => {
+ setQueryMatches(null);
+ const payload = calcPayload(props.fields, props.query);
+ fetch(
+ '/dataquery/queries',
+ {
+ method: 'POST',
+ credentials: 'same-origin',
+ body: JSON.stringify(payload),
+ },
+ ).then(
+ (resp) => {
+ if (!resp.ok) {
+ throw new Error('Error creating query.');
+ }
+ return resp.json();
+ }
+ ).then(
+ (data) => {
fetch(
- '/dataquery/queries',
- {
- method: 'POST',
- credentials: 'same-origin',
- body: JSON.stringify(payload),
- },
- ).then(
- (resp) => {
- if (!resp.ok) {
- throw new Error('Error creating query.');
- }
- return resp.json();
- }
- ).then(
- (data) => {
- fetch(
- '/dataquery/queries/'
+ '/dataquery/queries/'
+ data.QueryID + '/count',
- {
- method: 'GET',
- credentials: 'same-origin',
- }
- ).then((resp) => {
- if (!resp.ok) {
- throw new Error('Could not get count.');
- }
- return resp.json();
- }).then((result) => {
- setQueryMatches(result.count);
- });
- }
- );
- }, [props.fields, props.query]);
+ {
+ method: 'GET',
+ credentials: 'same-origin',
+ }
+ ).then((resp) => {
+ if (!resp.ok) {
+ throw new Error('Could not get count.');
+ }
+ return resp.json();
+ }).then((result) => {
+ setQueryMatches(result.count);
+ });
+ }
+ );
+ }, [props.fields, props.query]);
- const bGroupStyle = {
- display: 'flex' as const,
- flexWrap: 'wrap' as const,
- marginTop: '1ex',
- };
+ const bGroupStyle = {
+ display: 'flex' as const,
+ flexWrap: 'wrap' as const,
+ marginTop: '1ex',
+ };
- const mapModuleName = props.mapModuleName;
- const mapCategoryName = props.mapCategoryName;
+ const mapModuleName = props.mapModuleName;
+ const mapCategoryName = props.mapCategoryName;
- const advancedLabel = showAdvanced ? 'Hide Advanced' : 'Show Advanced';
- let advancedButtons;
- const toggleAdvancedButton = (
+ const advancedLabel = showAdvanced ? 'Hide Advanced' : 'Show Advanced';
+ let advancedButtons;
+ const toggleAdvancedButton = (
+
+
+ {
+ e.preventDefault();
+ setShowAdvanced(!showAdvanced);
+ }}
+ />
+
+
+ );
+ if (props.query.group.length == 0) {
+ if (showAdvanced) {
+ advancedButtons = (
-
- {
- e.preventDefault();
- setShowAdvanced(!showAdvanced);
- }}
- />
-
-
- );
- if (props.query.group.length == 0) {
- if (showAdvanced) {
- advancedButtons = (
-
-
The "nested groups" options are advanced options for queries
+
The "nested groups" options are advanced options for queries
that do not have any specific condition at the
base of the query.
Use Add nested "or" condition groups
if
you need to build a query of the form.
- (a or b) and (c or d) [or (e and f)..] .
-
-
- {
- e.preventDefault();
- props.query.operator = 'and';
- props.addNewQueryGroup(props.query);
- }}
- />
-
-
+ (a or b) and (c or d) [or (e and f)..] .
+
+
+ {
+ e.preventDefault();
+ props.query.operator = 'and';
+ props.addNewQueryGroup(props.query);
+ }}
+ />
+
+
Use Add nested "and" condition groups
if you
need to build a query of the form
- (a and b) or (c and d) [or (e and f)..] .
-
-
- {
- e.preventDefault();
- props.query.operator = 'or';
- props.addNewQueryGroup(props.query);
- }}
- />
-
-
- );
- }
- // Only 1 add condition button since "and" or "or"
- // are the same with only 1 term
- displayquery =
-
-
Currently querying for ALL candidates.
-
You can add conditions by clicking one of the buttons below.
-
Click Add Condition
to add one or more conditions
+ (a and b) or (c and d) [or (e and f)..] .
+
+
+ {
+ e.preventDefault();
+ props.query.operator = 'or';
+ props.addNewQueryGroup(props.query);
+ }}
+ />
+
+
+ );
+ }
+ // Only 1 add condition button since "and" or "or"
+ // are the same with only 1 term
+ displayquery =
+
+
Currently querying for ALL candidates.
+
You can add conditions by clicking one of the buttons below.
+
Click Add Condition
to add one or more conditions
to your filters (ie. "Date Of Birth < 2015-02-15"). This is
most likely where you want to start your filters.
-
-
You can also import a population from a CSV by clicking
+
+
You can also import a population from a CSV by clicking
the Import from CSV
button.
-
The advanced options are for queries that do not have
+
The advanced options are for queries that do not have
a condition to add at the base of the query.
+
+
+
+
+
+ {
+ e.preventDefault();
+ setAddModal(true);
+ }}
+ />
-
-
-
-
- {
- e.preventDefault();
- setAddModal(true);
- }}
- />
-
-
- {
- e.preventDefault();
- // Need to be sure that we've loaded
- // candidate_parameters so it's in
- // fulldictionary
- props.getModuleFields(
- 'candidate_parameters',
- );
- setCSVModal(true);
- }}
- />
-
-
- {toggleAdvancedButton}
- {advancedButtons}
-
-
-
;
- } else if (props.query.group.length == 1 &&
+
+ {
+ e.preventDefault();
+ // Need to be sure that we've loaded
+ // candidate_parameters so it's in
+ // fulldictionary
+ props.getModuleFields(
+ 'candidate_parameters',
+ );
+ setCSVModal(true);
+ }}
+ />
+
+
+ {toggleAdvancedButton}
+ {advancedButtons}
+
+
+
;
+ } else if (props.query.group.length == 1 &&
props.query.group[0] instanceof QueryTerm
- ) {
- if (showAdvanced) {
- advancedButtons = (
-
-
-
Use New "and" subgroup
if the rest of the
+ ) {
+ if (showAdvanced) {
+ advancedButtons = (
+
+
+
Use New "and" subgroup
if the rest of the
query you need to write is a subgroup consisting
of "and" conditions. ie your query is of the form:
-
- (your condition above) or (c and d [and e and f..])
-
-
-
{
- e.preventDefault();
- props.query.operator = 'or';
- props.addNewQueryGroup(props.query);
- }} />
- Use New "or" subgroup
if the rest of the
+
+ (your condition above) or (c and d [and e and f..])
+
+
+ {
+ e.preventDefault();
+ props.query.operator = 'or';
+ props.addNewQueryGroup(props.query);
+ }} />
+ Use New "or" subgroup
if the rest of the
query you need to write is a subgroup consisting
of "or" conditions. ie your query is of the form:
-
- (your condition above) and (c or d [or e or f..])
-
-
- {
- e.preventDefault();
- props.query.operator = 'and';
- props.addNewQueryGroup(props.query);
- }} />
-
-
- );
- }
- // buttons for 1. Add "and" condition 2. Add "or" condition
- displayquery = (
-
Currently querying for any candidates with:
+
+ (your condition above) and (c or d [or e or f..])
+
+
+
{
+ e.preventDefault();
+ props.query.operator = 'and';
+ props.addNewQueryGroup(props.query);
+ }} />
+
+
+ );
+ }
+ // buttons for 1. Add "and" condition 2. Add "or" condition
+ displayquery = (
+
Currently querying for any candidates with:
-
-
-
-
-
{
- const newquery = props.removeQueryGroupItem(
- props.query,
- 0
- );
- setModalGroup(newquery);
- setDeleteItemIndex(null);
- }}
- onMouseEnter={() => setDeleteItemIndex(0)}
- onMouseLeave={() => setDeleteItemIndex(null)}
- style={{cursor: 'pointer'}} />
-
-
-
-
- {
- e.preventDefault();
- props.query.operator = 'and';
- setAddModal(true);
- }} />
- {
- e.preventDefault();
- setAddModal(true);
- props.query.operator = 'or';
- }} />
-
-
- {toggleAdvancedButton}
- {advancedButtons}
-
-
-
);
- } else {
- // Add buttons are delegated to the QueryTree rendering so they
- // can be placed at the right level
- displayquery =
-
Currently querying for any candidates with:
- {
- setModalGroup(group);
- setAddModal(true);
- }}
- setModalGroup={setModalGroup}
- backgroundColour='rgb(240, 240, 240)'
- newGroup={props.addNewQueryGroup}
- fulldictionary={props.fulldictionary}
- />
-
-
-
;
- }
- const modal = addModal ? (
-
setAddModal(false)}
- addQueryGroupItem={(querygroup, condition) => {
- const newquery = props.addQueryGroupItem(
- querygroup,
- condition,
+
+
+
{
+ const newquery = props.removeQueryGroupItem(
+ props.query,
+ 0
);
setModalGroup(newquery);
+ setDeleteItemIndex(null);
+ }}
+ onMouseEnter={() => setDeleteItemIndex(0)}
+ onMouseLeave={() => setDeleteItemIndex(null)}
+ style={{cursor: 'pointer'}} />
+
+
+
+
+ {
+ e.preventDefault();
+ props.query.operator = 'and';
+ setAddModal(true);
+ }} />
+ {
+ e.preventDefault();
+ setAddModal(true);
+ props.query.operator = 'or';
+ }} />
+
+
+ {toggleAdvancedButton}
+ {advancedButtons}
+
+
+ );
+ } else {
+ // Add buttons are delegated to the QueryTree rendering so they
+ // can be placed at the right level
+ displayquery =
+
Currently querying for any candidates with:
+
+
+ {
+ setModalGroup(group);
+ setAddModal(true);
}}
- categories={props.categories}
- onCategoryChange={props.onCategoryChange}
- displayedFields={props.displayedFields}
+ setModalGroup={setModalGroup}
+ backgroundColour='rgb(240, 240, 240)'
+ newGroup={props.addNewQueryGroup}
+ fulldictionary={props.fulldictionary}
+ />
+
+
+
;
+ }
+ const modal = addModal ? (
+ setAddModal(false)}
+ addQueryGroupItem={(querygroup, condition) => {
+ const newquery = props.addQueryGroupItem(
+ querygroup,
+ condition,
+ );
+ setModalGroup(newquery);
+ }}
+ categories={props.categories}
+ onCategoryChange={props.onCategoryChange}
+ displayedFields={props.displayedFields}
- module={props.module}
- category={props.category}
- />)
- : '';
- const csvModalHTML = csvModal ? (
- setCSVModal(false)}
- />
- ) : '';
+ module={props.module}
+ category={props.category}
+ />)
+ : '';
+ const csvModalHTML = csvModal ? (
+ setCSVModal(false)}
+ />
+ ) : '';
- const matchCount = queryMatches === null
- ?
// So the header doesn't jump around
- : Query matches {queryMatches} candidates
;
- return (
- {modal}
- {csvModalHTML}
-
-
Current Query
- {matchCount}
-
-
+ const matchCount = queryMatches === null
+ ?
// So the header doesn't jump around
+ : Query matches {queryMatches} candidates
;
+ return (
+ {modal}
+ {csvModalHTML}
+
+
Current Query
+ {matchCount}
+
+
Note that only candidates which you have permission to
access in LORIS are included in results. Number of
results may vary from other users running the same query.
-
- {displayquery}
-
- );
+
+ {displayquery}
+
+ );
}
export default DefineFilters;
diff --git a/modules/dataquery/jsx/fielddisplay.tsx b/modules/dataquery/jsx/fielddisplay.tsx
index 13b94664899..e58cad1f60a 100644
--- a/modules/dataquery/jsx/fielddisplay.tsx
+++ b/modules/dataquery/jsx/fielddisplay.tsx
@@ -23,22 +23,22 @@ function FieldDisplay(props: {
mapModuleName: (module: string) => string,
mapCategoryName: (module: string, category: string) => string,
}) {
- const description = getDictionaryDescription(
- props.module,
- props.category,
- props.fieldname,
- props.fulldictionary,
- );
+ const description = getDictionaryDescription(
+ props.module,
+ props.category,
+ props.fieldname,
+ props.fulldictionary,
+ );
- return (
-
- {description}
-
-
- {props.mapCategoryName(props.module, props.category)}
+ return (
+
+ {description}
+
+
+ {props.mapCategoryName(props.module, props.category)}
({props.mapModuleName(props.module)})
-
-
- );
+
+
+ );
}
export default FieldDisplay;
diff --git a/modules/dataquery/jsx/getdictionarydescription.tsx b/modules/dataquery/jsx/getdictionarydescription.tsx
index 02d5f9c32ed..c403f277214 100644
--- a/modules/dataquery/jsx/getdictionarydescription.tsx
+++ b/modules/dataquery/jsx/getdictionarydescription.tsx
@@ -10,20 +10,20 @@ import {FullDictionary} from './types';
* @returns {string} - the description if available, otherwise the fieldname
*/
function getDictionaryDescription(
- module: string,
- category: string,
- fieldname: string,
- dict: FullDictionary,
+ module: string,
+ category: string,
+ fieldname: string,
+ dict: FullDictionary,
): string {
- if (!dict
+ if (!dict
|| !dict[module]
|| !dict[module][category]
|| !dict[module][category][fieldname]
- ) {
- return fieldname;
- }
+ ) {
+ return fieldname;
+ }
- return dict[module][category][fieldname].description;
+ return dict[module][category][fieldname].description;
}
export default getDictionaryDescription;
diff --git a/modules/dataquery/jsx/hooks/usebreadcrumbs.tsx b/modules/dataquery/jsx/hooks/usebreadcrumbs.tsx
index be9023cdeba..ba65fb9c5b7 100644
--- a/modules/dataquery/jsx/hooks/usebreadcrumbs.tsx
+++ b/modules/dataquery/jsx/hooks/usebreadcrumbs.tsx
@@ -12,85 +12,85 @@ declare const loris: any;
* @param {function} setActiveTab - set the state on click
*/
function useBreadcrumbs(
- activeTab: string,
- setActiveTab: (newtab: string) => void
+ activeTab: string,
+ setActiveTab: (newtab: string) => void
) {
- // update breadcrumbs breadcrumbs
- useEffect(() => {
- const breadcrumbs = [
- {
- text: 'Data Query Tool (Beta)',
- /**
- * OnClick handler for the main breadcrumb
- *
- * @param {React.MouseEvent} e - Callback for when hovering over the delete icon
- * @returns {void}
- */
- onClick: (e: React.MouseEvent) => {
- e.preventDefault();
- setActiveTab('Info');
- },
- },
- ];
- if (activeTab == 'DefineFields'
+ // update breadcrumbs breadcrumbs
+ useEffect(() => {
+ const breadcrumbs = [
+ {
+ text: 'Data Query Tool (Beta)',
+ /**
+ * OnClick handler for the main breadcrumb
+ *
+ * @param {React.MouseEvent} e - Callback for when hovering over the delete icon
+ * @returns {void}
+ */
+ onClick: (e: React.MouseEvent) => {
+ e.preventDefault();
+ setActiveTab('Info');
+ },
+ },
+ ];
+ if (activeTab == 'DefineFields'
|| activeTab == 'DefineFilters'
|| activeTab == 'ViewData') {
- breadcrumbs.push({
- text: 'Define Fields',
- /**
- * OnClick handler for the define fields breadcrumb
- *
- * @param {React.MouseEventHandler} e - Callback for when hovering over the delete icon
- * @returns {void}
- */
- onClick: (e) => {
- e.preventDefault();
- setActiveTab('DefineFields');
- },
- });
- }
- if (activeTab == 'DefineFilters'
+ breadcrumbs.push({
+ text: 'Define Fields',
+ /**
+ * OnClick handler for the define fields breadcrumb
+ *
+ * @param {React.MouseEventHandler} e - Callback for when hovering over the delete icon
+ * @returns {void}
+ */
+ onClick: (e) => {
+ e.preventDefault();
+ setActiveTab('DefineFields');
+ },
+ });
+ }
+ if (activeTab == 'DefineFilters'
|| activeTab == 'ViewData') {
- breadcrumbs.push({
- text: 'Define Filters',
- /**
- * OnClick handler for the define filters breadcrumb
- *
- * @param {React.MouseEventHandler} e - Callback for when hovering over the delete icon
- * @returns {void}
- */
- onClick: (e) => {
- e.preventDefault();
- setActiveTab('DefineFilters');
- },
- });
- }
+ breadcrumbs.push({
+ text: 'Define Filters',
+ /**
+ * OnClick handler for the define filters breadcrumb
+ *
+ * @param {React.MouseEventHandler} e - Callback for when hovering over the delete icon
+ * @returns {void}
+ */
+ onClick: (e) => {
+ e.preventDefault();
+ setActiveTab('DefineFilters');
+ },
+ });
+ }
- if (activeTab == 'ViewData') {
- breadcrumbs.push({
- text: 'View Data',
- /**
- * OnClick handler for the View Data breadcrumb
- *
- * @param {React.MouseEventHandler} e - Callback for when hovering over the delete icon
- * @returns {void}
- */
- onClick: (e) => {
- e.preventDefault();
- setActiveTab('ViewData');
- },
- });
- }
+ if (activeTab == 'ViewData') {
+ breadcrumbs.push({
+ text: 'View Data',
+ /**
+ * OnClick handler for the View Data breadcrumb
+ *
+ * @param {React.MouseEventHandler} e - Callback for when hovering over the delete icon
+ * @returns {void}
+ */
+ onClick: (e) => {
+ e.preventDefault();
+ setActiveTab('ViewData');
+ },
+ });
+ }
- if (breadcrumbsRoot) {
- breadcrumbsRoot.render(
- ,
- );
- }
- }, [activeTab]);
+ if (breadcrumbsRoot) {
+ breadcrumbsRoot.render(
+ ,
+ );
+ }
+ }, [activeTab]);
}
export default useBreadcrumbs;
diff --git a/modules/dataquery/jsx/hooks/usedatadictionary.tsx b/modules/dataquery/jsx/hooks/usedatadictionary.tsx
index bc94f750a60..82d7108f62e 100644
--- a/modules/dataquery/jsx/hooks/usedatadictionary.tsx
+++ b/modules/dataquery/jsx/hooks/usedatadictionary.tsx
@@ -22,25 +22,25 @@ export type CategoriesAPIReturn = {
* @returns {CategoriesAPIReturn} - Categories returned by dictionary API
*/
function useCategories(): CategoriesAPIReturn|null {
- const [categories, setCategories] = useState(null);
- useEffect(() => {
- if (categories !== null) {
- return;
+ const [categories, setCategories] = useState(null);
+ useEffect(() => {
+ if (categories !== null) {
+ return;
+ }
+ fetch('/dictionary/categories', {credentials: 'same-origin'})
+ .then((resp) => {
+ if (!resp.ok) {
+ throw new Error('Invalid response');
}
- fetch('/dictionary/categories', {credentials: 'same-origin'})
- .then((resp) => {
- if (!resp.ok) {
- throw new Error('Invalid response');
- }
- return resp.json();
- }).then((result) => {
- setCategories(result);
- }
- ).catch( (error) => {
- console.error(error);
- });
- }, []);
- return categories;
+ return resp.json();
+ }).then((result) => {
+ setCategories(result);
+ }
+ ).catch( (error) => {
+ console.error(error);
+ });
+ }, []);
+ return categories;
}
type DataDictionaryReturnType = [
@@ -54,52 +54,52 @@ type DataDictionaryReturnType = [
* @returns {array} - The retrieved dictionary and a callback to populate a new module into it
*/
function useDataDictionary(): DataDictionaryReturnType {
- const [fulldictionary, setDictionary] = useState({});
- // XXX: This should be {[module: string]: Promise} but then
- // typescript says the key is always defined when we try and check if
- // it's set, need to figure out the correct way to do that, for now just use any
- const [pendingModules, setPendingModules] = useState({});
+ const [fulldictionary, setDictionary] = useState({});
+ // XXX: This should be {[module: string]: Promise} but then
+ // typescript says the key is always defined when we try and check if
+ // it's set, need to figure out the correct way to do that, for now just use any
+ const [pendingModules, setPendingModules] = useState({});
- /**
- * Fetch a module's dictionary and cache it into fulldictionary.
- *
- * @param {string} module - The module name to fetch the dictionary for
- * @returns {Promise} - A promise that resolves to module's dictionary
- */
- const fetchModuleDictionary = (module: string): Promise => {
- if (fulldictionary[module]) {
- const promise = Promise.resolve(fulldictionary[module]);
- return promise;
- }
- if (pendingModules[module]) {
- return pendingModules[module];
- }
- const promise: Promise = new Promise(
- (resolve, reject) => {
- fetch('/dictionary/module/' + module,
- {credentials: 'same-origin'}
- ).then((resp) => {
- if (!resp.ok) {
- throw new Error('Invalid response');
- }
- return resp.json();
- }).then((result) => {
- fulldictionary[module] = result;
- setDictionary({...fulldictionary});
+ /**
+ * Fetch a module's dictionary and cache it into fulldictionary.
+ *
+ * @param {string} module - The module name to fetch the dictionary for
+ * @returns {Promise} - A promise that resolves to module's dictionary
+ */
+ const fetchModuleDictionary = (module: string): Promise => {
+ if (fulldictionary[module]) {
+ const promise = Promise.resolve(fulldictionary[module]);
+ return promise;
+ }
+ if (pendingModules[module]) {
+ return pendingModules[module];
+ }
+ const promise: Promise = new Promise(
+ (resolve, reject) => {
+ fetch('/dictionary/module/' + module,
+ {credentials: 'same-origin'}
+ ).then((resp) => {
+ if (!resp.ok) {
+ throw new Error('Invalid response');
+ }
+ return resp.json();
+ }).then((result) => {
+ fulldictionary[module] = result;
+ setDictionary({...fulldictionary});
- resolve(result);
- }).catch( (error) => {
- console.error(error);
- reject(error);
- });
- }
- );
- const newUsedModules = pendingModules;
- newUsedModules[module] = promise;
- setPendingModules(newUsedModules);
- return promise;
- };
- return [fulldictionary, fetchModuleDictionary];
+ resolve(result);
+ }).catch( (error) => {
+ console.error(error);
+ reject(error);
+ });
+ }
+ );
+ const newUsedModules = pendingModules;
+ newUsedModules[module] = promise;
+ setPendingModules(newUsedModules);
+ return promise;
+ };
+ return [fulldictionary, fetchModuleDictionary];
}
export {useDataDictionary, useCategories};
diff --git a/modules/dataquery/jsx/hooks/usequery.tsx b/modules/dataquery/jsx/hooks/usequery.tsx
index 20a84bbe2c8..75a34e300cb 100644
--- a/modules/dataquery/jsx/hooks/usequery.tsx
+++ b/modules/dataquery/jsx/hooks/usequery.tsx
@@ -47,281 +47,281 @@ type useQueryReturnType = [
* @returns {useQueryReturnType} - A veritable plethora of actions and values
*/
function useQuery(): useQueryReturnType {
- const [fields, setFields] = useState([]);
- const [criteria, setCriteria] = useState(new QueryGroup('and'));
+ const [fields, setFields] = useState([]);
+ const [criteria, setCriteria] = useState(new QueryGroup('and'));
- /**
- * Add a term to the current QueryGroup
- *
- * @param {QueryGroup} querygroup - The group to add the term to
- * @param {QueryTerm} condition - The term to add to the group
- * @returns {QueryGroup} - The new QueryGroup
- */
- const addQueryGroupItem = (
- querygroup: QueryGroup,
- condition: QueryTerm,
- ): QueryGroup => {
- // clone the top level query to force
- // a new rendering
- const newquery = new QueryGroup(criteria.operator);
+ /**
+ * Add a term to the current QueryGroup
+ *
+ * @param {QueryGroup} querygroup - The group to add the term to
+ * @param {QueryTerm} condition - The term to add to the group
+ * @returns {QueryGroup} - The new QueryGroup
+ */
+ const addQueryGroupItem = (
+ querygroup: QueryGroup,
+ condition: QueryTerm,
+ ): QueryGroup => {
+ // clone the top level query to force
+ // a new rendering
+ const newquery = new QueryGroup(criteria.operator);
- // Add to this level of the tree
- querygroup.addTerm(condition);
+ // Add to this level of the tree
+ querygroup.addTerm(condition);
- newquery.group = [...criteria.group];
- setCriteria(newquery);
- return newquery;
- };
+ newquery.group = [...criteria.group];
+ setCriteria(newquery);
+ return newquery;
+ };
- /**
- * Remove a given index from the current QueryGroup and return
- * a new group.
- *
- * @param {QueryGroup} querygroup - The querygroup to remove an item from
- * @param {number} idx - The index to remove
- * @returns {QueryGroup} - the new QueryGroup
- */
- const removeQueryGroupItem = (
- querygroup: QueryGroup,
- idx: number
- ): QueryGroup => {
- // Remove from this level of the tree
- querygroup.removeTerm(idx);
+ /**
+ * Remove a given index from the current QueryGroup and return
+ * a new group.
+ *
+ * @param {QueryGroup} querygroup - The querygroup to remove an item from
+ * @param {number} idx - The index to remove
+ * @returns {QueryGroup} - the new QueryGroup
+ */
+ const removeQueryGroupItem = (
+ querygroup: QueryGroup,
+ idx: number
+ ): QueryGroup => {
+ // Remove from this level of the tree
+ querygroup.removeTerm(idx);
+
+ // clone the top level query to force
+ // a new rendering
+ const newquery = new QueryGroup(criteria.operator);
- // clone the top level query to force
- // a new rendering
- const newquery = new QueryGroup(criteria.operator);
+ newquery.group = [...criteria.group];
+ setCriteria(newquery);
- newquery.group = [...criteria.group];
- setCriteria(newquery);
+ return newquery;
+ };
- return newquery;
- };
+ /**
+ * Add a new, empty query group to the end of a QueryGroup
+ *
+ * @param {QueryGroup} parentgroup - the group to get a new querygroup child
+ * @returns {void}
+ */
+ const addNewQueryGroup = (parentgroup: QueryGroup): void => {
+ // Add to this level of the tree
+ parentgroup.addGroup();
+ // clone the top level query to force
+ // a new rendering
+ const newquery = new QueryGroup(criteria.operator);
+ newquery.group = [...criteria.group];
+
+ setCriteria(newquery);
+ };
+
+ /**
+ * Load a new query as the currently managed query by this hook.
+ *
+ * @param {APIQueryField[]} fields - The fields of the new query
+ * @param {QueryGroup} filters - The filters of the new query.
+ * @returns {void}
+ */
+ const loadQuery = (
+ fields: APIQueryField[],
+ filters: QueryGroup|null
+ ): void => {
+ setFields(fields);
+ if (!filters) {
+ setCriteria(new QueryGroup('and'));
+ } else {
+ setCriteria(filters);
+ }
+ };
+ const fieldActions: FieldActions = {
/**
- * Add a new, empty query group to the end of a QueryGroup
+ * Clear all fields from this query
*
- * @param {QueryGroup} parentgroup - the group to get a new querygroup child
* @returns {void}
*/
- const addNewQueryGroup = (parentgroup: QueryGroup): void => {
- // Add to this level of the tree
- parentgroup.addGroup();
-
- // clone the top level query to force
- // a new rendering
- const newquery = new QueryGroup(criteria.operator);
- newquery.group = [...criteria.group];
-
- setCriteria(newquery);
- };
-
+ clear: function() {
+ setFields([]);
+ },
/**
- * Load a new query as the currently managed query by this hook.
+ * Remove a field from this query
*
- * @param {APIQueryField[]} fields - The fields of the new query
- * @param {QueryGroup} filters - The filters of the new query.
+ * @param {string} module - The module of the field to remove
+ * @param {string} category - The category of the field to remove
+ * @param {string} field - The field to remove
* @returns {void}
*/
- const loadQuery = (
- fields: APIQueryField[],
- filters: QueryGroup|null
+ remove: (
+ module: string,
+ category: string,
+ field: string,
): void => {
- setFields(fields);
- if (!filters) {
- setCriteria(new QueryGroup('and'));
- } else {
- setCriteria(filters);
- }
- };
- const fieldActions: FieldActions = {
- /**
- * Clear all fields from this query
- *
- * @returns {void}
- */
- clear: function() {
- setFields([]);
- },
- /**
- * Remove a field from this query
- *
- * @param {string} module - The module of the field to remove
- * @param {string} category - The category of the field to remove
- * @param {string} field - The field to remove
- * @returns {void}
- */
- remove: (
- module: string,
- category: string,
- field: string,
- ): void => {
- /**
- * Returns true if an element in fields is equal to this field
- *
- * @param {APIQueryField} element - The element to compare
- * @returns {boolean} - true if equal
- */
- const equalField = (element: APIQueryField): boolean => {
- return (element.module == module
+ /**
+ * Returns true if an element in fields is equal to this field
+ *
+ * @param {APIQueryField} element - The element to compare
+ * @returns {boolean} - true if equal
+ */
+ const equalField = (element: APIQueryField): boolean => {
+ return (element.module == module
&& element.category === category
&& element.field == field);
- };
- const newfields = fields.filter((el) => !(equalField(el)));
- setFields(newfields);
- },
- /**
- * Modify the visits for a selected field
- *
- * @param {string} module - The module of the field to modify
- * @param {string} category - The category of the field to modify
- * @param {string} field - The field to modify
- * @param {string[]} visits - The new visits for the field
- * @returns {void}
- */
- modifyVisits: (
- module: string,
- category: string,
- field: string,
- visits: string[]
- ) => {
- const newfields: APIQueryField[] = [...fields];
+ };
+ const newfields = fields.filter((el) => !(equalField(el)));
+ setFields(newfields);
+ },
+ /**
+ * Modify the visits for a selected field
+ *
+ * @param {string} module - The module of the field to modify
+ * @param {string} category - The category of the field to modify
+ * @param {string} field - The field to modify
+ * @param {string[]} visits - The new visits for the field
+ * @returns {void}
+ */
+ modifyVisits: (
+ module: string,
+ category: string,
+ field: string,
+ visits: string[]
+ ) => {
+ const newfields: APIQueryField[] = [...fields];
- /**
- * Returns true if an element in fields is equal to this field
- *
- * @param {APIQueryField} element - The element to compare
- * @returns {boolean} - true if equal
- */
- const equalField = (element: APIQueryField) => {
- return (element.module == module
+ /**
+ * Returns true if an element in fields is equal to this field
+ *
+ * @param {APIQueryField} element - The element to compare
+ * @returns {boolean} - true if equal
+ */
+ const equalField = (element: APIQueryField) => {
+ return (element.module == module
&& element.category === category
&& element.field == field);
- };
+ };
- for (let i = 0; i < newfields.length; i++) {
- if (equalField(newfields[i])) {
- newfields[i].visits = visits;
- setFields(newfields);
- return;
- }
- }
- },
- /**
- * Toggle whether a field is present by adding it if missing or removing it if
- * present.
- *
- * @param {string} module - the module for the field
- * @param {string} category - the category for the field
- * @param {string} field - the field name
- * @param {string[]} visits - the list of visits to add if adding
- */
- addRemoveField: (
- module: string,
- category: string,
- field: string,
- visits: string[]
- ): void => {
- const newFieldObj: APIQueryField = {
- module: module,
- category: category,
- field: field,
- visits: visits,
- };
- /**
- * Returns true if an element in fields is equal to this field
- *
- * @param {APIQueryField} element - The element to compare
- * @returns {boolean} - true if equal
- */
- const equalField = (element: APIQueryField) => {
- return (element.module == module
+ for (let i = 0; i < newfields.length; i++) {
+ if (equalField(newfields[i])) {
+ newfields[i].visits = visits;
+ setFields(newfields);
+ return;
+ }
+ }
+ },
+ /**
+ * Toggle whether a field is present by adding it if missing or removing it if
+ * present.
+ *
+ * @param {string} module - the module for the field
+ * @param {string} category - the category for the field
+ * @param {string} field - the field name
+ * @param {string[]} visits - the list of visits to add if adding
+ */
+ addRemoveField: (
+ module: string,
+ category: string,
+ field: string,
+ visits: string[]
+ ): void => {
+ const newFieldObj: APIQueryField = {
+ module: module,
+ category: category,
+ field: field,
+ visits: visits,
+ };
+ /**
+ * Returns true if an element in fields is equal to this field
+ *
+ * @param {APIQueryField} element - The element to compare
+ * @returns {boolean} - true if equal
+ */
+ const equalField = (element: APIQueryField) => {
+ return (element.module == module
&& element.category === category
&& element.field == field);
- };
- if (fields.some(equalField)) {
- // Remove
- const newfields: APIQueryField[] = fields.filter(
- (el) => !(equalField(el))
- );
- setFields(newfields);
- } else {
- // Add
- const newfields: APIQueryField[] = [...fields, newFieldObj];
- setFields(newfields);
- }
- },
- /**
- * Remove multiple elements from the current query
- *
- * @param {APIQueryField[]} removeelements - The elements to remove
- * @returns {void}
- */
- removeMany: (removeelements: APIQueryField[]): void => {
- /**
- * Returns true if el1 is equal to el2
- *
- * @param {APIQueryField} el1 - The first element to compare
- * @param {APIQueryField} el2 - The second element to compare
- * @returns {boolean} - true if equal
- */
- const equalField = (
- el1: APIQueryField,
- el2: APIQueryField
- ): boolean => {
- return (el1.module == el2.module
+ };
+ if (fields.some(equalField)) {
+ // Remove
+ const newfields: APIQueryField[] = fields.filter(
+ (el) => !(equalField(el))
+ );
+ setFields(newfields);
+ } else {
+ // Add
+ const newfields: APIQueryField[] = [...fields, newFieldObj];
+ setFields(newfields);
+ }
+ },
+ /**
+ * Remove multiple elements from the current query
+ *
+ * @param {APIQueryField[]} removeelements - The elements to remove
+ * @returns {void}
+ */
+ removeMany: (removeelements: APIQueryField[]): void => {
+ /**
+ * Returns true if el1 is equal to el2
+ *
+ * @param {APIQueryField} el1 - The first element to compare
+ * @param {APIQueryField} el2 - The second element to compare
+ * @returns {boolean} - true if equal
+ */
+ const equalField = (
+ el1: APIQueryField,
+ el2: APIQueryField
+ ): boolean => {
+ return (el1.module == el2.module
&& el1.category === el2.category
&& el1.field == el2.field);
- };
- const newfields = fields.filter((el) => {
- if (removeelements.some((rel) => equalField(rel, el))) {
- return false;
- }
- return true;
- });
- setFields(newfields);
- },
+ };
+ const newfields = fields.filter((el) => {
+ if (removeelements.some((rel) => equalField(rel, el))) {
+ return false;
+ }
+ return true;
+ });
+ setFields(newfields);
+ },
+ /**
+ * Adds many fields to the selected query
+ *
+ * @param {APIQueryField[]} elements - the fields to add
+ * @returns {void}
+ */
+ addMany: (elements: APIQueryField[]): void => {
+ let newfields = fields;
+ for (let i = 0; i < elements.length; i++) {
+ const newFieldObj = elements[i];
/**
- * Adds many fields to the selected query
+ * Returns true if an element in fields is equal to this field
*
- * @param {APIQueryField[]} elements - the fields to add
- * @returns {void}
+ * @param {APIQueryField} element - The element to compare
+ * @returns {boolean} - true if equal
*/
- addMany: (elements: APIQueryField[]): void => {
- let newfields = fields;
- for (let i = 0; i < elements.length; i++) {
- const newFieldObj = elements[i];
- /**
- * Returns true if an element in fields is equal to this field
- *
- * @param {APIQueryField} element - The element to compare
- * @returns {boolean} - true if equal
- */
- const equalField = (element: APIQueryField) => {
- return (element.module == newFieldObj.module
+ const equalField = (element: APIQueryField) => {
+ return (element.module == newFieldObj.module
&& element.category === newFieldObj.category
&& element.field == newFieldObj.field);
- };
- if (!newfields.some((el: APIQueryField) => equalField(el))) {
- newfields = [...newfields, newFieldObj];
- }
- }
- setFields(newfields);
- },
- setFields: setFields,
- };
- return [
- criteria,
- loadQuery,
- fields,
- fieldActions,
- {
- addQueryGroupItem: addQueryGroupItem,
- removeQueryGroupItem: removeQueryGroupItem,
- addNewQueryGroup: addNewQueryGroup,
- setCriteria: setCriteria,
- },
+ };
+ if (!newfields.some((el: APIQueryField) => equalField(el))) {
+ newfields = [...newfields, newFieldObj];
+ }
+ }
+ setFields(newfields);
+ },
+ setFields: setFields,
+ };
+ return [
+ criteria,
+ loadQuery,
+ fields,
+ fieldActions,
+ {
+ addQueryGroupItem: addQueryGroupItem,
+ removeQueryGroupItem: removeQueryGroupItem,
+ addNewQueryGroup: addNewQueryGroup,
+ setCriteria: setCriteria,
+ },
];
}
diff --git a/modules/dataquery/jsx/hooks/usesharedqueries.tsx b/modules/dataquery/jsx/hooks/usesharedqueries.tsx
index 2ac2e1cba9e..642b063baf9 100644
--- a/modules/dataquery/jsx/hooks/usesharedqueries.tsx
+++ b/modules/dataquery/jsx/hooks/usesharedqueries.tsx
@@ -26,28 +26,28 @@ interface FlattenedQueryMap {
* @returns {array} - QueryAction functions
*/
function useStarredQueries(onCompleteCallback: () => void): QueryActionType {
- const [starQueryID, setStarQueryID] = useState(null);
- const [starAction, setStarAction] = useState('true');
- useEffect(() => {
- if (starQueryID == null) {
- return;
- }
+ const [starQueryID, setStarQueryID] = useState(null);
+ const [starAction, setStarAction] = useState('true');
+ useEffect(() => {
+ if (starQueryID == null) {
+ return;
+ }
- fetch(
- '/dataquery/queries/' + starQueryID + '?star=' + starAction,
- {
- method: 'PATCH',
- credentials: 'same-origin',
- },
- ).then( () => {
- setStarQueryID(null);
- if (onCompleteCallback) {
- onCompleteCallback();
- }
- }
- );
- }, [starQueryID, starAction]);
- return [setStarQueryID, setStarAction];
+ fetch(
+ '/dataquery/queries/' + starQueryID + '?star=' + starAction,
+ {
+ method: 'PATCH',
+ credentials: 'same-origin',
+ },
+ ).then( () => {
+ setStarQueryID(null);
+ if (onCompleteCallback) {
+ onCompleteCallback();
+ }
+ }
+ );
+ }, [starQueryID, starAction]);
+ return [setStarQueryID, setStarAction];
}
/**
@@ -58,28 +58,28 @@ function useStarredQueries(onCompleteCallback: () => void): QueryActionType {
* @returns {array} - QueryAction functions
*/
function useShareQueries(onCompleteCallback: () => void): QueryActionType {
- const [shareQueryID, setShareQueryID] = useState(null);
- const [shareAction, setShareAction] = useState('true');
- useEffect(() => {
- if (shareQueryID == null) {
- return;
- }
+ const [shareQueryID, setShareQueryID] = useState(null);
+ const [shareAction, setShareAction] = useState('true');
+ useEffect(() => {
+ if (shareQueryID == null) {
+ return;
+ }
- fetch(
- '/dataquery/queries/' + shareQueryID + '?share=' + shareAction,
- {
- method: 'PATCH',
- credentials: 'same-origin',
- },
- ).then( () => {
- setShareQueryID(null);
- if (onCompleteCallback) {
- onCompleteCallback();
- }
- }
- );
- }, [shareQueryID, shareAction]);
- return [setShareQueryID, setShareAction];
+ fetch(
+ '/dataquery/queries/' + shareQueryID + '?share=' + shareAction,
+ {
+ method: 'PATCH',
+ credentials: 'same-origin',
+ },
+ ).then( () => {
+ setShareQueryID(null);
+ if (onCompleteCallback) {
+ onCompleteCallback();
+ }
+ }
+ );
+ }, [shareQueryID, shareAction]);
+ return [setShareQueryID, setShareAction];
}
type SharedQueriesType = [
@@ -103,156 +103,156 @@ type SharedQueriesType = [
* @returns {array} - [{queries}, reload function(), {queryActions}]
*/
function useSharedQueries(username: string): SharedQueriesType {
- const [recentQueries, setRecentQueries] = useState([]);
- const [sharedQueries, setSharedQueries] = useState([]);
- const [topQueries, setTopQueries] = useState([]);
+ const [recentQueries, setRecentQueries] = useState([]);
+ const [sharedQueries, setSharedQueries] = useState([]);
+ const [topQueries, setTopQueries] = useState([]);
- const [loadQueriesForce, setLoadQueriesForce] = useState(0);
- /**
- * Force the client to reload queries
- *
- * @returns {void}
- */
- const reloadQueries = () => setLoadQueriesForce(loadQueriesForce+1);
- const [setStarQueryID, setStarAction] = useStarredQueries(reloadQueries);
- const [setShareQueryID, setShareAction] = useShareQueries(reloadQueries);
+ const [loadQueriesForce, setLoadQueriesForce] = useState(0);
+ /**
+ * Force the client to reload queries
+ *
+ * @returns {void}
+ */
+ const reloadQueries = () => setLoadQueriesForce(loadQueriesForce+1);
+ const [setStarQueryID, setStarAction] = useStarredQueries(reloadQueries);
+ const [setShareQueryID, setShareAction] = useShareQueries(reloadQueries);
- useEffect(() => {
- fetch('/dataquery/queries', {credentials: 'same-origin'})
- .then((resp) => {
- if (!resp.ok) {
- throw new Error('Invalid response');
- }
- return resp.json();
- }).then((result) => {
- const convertedshared: FlattenedQuery[] = [];
- const convertedtop: FlattenedQuery[] = [];
- const allQueries: FlattenedQueryMap = {};
- if (result.queries) {
- result.queries.forEach( (query: APIQuery) => {
- const flattened: FlattenedQuery = query2flattened(query);
- allQueries[query.QueryID] = flattened;
+ useEffect(() => {
+ fetch('/dataquery/queries', {credentials: 'same-origin'})
+ .then((resp) => {
+ if (!resp.ok) {
+ throw new Error('Invalid response');
+ }
+ return resp.json();
+ }).then((result) => {
+ const convertedshared: FlattenedQuery[] = [];
+ const convertedtop: FlattenedQuery[] = [];
+ const allQueries: FlattenedQueryMap = {};
+ if (result.queries) {
+ result.queries.forEach( (query: APIQuery) => {
+ const flattened: FlattenedQuery = query2flattened(query);
+ allQueries[query.QueryID] = flattened;
- if (query.Pinned == true) {
- convertedtop.push(flattened);
- }
- if (query.Public == true) {
- // If we're the only person who shared it, don't show it in our
- // shared queries.
- // If other people shared it too, then remove ourselves from the
- // "shared by" list in the Shared Queries panel.
- if (query.SharedBy.length == 1
+ if (query.Pinned == true) {
+ convertedtop.push(flattened);
+ }
+ if (query.Public == true) {
+ // If we're the only person who shared it, don't show it in our
+ // shared queries.
+ // If other people shared it too, then remove ourselves from the
+ // "shared by" list in the Shared Queries panel.
+ if (query.SharedBy.length == 1
&& query.SharedBy[0] == username
- ) {
- // don't include
- } else {
- // filter
- query.SharedBy = query.SharedBy.filter(
- (item: string) => {
- return item != username;
- }
- );
- // Make a new copy to avoid mutating the version used by other
- // tabs
- const flattened2: FlattenedQuery
- = query2flattened(query);
- convertedshared.push(flattened2);
- }
+ ) {
+ // don't include
+ } else {
+ // filter
+ query.SharedBy = query.SharedBy.filter(
+ (item: string) => {
+ return item != username;
}
- });
- }
- setSharedQueries(convertedshared);
- setTopQueries(convertedtop);
- return allQueries;
- }).then((allQueries) => {
+ );
+ // Make a new copy to avoid mutating the version used by other
+ // tabs
+ const flattened2: FlattenedQuery
+ = query2flattened(query);
+ convertedshared.push(flattened2);
+ }
+ }
+ });
+ }
+ setSharedQueries(convertedshared);
+ setTopQueries(convertedtop);
+ return allQueries;
+ }).then((allQueries) => {
fetch('/dataquery/queries/runs', {credentials: 'same-origin'})
- .then((resp) => {
- if (!resp.ok) {
- throw new Error('Invalid response');
- }
- return resp.json();
- }).then((result) => {
+ .then((resp) => {
+ if (!resp.ok) {
+ throw new Error('Invalid response');
+ }
+ return resp.json();
+ }).then((result) => {
if (result.queryruns) {
- const convertedrecent: FlattenedQuery[] = [];
- result.queryruns.forEach( (queryRun: APIQueryRun) => {
- const queryObj: FlattenedQuery
+ const convertedrecent: FlattenedQuery[] = [];
+ result.queryruns.forEach( (queryRun: APIQueryRun) => {
+ const queryObj: FlattenedQuery
= allQueries[queryRun.QueryID];
- if (!queryObj) {
- console.error(
- 'Could not get ',
- queryRun.QueryID,
- ' from ',
- allQueries);
- return;
- }
- convertedrecent.push({
- RunTime: queryRun.RunTime,
- ...queryObj,
- });
+ if (!queryObj) {
+ console.error(
+ 'Could not get ',
+ queryRun.QueryID,
+ ' from ',
+ allQueries);
+ return;
+ }
+ convertedrecent.push({
+ RunTime: queryRun.RunTime,
+ ...queryObj,
});
- setRecentQueries(convertedrecent);
+ });
+ setRecentQueries(convertedrecent);
}
- });
- }).catch( (error) => {
- console.error(error);
- });
- }, [loadQueriesForce]);
+ });
+ }).catch( (error) => {
+ console.error(error);
+ });
+ }, [loadQueriesForce]);
- return [
- {
- recent: recentQueries,
- shared: sharedQueries,
- top: topQueries,
- },
- reloadQueries,
- {
- /**
- * Stars a query on the server
- *
- * @param {number} queryID - The queryID to star
- * @returns {void}
- */
- star: (queryID: number) => {
- setShareQueryID(null);
- setStarAction('true');
- setStarQueryID(queryID);
- },
- /**
- * Unstars a query on the server
- *
- * @param {number} queryID - The queryID to unstar
- * @returns {void}
- */
- unstar: (queryID: number) => {
- setShareQueryID(null);
- setStarAction('false');
- setStarQueryID(queryID);
- },
- /**
- * Shares a query on the server
- *
- * @param {number} queryID - The queryID to share
- * @returns {void}
- */
- share: (queryID: number) => {
- setStarQueryID(null);
- setShareAction('true');
- setShareQueryID(queryID);
- },
- /**
- * Unshares a query on the server
- *
- * @param {number} queryID - The queryID to unshare
- * @returns {void}
- */
- unshare: (queryID: number) => {
- setStarQueryID(null);
- setShareAction('false');
- setShareQueryID(queryID);
- },
- },
- ];
+ return [
+ {
+ recent: recentQueries,
+ shared: sharedQueries,
+ top: topQueries,
+ },
+ reloadQueries,
+ {
+ /**
+ * Stars a query on the server
+ *
+ * @param {number} queryID - The queryID to star
+ * @returns {void}
+ */
+ star: (queryID: number) => {
+ setShareQueryID(null);
+ setStarAction('true');
+ setStarQueryID(queryID);
+ },
+ /**
+ * Unstars a query on the server
+ *
+ * @param {number} queryID - The queryID to unstar
+ * @returns {void}
+ */
+ unstar: (queryID: number) => {
+ setShareQueryID(null);
+ setStarAction('false');
+ setStarQueryID(queryID);
+ },
+ /**
+ * Shares a query on the server
+ *
+ * @param {number} queryID - The queryID to share
+ * @returns {void}
+ */
+ share: (queryID: number) => {
+ setStarQueryID(null);
+ setShareAction('true');
+ setShareQueryID(queryID);
+ },
+ /**
+ * Unshares a query on the server
+ *
+ * @param {number} queryID - The queryID to unshare
+ * @returns {void}
+ */
+ unshare: (queryID: number) => {
+ setStarQueryID(null);
+ setShareAction('false');
+ setShareQueryID(queryID);
+ },
+ },
+ ];
}
/**
@@ -263,9 +263,9 @@ function useSharedQueries(username: string): SharedQueriesType {
* @returns {boolean} - true if the term is an APIQueryCriteriaGroup
*/
function isAPIQueryCriteriaGroup(
- term: APIQueryGroupField|APIQueryCriteriaGroup
+ term: APIQueryGroupField|APIQueryCriteriaGroup
): term is APIQueryCriteriaGroup {
- return (term as APIQueryCriteriaGroup).operator !== undefined;
+ return (term as APIQueryCriteriaGroup).operator !== undefined;
}
/**
* Takes a saved query from a JSON object and marshal
@@ -275,39 +275,39 @@ function isAPIQueryCriteriaGroup(
* @returns {QueryGroup} - The object converted into a QueryGroup
*/
function unserializeSavedQuery(query: APIQueryCriteriaGroup): QueryGroup {
- if (!query.operator) {
- throw new Error('Invalid query tree');
- }
- const root = new QueryGroup(query.operator);
- query.group.forEach((val) => {
- if (isAPIQueryCriteriaGroup(val)) {
- const childTree: QueryGroup = unserializeSavedQuery(val);
- root.group.push(childTree);
- return;
- } else {
- if (!val.module
+ if (!query.operator) {
+ throw new Error('Invalid query tree');
+ }
+ const root = new QueryGroup(query.operator);
+ query.group.forEach((val) => {
+ if (isAPIQueryCriteriaGroup(val)) {
+ const childTree: QueryGroup = unserializeSavedQuery(val);
+ root.group.push(childTree);
+ return;
+ } else {
+ if (!val.module
|| !val.category
|| !val.fieldname
|| !val.op) {
- console.error('Invalid criteria', val);
- return;
- }
+ console.error('Invalid criteria', val);
+ return;
+ }
- const term = val as APIQueryGroupField;
+ const term = val as APIQueryGroupField;
- root.addTerm(
- new QueryTerm(
- term.module,
- term.category,
- term.fieldname,
- term.op,
- term.value,
- term.visits,
- )
- );
- }
- });
- return root;
+ root.addTerm(
+ new QueryTerm(
+ term.module,
+ term.category,
+ term.fieldname,
+ term.op,
+ term.value,
+ term.visits,
+ )
+ );
+ }
+ });
+ return root;
}
/**
@@ -316,45 +316,45 @@ function unserializeSavedQuery(query: APIQueryCriteriaGroup): QueryGroup {
* @param {function} loadQuery - function to load the query into React state
*/
function useLoadQueryFromURL(
- loadQuery: (fields: APIQueryField[], filters: QueryGroup|null) => void
+ loadQuery: (fields: APIQueryField[], filters: QueryGroup|null) => void
) {
- // Load query if queryID was passed
- useEffect(() => {
- const params = new URLSearchParams(window.location.search);
- const queryID = params.get('queryID');
- if (!queryID) {
- return;
- }
- fetch(
- '/dataquery/queries/' + queryID,
- {
- method: 'GET',
- credentials: 'same-origin',
- },
- ).then((resp) => {
- if (!resp.ok) {
- throw new Error('Invalid response');
- }
- return resp.json();
- }).then((result) => {
- if (result.Query.criteria) {
- result.Query.criteria = unserializeSavedQuery(
- result.Query.criteria
- );
- }
- loadQuery(result.Query.fields, result.Query.criteria);
- swal.fire({
- type: 'success',
- text: 'Loaded query',
- });
- }).catch( (error) => {
- swal.fire({
- type: 'error',
- text: 'Could not load query',
- });
- console.error(error);
- });
- }, []);
+ // Load query if queryID was passed
+ useEffect(() => {
+ const params = new URLSearchParams(window.location.search);
+ const queryID = params.get('queryID');
+ if (!queryID) {
+ return;
+ }
+ fetch(
+ '/dataquery/queries/' + queryID,
+ {
+ method: 'GET',
+ credentials: 'same-origin',
+ },
+ ).then((resp) => {
+ if (!resp.ok) {
+ throw new Error('Invalid response');
+ }
+ return resp.json();
+ }).then((result) => {
+ if (result.Query.criteria) {
+ result.Query.criteria = unserializeSavedQuery(
+ result.Query.criteria
+ );
+ }
+ loadQuery(result.Query.fields, result.Query.criteria);
+ swal.fire({
+ type: 'success',
+ text: 'Loaded query',
+ });
+ }).catch( (error) => {
+ swal.fire({
+ type: 'error',
+ text: 'Could not load query',
+ });
+ console.error(error);
+ });
+ }, []);
}
@@ -366,51 +366,51 @@ function useLoadQueryFromURL(
* @returns {FlattenedQuery} - The query converted to the new type
*/
function query2flattened(query: APIQuery): FlattenedQuery {
- const rv: FlattenedQuery = {
- QueryID: query.QueryID,
- fields: query.Query.fields.map(
- (field: APIQueryField): FlattenedField => {
- if (!field.visits) {
- return {
- module: field.module,
- category: field.category,
- field: field.field,
- visits: null,
- };
- }
- return {
- module: field.module,
- category: field.category,
- field: field.field,
- visits: field.visits.map( (vl: string): VisitOption => {
- return {label: vl, value: vl};
- }),
- };
- }),
- };
+ const rv: FlattenedQuery = {
+ QueryID: query.QueryID,
+ fields: query.Query.fields.map(
+ (field: APIQueryField): FlattenedField => {
+ if (!field.visits) {
+ return {
+ module: field.module,
+ category: field.category,
+ field: field.field,
+ visits: null,
+ };
+ }
+ return {
+ module: field.module,
+ category: field.category,
+ field: field.field,
+ visits: field.visits.map( (vl: string): VisitOption => {
+ return {label: vl, value: vl};
+ }),
+ };
+ }),
+ };
- if (query.Query.criteria) {
- rv.criteria = unserializeSavedQuery(query.Query.criteria);
- }
- if (query.Name) {
- rv.Name = query.Name;
- }
- if (query.AdminName) {
- rv.AdminName = query.AdminName;
- }
- if (query.Public) {
- rv.Public = query.Public;
- }
- if (query.Starred) {
- rv.Starred = query.Starred;
- }
- if (query.SharedBy) {
- rv.SharedBy = query.SharedBy;
- }
- return rv;
+ if (query.Query.criteria) {
+ rv.criteria = unserializeSavedQuery(query.Query.criteria);
+ }
+ if (query.Name) {
+ rv.Name = query.Name;
+ }
+ if (query.AdminName) {
+ rv.AdminName = query.AdminName;
+ }
+ if (query.Public) {
+ rv.Public = query.Public;
+ }
+ if (query.Starred) {
+ rv.Starred = query.Starred;
+ }
+ if (query.SharedBy) {
+ rv.SharedBy = query.SharedBy;
+ }
+ return rv;
}
export {
- useSharedQueries,
- useLoadQueryFromURL,
+ useSharedQueries,
+ useLoadQueryFromURL,
};
diff --git a/modules/dataquery/jsx/hooks/usevisits.tsx b/modules/dataquery/jsx/hooks/usevisits.tsx
index 1fbc4d4f527..18a0adbfda1 100644
--- a/modules/dataquery/jsx/hooks/usevisits.tsx
+++ b/modules/dataquery/jsx/hooks/usevisits.tsx
@@ -16,36 +16,36 @@ type UseVisitsReturn = {
* @returns {UseVisitsReturn} - list of default and all visits
*/
function useVisits(): UseVisitsReturn {
- const [allVisits, setAllVisits] = useState([]);
- const [defaultVisits, setDefaultVisits] = useState([]);
- useEffect(() => {
- fetch('/dataquery/visitlist', {credentials: 'same-origin'})
- .then((resp) => {
- if (!resp.ok) {
- throw new Error('Invalid response');
- }
- return resp.json();
- }).then((result) => {
- setDefaultVisits(result.Visits);
- setAllVisits(result.Visits);
- }
- ).catch( (error) => {
- console.error(error);
- });
- }, []);
- return {
- all: allVisits,
- default_: defaultVisits,
- /**
- * Modify the default visits to use
- *
- * @param {VisitOption[]} values - The selected options from ReactSelect
- * @returns {void}
- */
- modifyDefault: (values: readonly VisitOption[]) => {
- setDefaultVisits(values.map((el) => el.value));
- },
- };
+ const [allVisits, setAllVisits] = useState([]);
+ const [defaultVisits, setDefaultVisits] = useState([]);
+ useEffect(() => {
+ fetch('/dataquery/visitlist', {credentials: 'same-origin'})
+ .then((resp) => {
+ if (!resp.ok) {
+ throw new Error('Invalid response');
+ }
+ return resp.json();
+ }).then((result) => {
+ setDefaultVisits(result.Visits);
+ setAllVisits(result.Visits);
+ }
+ ).catch( (error) => {
+ console.error(error);
+ });
+ }, []);
+ return {
+ all: allVisits,
+ default_: defaultVisits,
+ /**
+ * Modify the default visits to use
+ *
+ * @param {VisitOption[]} values - The selected options from ReactSelect
+ * @returns {void}
+ */
+ modifyDefault: (values: readonly VisitOption[]) => {
+ setDefaultVisits(values.map((el) => el.value));
+ },
+ };
}
export default useVisits;
diff --git a/modules/dataquery/jsx/index.tsx b/modules/dataquery/jsx/index.tsx
index f234e44eaa1..4b0d3a88b54 100644
--- a/modules/dataquery/jsx/index.tsx
+++ b/modules/dataquery/jsx/index.tsx
@@ -32,32 +32,32 @@ type ActiveCategoryType = {
* @returns {ActiveCategoryType} - an object of the current dictionary and action to change it
*/
function useActiveCategory(
- retrieveModuleDictionary: (module: string) => Promise
+ retrieveModuleDictionary: (module: string) => Promise
): ActiveCategoryType {
- const [module, setModule] = useState('');
- const [category, setCategory] = useState('');
- const [moduleDict, setModuleDict] = useState({});
- /**
- * Change the current category, retrieving the module dictionary from
- * the server if necessary.
- *
- * @param {string} module - the module to become active
- * @param {string} category - the category to become active
- * @returns {void}
- */
- const changeCategory = (module: string, category: string) => {
- retrieveModuleDictionary(module).then( (dict) => {
- setModule(module);
- setCategory(category);
- setModuleDict(dict[category]);
- });
- };
- return {
- module: module,
- category: category,
- currentDictionary: moduleDict,
- changeCategory: changeCategory,
- };
+ const [module, setModule] = useState('');
+ const [category, setCategory] = useState('');
+ const [moduleDict, setModuleDict] = useState({});
+ /**
+ * Change the current category, retrieving the module dictionary from
+ * the server if necessary.
+ *
+ * @param {string} module - the module to become active
+ * @param {string} category - the category to become active
+ * @returns {void}
+ */
+ const changeCategory = (module: string, category: string) => {
+ retrieveModuleDictionary(module).then( (dict) => {
+ setModule(module);
+ setCategory(category);
+ setModuleDict(dict[category]);
+ });
+ };
+ return {
+ module: module,
+ category: category,
+ currentDictionary: moduleDict,
+ changeCategory: changeCategory,
+ };
}
/**
@@ -72,50 +72,50 @@ function DataQueryApp(props: {
queryAdmin: boolean,
username: string
}) {
- const [activeTab, setActiveTab] = useState('Info');
- useBreadcrumbs(activeTab, setActiveTab);
+ const [activeTab, setActiveTab] = useState('Info');
+ useBreadcrumbs(activeTab, setActiveTab);
- const [queries, reloadQueries, queryActions]
+ const [queries, reloadQueries, queryActions]
= useSharedQueries(props.username);
- const visits = useVisits();
+ const visits = useVisits();
- const [
- fulldictionary,
- fetchModuleDictionary,
- ] = useDataDictionary();
- const categories = useCategories();
+ const [
+ fulldictionary,
+ fetchModuleDictionary,
+ ] = useDataDictionary();
+ const categories = useCategories();
- const activeCategory = useActiveCategory(
- fetchModuleDictionary,
- );
+ const activeCategory = useActiveCategory(
+ fetchModuleDictionary,
+ );
- const [query,
- loadQuery,
- selectedFields,
- fieldActions,
- criteriaActions,
- ] = useQuery();
+ const [query,
+ loadQuery,
+ selectedFields,
+ fieldActions,
+ criteriaActions,
+ ] = useQuery();
- useLoadQueryFromURL(loadQuery);
+ useLoadQueryFromURL(loadQuery);
- if (!categories) {
- return Loading...
;
+ if (!categories) {
+ return Loading...
;
+ }
+ let content;
+
+ /**
+ * Maps a module name from the backend name to a human friendly name.
+ *
+ * @param {string} name - The module name
+ * @returns {string} - the human friendly name
+ */
+ const mapModuleName = (name: string): string => {
+ if (categories && categories.modules) {
+ return categories.modules[name];
}
- let content;
-
- /**
- * Maps a module name from the backend name to a human friendly name.
- *
- * @param {string} name - The module name
- * @returns {string} - the human friendly name
- */
- const mapModuleName = (name: string): string => {
- if (categories && categories.modules) {
- return categories.modules[name];
- }
- return name;
- };
+ return name;
+ };
/**
* Maps a category name from the backend name to a human friendly name.
*
@@ -123,137 +123,137 @@ function DataQueryApp(props: {
* @param {string} category - The category name within the module
* @returns {string} - the human friendly name
*/
- const mapCategoryName = (module: string, category: string): string => {
- if (categories && categories.categories
+ const mapCategoryName = (module: string, category: string): string => {
+ if (categories && categories.categories
&& categories.categories[module]) {
- return categories.categories[module][category];
- }
- return category;
- };
-
- /**
- * Function to retrieve a module's data dictionary from the server.
- *
- * @param {string} module - the module whole fields should be retrieved
- * @returns {void}
- */
- const getModuleFields = (module: string): void => {
- fetchModuleDictionary(module);
- };
-
- switch (activeTab) {
- case 'Info':
- content = setActiveTab('DefineFields')}
-
- queryAdmin={props.queryAdmin}
- />;
- break;
- case 'DefineFields':
- content = ;
- break;
- case 'DefineFilters':
- content = ;
- break;
- case 'ViewData':
- content = ;
- break;
- default:
- content = Invalid tab
;
+ return categories.categories[module][category];
}
- return
-
{content}
-
setActiveTab(page)
- }/>
- ;
+ return category;
+ };
+
+ /**
+ * Function to retrieve a module's data dictionary from the server.
+ *
+ * @param {string} module - the module whole fields should be retrieved
+ * @returns {void}
+ */
+ const getModuleFields = (module: string): void => {
+ fetchModuleDictionary(module);
+ };
+
+ switch (activeTab) {
+ case 'Info':
+ content = setActiveTab('DefineFields')}
+
+ queryAdmin={props.queryAdmin}
+ />;
+ break;
+ case 'DefineFields':
+ content = ;
+ break;
+ case 'DefineFilters':
+ content = ;
+ break;
+ case 'ViewData':
+ content = ;
+ break;
+ default:
+ content = Invalid tab
;
+ }
+ return
+
{content}
+
setActiveTab(page)
+ }/>
+ ;
}
declare const loris: any;
window.addEventListener('load', () => {
const element = document.getElementById('lorisworkspace');
if (!element) {
- throw new Error('Missing lorisworkspace');
+ throw new Error('Missing lorisworkspace');
}
const root = createRoot(element);
root.render(
,
);
});
diff --git a/modules/dataquery/jsx/nextsteps.tsx b/modules/dataquery/jsx/nextsteps.tsx
index 0264501e693..59a88cdfac2 100644
--- a/modules/dataquery/jsx/nextsteps.tsx
+++ b/modules/dataquery/jsx/nextsteps.tsx
@@ -19,146 +19,146 @@ function NextSteps(props: {
page: string,
changePage: (newpage: string) => void,
}) {
- const [expanded, setExpanded] = useState(true);
- const steps: React.ReactElement[] = [];
+ const [expanded, setExpanded] = useState(true);
+ const steps: React.ReactElement[] = [];
- const canRun = (props.fields && props.fields.length > 0);
- const fieldLabel = (props.fields && props.fields.length > 0)
- ? 'Modify Fields'
- : 'Choose Fields';
- const filterLabel = (props.filters && props.filters.group.length > 0)
- ? 'Modify Filters'
- : 'Add Filters';
- switch (props.page) {
- case 'Info':
- if (canRun) {
- // A previous query was loaded, it can be either
- // modified or run
- steps.push( props.changePage('DefineFields')}
- />);
- steps.push( props.changePage('DefineFilters')}
- />);
- steps.push( props.changePage('ViewData')}
- />);
- } else {
- // No query loaded, must define fields
- steps.push( props.changePage('DefineFields')}
- />);
- }
- break;
- case 'DefineFields':
- steps.push( props.changePage('DefineFilters')}
- />);
- if (canRun) {
- steps.push( props.changePage('ViewData')}
- />);
- }
- break;
- case 'DefineFilters':
- if (canRun) {
- steps.push( props.changePage('ViewData')}
- />);
- }
- steps.push( props.changePage('DefineFields')}
- />);
- break;
- case 'ViewData':
- steps.push( props.changePage('DefineFields')}
- />);
- steps.push( props.changePage('DefineFilters')}
- />);
- break;
+ const canRun = (props.fields && props.fields.length > 0);
+ const fieldLabel = (props.fields && props.fields.length > 0)
+ ? 'Modify Fields'
+ : 'Choose Fields';
+ const filterLabel = (props.filters && props.filters.group.length > 0)
+ ? 'Modify Filters'
+ : 'Add Filters';
+ switch (props.page) {
+ case 'Info':
+ if (canRun) {
+ // A previous query was loaded, it can be either
+ // modified or run
+ steps.push( props.changePage('DefineFields')}
+ />);
+ steps.push( props.changePage('DefineFilters')}
+ />);
+ steps.push( props.changePage('ViewData')}
+ />);
+ } else {
+ // No query loaded, must define fields
+ steps.push( props.changePage('DefineFields')}
+ />);
}
+ break;
+ case 'DefineFields':
+ steps.push( props.changePage('DefineFilters')}
+ />);
+ if (canRun) {
+ steps.push( props.changePage('ViewData')}
+ />);
+ }
+ break;
+ case 'DefineFilters':
+ if (canRun) {
+ steps.push( props.changePage('ViewData')}
+ />);
+ }
+ steps.push( props.changePage('DefineFields')}
+ />);
+ break;
+ case 'ViewData':
+ steps.push( props.changePage('DefineFields')}
+ />);
+ steps.push( props.changePage('DefineFilters')}
+ />);
+ break;
+ }
- const expandIcon = setExpanded(!expanded)}
- > ;
- const style = expanded ? {
- background: 'white',
- padding: '0.5em',
- paddingLeft: '2em',
- } : {
- display: 'none',
- visibility: 'hidden' as const,
- padding: '0.5em',
- paddingLeft: '2em',
- };
+ const expandIcon = setExpanded(!expanded)}
+ > ;
+ const style = expanded ? {
+ background: 'white',
+ padding: '0.5em',
+ paddingLeft: '2em',
+ } : {
+ display: 'none',
+ visibility: 'hidden' as const,
+ padding: '0.5em',
+ paddingLeft: '2em',
+ };
- return (
-
-
-
-
Next Steps
-
- {steps}
-
-
-
{expandIcon}
+ return (
+
+ );
}
export default NextSteps;
diff --git a/modules/dataquery/jsx/querydef.tsx b/modules/dataquery/jsx/querydef.tsx
index 5c28b34362a..2cac2d7bdf4 100644
--- a/modules/dataquery/jsx/querydef.tsx
+++ b/modules/dataquery/jsx/querydef.tsx
@@ -20,19 +20,19 @@ export class QueryTerm {
* @param {array} visits - the visits for the criteria
*/
constructor(
- module: string,
- category: string,
- fieldname: string,
- op: string,
- value: string|string[],
- visits?: string[]
+ module: string,
+ category: string,
+ fieldname: string,
+ op: string,
+ value: string|string[],
+ visits?: string[]
) {
- this.module = module;
- this.category = category;
- this.fieldname = fieldname;
- this.op = op;
- this.value = value;
- this.visits = visits;
+ this.module = module;
+ this.category = category;
+ this.fieldname = fieldname;
+ this.op = op;
+ this.value = value;
+ this.visits = visits;
}
}
@@ -49,8 +49,8 @@ export class QueryGroup {
* @param {string} op -- 'and' or 'or' -- the operator used for this group
*/
constructor(op: 'and' | 'or') {
- this.operator = op;
- this.group = [];
+ this.operator = op;
+ this.group = [];
}
/**
@@ -59,7 +59,7 @@ export class QueryGroup {
* @param {object} condition - the term's conditions
*/
addTerm(condition: QueryTerm) {
- this.group.push(condition);
+ this.group.push(condition);
}
/**
@@ -69,10 +69,10 @@ export class QueryGroup {
* @returns {QueryGroup} - the new querygroup
*/
removeTerm(idx: number): QueryGroup {
- this.group = this.group.filter((el, fidx) => {
- return idx != fidx;
- });
- return this;
+ this.group = this.group.filter((el, fidx) => {
+ return idx != fidx;
+ });
+ return this;
}
/**
@@ -80,10 +80,10 @@ export class QueryGroup {
* as a subgroup.
*/
addGroup(): void {
- // The default operation for a subgroup
- // is the opposite of this one, otherwise
- // there would be no reason for a new group
- const newOp = this.operator == 'and' ? 'or' : 'and';
- this.group.push(new QueryGroup(newOp));
+ // The default operation for a subgroup
+ // is the opposite of this one, otherwise
+ // there would be no reason for a new group
+ const newOp = this.operator == 'and' ? 'or' : 'and';
+ this.group.push(new QueryGroup(newOp));
}
}
diff --git a/modules/dataquery/jsx/querytree.tsx b/modules/dataquery/jsx/querytree.tsx
index 1cb319674cd..ca741839935 100644
--- a/modules/dataquery/jsx/querytree.tsx
+++ b/modules/dataquery/jsx/querytree.tsx
@@ -11,10 +11,10 @@ import {FullDictionary} from './types';
* @returns {string} - The next colour after c
*/
function alternateColour(c: string): string {
- if (c == 'rgb(255, 255, 255)') {
- return 'rgb(240, 240, 240)';
- }
- return 'rgb(255, 255, 255)';
+ if (c == 'rgb(255, 255, 255)') {
+ return 'rgb(240, 240, 240)';
+ }
+ return 'rgb(255, 255, 255)';
}
/**
@@ -58,228 +58,228 @@ function QueryTree(props: {
mapModuleName: (module: string) => string,
mapCategoryName: (module: string, category: string) => string,
}) {
- const [deleteItemIndex, setDeleteItemIndex] = useState
(null);
+ const [deleteItemIndex, setDeleteItemIndex] = useState(null);
- /**
- * Render a single term of the QueryTree group.
- *
- * @param {QueryGroup|QueryTerm} item - The item to render from a group
- * @param {number} i - the index being rendered
- * @returns {React.ReactElement} - The react element
- */
- const renderitem =
+ /**
+ * Render a single term of the QueryTree group.
+ *
+ * @param {QueryGroup|QueryTerm} item - The item to render from a group
+ * @param {number} i - the index being rendered
+ * @returns {React.ReactElement} - The react element
+ */
+ const renderitem =
(item: QueryGroup|QueryTerm, i: number): React.ReactElement => {
- const operator = i != props.items.group.length-1 ?
+ const operator = i != props.items.group.length-1 ?
props.items.operator : '';
- const style: React.CSSProperties = {
+ const style: React.CSSProperties = {
display: 'flex' as const,
flexDirection: 'column' as const,
width: '100%',
- };
- const operatorStyle = {
+ };
+ const operatorStyle = {
alignSelf: 'center',
fontWeight: 'bold',
- };
- if (deleteItemIndex == i) {
+ };
+ if (deleteItemIndex == i) {
style.textDecoration = 'line-through';
- }
+ }
- /**
- * Deletes an item from the group and call the removeQueryGroupItem
- * callback.
- *
- * @returns {void}
- */
- const deleteItem = () => {
+ /**
+ * Deletes an item from the group and call the removeQueryGroupItem
+ * callback.
+ *
+ * @returns {void}
+ */
+ const deleteItem = () => {
if (props.removeQueryGroupItem) {
- const newquery = props.removeQueryGroupItem(
- props.items,
- i,
- );
- if (props.setModalGroup) {
- props.setModalGroup(newquery);
- }
+ const newquery = props.removeQueryGroupItem(
+ props.items,
+ i,
+ );
+ if (props.setModalGroup) {
+ props.setModalGroup(newquery);
+ }
}
- };
- if (item instanceof QueryTerm) {
+ };
+ if (item instanceof QueryTerm) {
const deleteIcon = props.removeQueryGroupItem ? (
-
- setDeleteItemIndex(i)}
- onMouseLeave={() => setDeleteItemIndex(null)}
- style={{cursor: 'pointer'}}
- />
-
+
+ setDeleteItemIndex(i)}
+ onMouseLeave={() => setDeleteItemIndex(null)}
+ style={{cursor: 'pointer'}}
+ />
+
) : '';
return
-
-
- {deleteIcon}
-
- {operator}
- ;
- } else if (item instanceof QueryGroup) {
+
+
+ {deleteIcon}
+
+ {operator}
+ ;
+ } else if (item instanceof QueryGroup) {
const buttonStyle: React.CSSProperties = deleteItemIndex == i ? {
- textDecoration: 'line-through',
+ textDecoration: 'line-through',
} : {};
return (
-
-
+ setDeleteItemIndex(i)}
- onDeleteLeave={
- () => setDeleteItemIndex(null)
- }
- subtree={true}
- fulldictionary={props.fulldictionary}
+ )
+ }
+ deleteItem={deleteItem}
+ buttonStyle={buttonStyle}
+ onDeleteHover={() => setDeleteItemIndex(i)}
+ onDeleteLeave={
+ () => setDeleteItemIndex(null)
+ }
+ subtree={true}
+ fulldictionary={props.fulldictionary}
- />
-
- {operator}
+ />
+
+
{operator}
);
- } else {
+ } else {
console.error('Invalid tree');
- }
- return
{i} ;
- };
+ }
+ return
{i} ;
+ };
- const terms: React.ReactElement[] = props.items.group.map(renderitem);
- let warning;
- switch (props.items.group.length) {
- case 0:
- warning =
-
-
+ const terms: React.ReactElement[] = props.items.group.map(renderitem);
+ let warning;
+ switch (props.items.group.length) {
+ case 0:
+ warning =
+
+
Group does not have any items.
-
-
;
- break;
- case 1:
- warning =
;
+ break;
+ case 1:
+ warning =
+
+
Group only has 1 item. A group with only 1 item is equivalent
to not having the group.
-
-
;
- break;
- }
+
+
;
+ break;
+ }
- /**
- * Handler to calls newItem callback onClick
- *
- * @param {React.MouseEvent} e - The event
- * @returns {void}
- */
- const newItemClick = (e: React.MouseEvent) => {
- e.preventDefault();
- if (props.newItem) {
- props.newItem(props.items);
- }
- };
-
- /**
- * Call newGroup callback onClick
- *
- * @param {React.MouseEvent} e - The event
- * @returns {void}
- */
- const newGroupClick = (e: React.MouseEvent) => {
- e.preventDefault();
- if (props.newGroup) {
- props.newGroup(props.items);
- }
- };
+ /**
+ * Handler to calls newItem callback onClick
+ *
+ * @param {React.MouseEvent} e - The event
+ * @returns {void}
+ */
+ const newItemClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ if (props.newItem) {
+ props.newItem(props.items);
+ }
+ };
- const antiOperator = props.items.operator == 'and' ? 'or' : 'and';
- const style: React.CSSProperties= {};
- if (props.activeGroup == props.items) {
- style.background = 'pink';
+ /**
+ * Call newGroup callback onClick
+ *
+ * @param {React.MouseEvent} e - The event
+ * @returns {void}
+ */
+ const newGroupClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ if (props.newGroup) {
+ props.newGroup(props.items);
}
+ };
- let deleteGroupHTML;
- if (props.deleteItem) {
- deleteGroupHTML = (
-
-
-
- );
- }
- const marginStyle: React.CSSProperties = props.subtree === true ? {} : {
- margin: 0,
- padding: 0,
- };
- return (
-
-
- {terms}
-
-
-
-
+
+
+ );
+ }
+ const marginStyle: React.CSSProperties = props.subtree === true ? {} : {
+ margin: 0,
+ padding: 0,
+ };
+ return (
+
+
+ {terms}
+
+
+
+
-
-
-
-
- {warning}
- {deleteGroupHTML}
+ onUserInput={newItemClick}
+ style={props.buttonStyle}
+ columnSize='col-sm-12'
+ />
+
+
+
+
+ {warning}
+ {deleteGroupHTML}
-
-
- );
+
+
+ );
}
export default QueryTree;
diff --git a/modules/dataquery/jsx/viewdata.tsx b/modules/dataquery/jsx/viewdata.tsx
index cea6e7d6bb5..cf3afb68c38 100644
--- a/modules/dataquery/jsx/viewdata.tsx
+++ b/modules/dataquery/jsx/viewdata.tsx
@@ -28,17 +28,17 @@ type SessionRowCell = {
* @returns {string} the non-JSON value
*/
function cellValue(data: string) {
- try {
- const parsed = JSON.parse(data);
- if (typeof parsed === 'object') {
- // Can't include objects as react children, if we got here
- // there's probably a bug.
- return data;
- }
- return parsed;
- } catch (e) {
- return data;
+ try {
+ const parsed = JSON.parse(data);
+ if (typeof parsed === 'object') {
+ // Can't include objects as react children, if we got here
+ // there's probably a bug.
+ return data;
}
+ return parsed;
+ } catch (e) {
+ return data;
+ }
}
/**
@@ -50,7 +50,7 @@ function cellValue(data: string) {
* @returns {React.ReactElement} - the Table Cell
*/
function TableCell(props: {data: string}) {
- return
{cellValue(props.data)} ;
+ return
{cellValue(props.data)} ;
}
enum EnumDisplayTypes {
@@ -72,34 +72,34 @@ function DisplayValue(props: {
dictionary: FieldDictionary,
enumDisplay: EnumDisplayTypes}
) {
- let display = props.value;
- switch (props.enumDisplay) {
- case EnumDisplayTypes.EnumLabel:
- if (props.dictionary.labels && props.dictionary.options) {
- for (let i = 0; i < props.dictionary.options.length; i++) {
- if (props.dictionary.options[i] == props.value) {
- display= props.dictionary.labels[i];
- break;
- }
- }
- }
- break;
+ let display = props.value;
+ switch (props.enumDisplay) {
+ case EnumDisplayTypes.EnumLabel:
+ if (props.dictionary.labels && props.dictionary.options) {
+ for (let i = 0; i < props.dictionary.options.length; i++) {
+ if (props.dictionary.options[i] == props.value) {
+ display= props.dictionary.labels[i];
+ break;
+ }
+ }
}
+ break;
+ }
- if (props.value === true) {
- return 'True';
- } else if (props.value === false) {
- return 'False';
- }
+ if (props.value === true) {
+ return 'True';
+ } else if (props.value === false) {
+ return 'False';
+ }
- if (props.dictionary.type == 'URI') {
- display = (
-
- {display}
-
- );
- }
- return display;
+ if (props.dictionary.type == 'URI') {
+ display = (
+
+ {display}
+
+ );
+ }
+ return display;
}
/**
@@ -112,36 +112,36 @@ function DisplayValue(props: {
* @returns {React.ReactElement} - The ProgressBar element
*/
function ProgressBar(props: {type: string, value: number, max: number}) {
- switch (props.type) {
- case 'loading':
- if (props.value == 0) {
- return
Query not yet run ;
- }
- return (
-
Loading data:
-
- {props.value} of {props.max} candidates
-
-
);
- case 'headers':
- return (
-
Organizing headers:
-
- {props.value} of {props.max} columns
-
-
);
- case 'dataorganization':
- return (
-
Organizing data:
-
- {props.value} of {props.max} columns
-
-
);
+ switch (props.type) {
+ case 'loading':
+ if (props.value == 0) {
+ return
Query not yet run ;
}
- return
Invalid progress type: {props.type} ;
+ return (
+
Loading data:
+
+ {props.value} of {props.max} candidates
+
+
);
+ case 'headers':
+ return (
+
Organizing headers:
+
+ {props.value} of {props.max} columns
+
+
);
+ case 'dataorganization':
+ return (
+
Organizing data:
+
+ {props.value} of {props.max} columns
+
+
);
+ }
+ return
Invalid progress type: {props.type} ;
}
type RunQueryType = {
@@ -158,77 +158,77 @@ type RunQueryType = {
* @returns {RunQueryType} - a description of the status of the loading and the loaded values
*/
function useRunQuery(
- fields: APIQueryField[],
- filters: QueryGroup,
- onRun: () => void
+ fields: APIQueryField[],
+ filters: QueryGroup,
+ onRun: () => void
): RunQueryType {
- const [expectedResults, setExpectedResults] = useState
(0);
- const [resultData, setResultData] = useState([]);
- const [loading, setLoading] = useState(false);
+ const [expectedResults, setExpectedResults] = useState(0);
+ const [resultData, setResultData] = useState([]);
+ const [loading, setLoading] = useState(false);
- useEffect(() => {
- setLoading(true);
- const payload: APIQueryObject = calcPayload(fields, filters);
+ useEffect(() => {
+ setLoading(true);
+ const payload: APIQueryObject = calcPayload(fields, filters);
+ fetch(
+ '/dataquery/queries',
+ {
+ method: 'post',
+ credentials: 'same-origin',
+ body: JSON.stringify(payload),
+ },
+ ).then(
+ (resp) => {
+ if (!resp.ok) {
+ throw new Error('Error creating query.');
+ }
+ return resp.json();
+ }
+ ).then(
+ (data) => {
+ const resultbuffer: any[] = [];
fetch(
- '/dataquery/queries',
- {
- method: 'post',
- credentials: 'same-origin',
- body: JSON.stringify(payload),
- },
- ).then(
- (resp) => {
- if (!resp.ok) {
- throw new Error('Error creating query.');
- }
- return resp.json();
- }
- ).then(
- (data) => {
- const resultbuffer: any[] = [];
- fetch(
- '/dataquery/queries/'
+ '/dataquery/queries/'
+ data.QueryID + '/count',
- {
- method: 'GET',
- credentials: 'same-origin',
- }
- ).then((resp) => resp.json()
- ).then( (json) => {
- setExpectedResults(json.count);
- });
- fetchDataStream(
- '/dataquery/queries/' + data.QueryID + '/run',
- (row: any) => {
- resultbuffer.push(row);
- },
- () => {
- if (resultbuffer.length % 10 == 0) {
- setResultData([...resultbuffer]);
- }
- },
- () => {
- setResultData([...resultbuffer]);
- setLoading(false);
- },
- 'post',
- );
- onRun(); // forces query list to be reloaded
- }
- ).catch(
- (msg) => {
- swal.fire({
- type: 'error',
- text: msg,
- });
+ {
+ method: 'GET',
+ credentials: 'same-origin',
+ }
+ ).then((resp) => resp.json()
+ ).then( (json) => {
+ setExpectedResults(json.count);
+ });
+ fetchDataStream(
+ '/dataquery/queries/' + data.QueryID + '/run',
+ (row: any) => {
+ resultbuffer.push(row);
+ },
+ () => {
+ if (resultbuffer.length % 10 == 0) {
+ setResultData([...resultbuffer]);
}
+ },
+ () => {
+ setResultData([...resultbuffer]);
+ setLoading(false);
+ },
+ 'post',
);
- }, [fields, filters]);
- return {
- loading: loading,
- data: resultData,
- totalcount: expectedResults,
- };
+ onRun(); // forces query list to be reloaded
+ }
+ ).catch(
+ (msg) => {
+ swal.fire({
+ type: 'error',
+ text: msg,
+ });
+ }
+ );
+ }, [fields, filters]);
+ return {
+ loading: loading,
+ data: resultData,
+ totalcount: expectedResults,
+ };
}
type DataOrganizationType = {
@@ -248,50 +248,50 @@ type DataOrganizationType = {
* @returns {object} - the headers and data re-organised according to the user's selection
*/
function useDataOrganization(
- queryData: RunQueryType,
- visitOrganization: VisitOrgType,
- headerDisplay: HeaderDisplayType,
- fields: APIQueryField[],
- fulldictionary: FullDictionary
+ queryData: RunQueryType,
+ visitOrganization: VisitOrgType,
+ headerDisplay: HeaderDisplayType,
+ fields: APIQueryField[],
+ fulldictionary: FullDictionary
) : DataOrganizationType {
- const [tableData, setTableData] = useState([]);
- const [orgStatus, setOrgStatus]
+ const [tableData, setTableData] = useState([]);
+ const [orgStatus, setOrgStatus]
= useState<'headers'|'data'|'done'|null>(null);
- const [progress, setProgress] = useState(0);
- const [headers, setHeaders] = useState([]);
- useEffect( () => {
- if (queryData.loading == true) {
- return;
- }
- setOrgStatus('headers');
- organizeHeaders(fields,
- visitOrganization,
- headerDisplay,
- fulldictionary,
- (i) => setProgress(i),
- ).then( (headers: string[]) => {
- setHeaders(headers);
- setOrgStatus('data');
+ const [progress, setProgress] = useState(0);
+ const [headers, setHeaders] = useState([]);
+ useEffect( () => {
+ if (queryData.loading == true) {
+ return;
+ }
+ setOrgStatus('headers');
+ organizeHeaders(fields,
+ visitOrganization,
+ headerDisplay,
+ fulldictionary,
+ (i) => setProgress(i),
+ ).then( (headers: string[]) => {
+ setHeaders(headers);
+ setOrgStatus('data');
- organizeData(
- queryData.data,
- visitOrganization,
- fulldictionary,
- fields,
- (i) => setProgress(i),
- ).then((data: TableRow[]) => {
- setTableData(data);
- setOrgStatus('done');
- });
- });
- }, [visitOrganization, headerDisplay, queryData.loading, queryData.data]);
- return {
- 'headers': headers,
- 'data': tableData,
+ organizeData(
+ queryData.data,
+ visitOrganization,
+ fulldictionary,
+ fields,
+ (i) => setProgress(i),
+ ).then((data: TableRow[]) => {
+ setTableData(data);
+ setOrgStatus('done');
+ });
+ });
+ }, [visitOrganization, headerDisplay, queryData.loading, queryData.data]);
+ return {
+ 'headers': headers,
+ 'data': tableData,
- 'status': orgStatus,
- 'progress': progress,
- };
+ 'status': orgStatus,
+ 'progress': progress,
+ };
}
/**
@@ -310,163 +310,163 @@ function ViewData(props: {
onRun: () => void
fulldictionary: FullDictionary,
}) {
- const [visitOrganization, setVisitOrganization]
+ const [visitOrganization, setVisitOrganization]
= useState('inline');
- const [headerDisplay, setHeaderDisplay]
+ const [headerDisplay, setHeaderDisplay]
= useState('fieldnamedesc');
- const [enumDisplay, setEnumDisplay]
+ const [enumDisplay, setEnumDisplay]
= useState(EnumDisplayTypes.EnumLabel);
- const queryData = useRunQuery(props.fields, props.filters, props.onRun);
- const organizedData = useDataOrganization(
- queryData,
- visitOrganization,
- headerDisplay,
- props.fields,
- props.fulldictionary
- );
- const [emptyVisits, setEmptyVisits] = useState(true);
+ const queryData = useRunQuery(props.fields, props.filters, props.onRun);
+ const organizedData = useDataOrganization(
+ queryData,
+ visitOrganization,
+ headerDisplay,
+ props.fields,
+ props.fulldictionary
+ );
+ const [emptyVisits, setEmptyVisits] = useState(true);
- let queryTable;
- if (queryData.loading) {
- queryTable = ;
- } else {
- switch (organizedData['status']) {
- case null:
- return queryTable = Query not yet run ;
- case 'headers':
- queryTable = ;
- break;
- case 'data':
- queryTable = ;
- break;
- case 'done':
- try {
- queryTable = {
- return {show: true, label: val};
- })
- }
- data={organizedData.data}
- getMappedCell={
- organizedMapper(
- visitOrganization,
- props.fields,
- props.fulldictionary,
- )
- }
- getFormattedCell={
- organizedFormatter(
- queryData.data,
- visitOrganization,
- props.fields,
- props.fulldictionary,
- emptyVisits,
- enumDisplay,
- )
- }
- hide={
- {
- rowsPerPage: false,
- defaultColumn: true,
- downloadCSV: visitOrganization == 'inline',
- }
- }
- />;
- } catch (e) {
- // OrganizedMapper/Formatter can throw an error
- // before the loading is complete
- return Loading..
;
+ let queryTable;
+ if (queryData.loading) {
+ queryTable = ;
+ } else {
+ switch (organizedData['status']) {
+ case null:
+ return queryTable = Query not yet run ;
+ case 'headers':
+ queryTable = ;
+ break;
+ case 'data':
+ queryTable = ;
+ break;
+ case 'done':
+ try {
+ queryTable = {
+ return {show: true, label: val};
+ })
+ }
+ data={organizedData.data}
+ getMappedCell={
+ organizedMapper(
+ visitOrganization,
+ props.fields,
+ props.fulldictionary,
+ )
+ }
+ getFormattedCell={
+ organizedFormatter(
+ queryData.data,
+ visitOrganization,
+ props.fields,
+ props.fulldictionary,
+ emptyVisits,
+ enumDisplay,
+ )
+ }
+ hide={
+ {
+ rowsPerPage: false,
+ defaultColumn: true,
+ downloadCSV: visitOrganization == 'inline',
}
- break;
- default:
- throw new Error('Unhandled organization status');
- }
+ }
+ />;
+ } catch (e) {
+ // OrganizedMapper/Formatter can throw an error
+ // before the loading is complete
+ return Loading..
;
+ }
+ break;
+ default:
+ throw new Error('Unhandled organization status');
}
+ }
- const emptyCheckbox = (visitOrganization === 'inline' ?
-
- setEmptyVisits(value)
- }
- />
- :
);
- return
-
- setHeaderDisplay(value)
- }
- sortByValue={false}
- />
-
- setVisitOrganization(value)
- }
- sortByValue={false}
- />
- {
- if (value == 'labels') {
- setEnumDisplay(EnumDisplayTypes.EnumLabel);
- } else {
- setEnumDisplay(EnumDisplayTypes.EnumValue);
- }
- }
- }
- sortByValue={false}
- />
- {emptyCheckbox}
- {queryTable}
-
;
+ const emptyCheckbox = (visitOrganization === 'inline' ?
+
+ setEmptyVisits(value)
+ }
+ />
+ :
);
+ return
+
+ setHeaderDisplay(value)
+ }
+ sortByValue={false}
+ />
+
+ setVisitOrganization(value)
+ }
+ sortByValue={false}
+ />
+ {
+ if (value == 'labels') {
+ setEnumDisplay(EnumDisplayTypes.EnumLabel);
+ } else {
+ setEnumDisplay(EnumDisplayTypes.EnumValue);
+ }
+ }
+ }
+ sortByValue={false}
+ />
+ {emptyCheckbox}
+ {queryTable}
+
;
}
/**
@@ -485,135 +485,135 @@ function ViewData(props: {
* for the sessions or headers.
*/
function organizeData(
- resultData: string[][],
- visitOrganization: VisitOrgType,
- fulldict: FullDictionary,
- fields: APIQueryField[],
- onProgress: (i: number) => void
+ resultData: string[][],
+ visitOrganization: VisitOrgType,
+ fulldict: FullDictionary,
+ fields: APIQueryField[],
+ onProgress: (i: number) => void
) : Promise {
- switch (visitOrganization) {
- case 'raw':
- return Promise.resolve(resultData);
- case 'inline':
- // Organize with flexbox within the cell by the
- // formatter
- return Promise.resolve(resultData);
- case 'longitudinal':
- // the formatter splits into multiple cells
- return Promise.resolve(resultData);
- case 'crosssection':
- return new Promise((resolve) => {
- let rowNum = 0;
- const promises: Promise[] = [];
- for (const candidaterow of resultData) {
- promises.push(new Promise((resolve) => {
- // Collect list of visits for this candidate
- setTimeout( () => {
- const candidatevisits: {[visit: string]: boolean} = {};
- for (const i in candidaterow) {
- if (!candidaterow.hasOwnProperty(i)) {
- continue;
- }
- const dictionary = getDictionary(fields[i], fulldict);
- if (dictionary && dictionary.scope == 'session') {
- if (candidaterow[i] === null
+ switch (visitOrganization) {
+ case 'raw':
+ return Promise.resolve(resultData);
+ case 'inline':
+ // Organize with flexbox within the cell by the
+ // formatter
+ return Promise.resolve(resultData);
+ case 'longitudinal':
+ // the formatter splits into multiple cells
+ return Promise.resolve(resultData);
+ case 'crosssection':
+ return new Promise((resolve) => {
+ let rowNum = 0;
+ const promises: Promise[] = [];
+ for (const candidaterow of resultData) {
+ promises.push(new Promise((resolve) => {
+ // Collect list of visits for this candidate
+ setTimeout( () => {
+ const candidatevisits: {[visit: string]: boolean} = {};
+ for (const i in candidaterow) {
+ if (!candidaterow.hasOwnProperty(i)) {
+ continue;
+ }
+ const dictionary = getDictionary(fields[i], fulldict);
+ if (dictionary && dictionary.scope == 'session') {
+ if (candidaterow[i] === null
|| candidaterow[i] == '') {
- continue;
- }
- const cellobj: any = JSON.parse(candidaterow[i]);
- for (const session in cellobj) {
- if (!cellobj.hasOwnProperty(session)
+ continue;
+ }
+ const cellobj: any = JSON.parse(candidaterow[i]);
+ for (const session in cellobj) {
+ if (!cellobj.hasOwnProperty(session)
|| session === 'keytype') {
- continue;
- }
- const vl: string = cellobj[session].VisitLabel;
- candidatevisits[vl] = true;
- }
- }
- }
+ continue;
+ }
+ const vl: string = cellobj[session].VisitLabel;
+ candidatevisits[vl] = true;
+ }
+ }
+ }
- const dataRows: TableRow[] = [];
- for (const visit in candidatevisits) {
- if (!candidatevisits.hasOwnProperty(visit)) {
- continue;
- }
- const dataRow: TableRow = [];
- dataRow.push(visit);
- for (let i = 0; i < candidaterow.length; i++) {
- const dictionary = getDictionary(fields[i], fulldict);
- if (dictionary && dictionary.scope == 'session') {
- if (candidaterow[i] === null
+ const dataRows: TableRow[] = [];
+ for (const visit in candidatevisits) {
+ if (!candidatevisits.hasOwnProperty(visit)) {
+ continue;
+ }
+ const dataRow: TableRow = [];
+ dataRow.push(visit);
+ for (let i = 0; i < candidaterow.length; i++) {
+ const dictionary = getDictionary(fields[i], fulldict);
+ if (dictionary && dictionary.scope == 'session') {
+ if (candidaterow[i] === null
|| candidaterow[i] == '') {
- dataRow.push(null);
- continue;
- }
- const allCells: SessionRowCell[] = Object.values(
- JSON.parse(candidaterow[i]));
- const values: SessionRowCell[] = allCells.filter(
- (sessionval: SessionRowCell) => {
- return sessionval.VisitLabel == visit;
- }
- );
- switch (values.length) {
- case 0:
- dataRow.push(null);
- break;
- case 1:
- switch (dictionary.cardinality) {
- case 'many':
- if (typeof values[0].values === 'undefined') {
- dataRow.push(null);
- } else {
- const thevalues = values[0].values;
- // I don't think this if statement should be required because of the
- // above if statement, but without it typescript gives an error
- // about Object.keys on possible type undefined.
- if (!thevalues) {
- dataRow.push(null);
- } else {
- const mappedVals = Object.keys(thevalues)
- .map(
- (key) => key + '=' + thevalues[key]
- )
- .join(';');
- dataRow.push(mappedVals);
- }
- }
- break;
- default:
- if (typeof values[0].value === 'undefined') {
- dataRow.push(null);
- } else {
- dataRow.push(values[0].value);
- }
- break;
- }
- break;
- default:
- throw new Error('Too many visit values');
- }
- } else {
- dataRow.push(candidaterow[i]);
- }
+ dataRow.push(null);
+ continue;
+ }
+ const allCells: SessionRowCell[] = Object.values(
+ JSON.parse(candidaterow[i]));
+ const values: SessionRowCell[] = allCells.filter(
+ (sessionval: SessionRowCell) => {
+ return sessionval.VisitLabel == visit;
+ }
+ );
+ switch (values.length) {
+ case 0:
+ dataRow.push(null);
+ break;
+ case 1:
+ switch (dictionary.cardinality) {
+ case 'many':
+ if (typeof values[0].values === 'undefined') {
+ dataRow.push(null);
+ } else {
+ const thevalues = values[0].values;
+ // I don't think this if statement should be required because of the
+ // above if statement, but without it typescript gives an error
+ // about Object.keys on possible type undefined.
+ if (!thevalues) {
+ dataRow.push(null);
+ } else {
+ const mappedVals = Object.keys(thevalues)
+ .map(
+ (key) => key + '=' + thevalues[key]
+ )
+ .join(';');
+ dataRow.push(mappedVals);
}
- dataRows.push(dataRow);
+ }
+ break;
+ default:
+ if (typeof values[0].value === 'undefined') {
+ dataRow.push(null);
+ } else {
+ dataRow.push(values[0].value);
+ }
+ break;
}
- onProgress(rowNum++);
- resolve(dataRows);
- });
- }));
+ break;
+ default:
+ throw new Error('Too many visit values');
+ }
+ } else {
+ dataRow.push(candidaterow[i]);
+ }
+ }
+ dataRows.push(dataRow);
}
+ onProgress(rowNum++);
+ resolve(dataRows);
+ });
+ }));
+ }
- Promise.all(promises).then((values: TableRow[][]) => {
- const mappedData: TableRow[] = [];
- for (const row of values) {
- mappedData.push(...row);
- }
- resolve(mappedData);
- });
- });
- default: throw new Error('Unhandled visit organization');
- }
+ Promise.all(promises).then((values: TableRow[][]) => {
+ const mappedData: TableRow[] = [];
+ for (const row of values) {
+ mappedData.push(...row);
+ }
+ resolve(mappedData);
+ });
+ });
+ default: throw new Error('Unhandled visit organization');
+ }
}
/**
@@ -626,48 +626,48 @@ function organizeData(
* @returns {function} - the appropriate column formatter for this data organization
*/
function organizedMapper(
- visitOrganization: VisitOrgType,
- fields: APIQueryField[],
- dict: FullDictionary
+ visitOrganization: VisitOrgType,
+ fields: APIQueryField[],
+ dict: FullDictionary
) {
- switch (visitOrganization) {
- case 'raw':
- return (fieldlabel: string, value: string|null): string => {
- if (value === null) {
- return '';
- }
- return value;
- };
- case 'crosssection':
- return (fieldlabel: string, value: string|null): string => {
- if (value === null) {
- return '';
- }
+ switch (visitOrganization) {
+ case 'raw':
+ return (fieldlabel: string, value: string|null): string => {
+ if (value === null) {
+ return '';
+ }
+ return value;
+ };
+ case 'crosssection':
+ return (fieldlabel: string, value: string|null): string => {
+ if (value === null) {
+ return '';
+ }
- return cellValue(value);
- };
- case 'longitudinal':
- return (label: string,
- value: string|null,
- row: TableRow,
- headers: string[],
- fieldNo: number): (string|null)[]|string|null => {
- if (value === null) {
- return '';
- }
- const cells = expandLongitudinalCells(value, fieldNo, fields, dict);
- if (cells === null) {
- return null;
- }
- return cells.map( (cell: LongitudinalExpansion): string => {
- if (cell.value === null) {
- return '';
- }
- return cellValue(cell.value);
- });
- };
- default: return (): string => 'error';
- }
+ return cellValue(value);
+ };
+ case 'longitudinal':
+ return (label: string,
+ value: string|null,
+ row: TableRow,
+ headers: string[],
+ fieldNo: number): (string|null)[]|string|null => {
+ if (value === null) {
+ return '';
+ }
+ const cells = expandLongitudinalCells(value, fieldNo, fields, dict);
+ if (cells === null) {
+ return null;
+ }
+ return cells.map( (cell: LongitudinalExpansion): string => {
+ if (cell.value === null) {
+ return '';
+ }
+ return cellValue(cell.value);
+ });
+ };
+ default: return (): string => 'error';
+ }
}
type LongitudinalExpansion = {
@@ -689,90 +689,90 @@ type LongitudinalExpansion = {
* there are no table cells to be added based on this data.
*/
function expandLongitudinalCells(
- value: string|null,
- fieldNo: number,
- fields: APIQueryField[],
- dict: FullDictionary
+ value: string|null,
+ fieldNo: number,
+ fields: APIQueryField[],
+ dict: FullDictionary
): LongitudinalExpansion[]|null {
- // We added num fields * num visits headers, but
- // resultData only has numFields rows. For each row
- // we add multiple table cells for the number of visits
- // for that fieldNo. ie. we treat cellPos as fieldNo.
- // This means we need to bail once we've passed the
- // number of fields we have in resultData.
- if (fieldNo >= fields.length) {
- return null;
+ // We added num fields * num visits headers, but
+ // resultData only has numFields rows. For each row
+ // we add multiple table cells for the number of visits
+ // for that fieldNo. ie. we treat cellPos as fieldNo.
+ // This means we need to bail once we've passed the
+ // number of fields we have in resultData.
+ if (fieldNo >= fields.length) {
+ return null;
+ }
+ // if candidate -- return directly
+ // if session -- get visits from query def, put in
+ const fieldobj = fields[fieldNo];
+ const fielddict = getDictionary(fieldobj, dict);
+ if (fielddict === null) {
+ return null;
+ }
+ switch (fielddict.scope) {
+ case 'candidate':
+ if (fielddict.cardinality == 'many') {
+ throw new Error('Candidate cardinality many not implemented');
}
- // if candidate -- return directly
- // if session -- get visits from query def, put in
- const fieldobj = fields[fieldNo];
- const fielddict = getDictionary(fieldobj, dict);
- if (fielddict === null) {
- return null;
+ return [{value: value, dictionary: fielddict}];
+ case 'session':
+ let displayedVisits: string[];
+ if (fieldobj.visits) {
+ displayedVisits = fieldobj.visits;
+ } else {
+ // All visits
+ if (fielddict.visits) {
+ displayedVisits = fielddict.visits;
+ } else {
+ displayedVisits = [];
+ }
}
- switch (fielddict.scope) {
- case 'candidate':
- if (fielddict.cardinality == 'many') {
- throw new Error('Candidate cardinality many not implemented');
- }
- return [{value: value, dictionary: fielddict}];
- case 'session':
- let displayedVisits: string[];
- if (fieldobj.visits) {
- displayedVisits = fieldobj.visits;
- } else {
- // All visits
- if (fielddict.visits) {
- displayedVisits = fielddict.visits;
- } else {
- displayedVisits = [];
- }
- }
- if (!displayedVisits) {
- displayedVisits = [];
- }
- let celldata: {[sessionid: string]: SessionRowCell};
- try {
- celldata = JSON.parse(value || '{}');
- } catch (e) {
- // This can sometimes happen when we go between Cross-Sectional
- // and Longitudinal and the data is in an inconsistent state
- // between renders, so instead of throwing an error (which crashes
- // the whole app), we just log to the console and return null.
- console.error('Internal error parsing: "' + value + '"');
- return null;
- }
- const values = displayedVisits.map((visit): LongitudinalExpansion => {
- if (!value) {
- return {value: null, dictionary: fielddict};
+ if (!displayedVisits) {
+ displayedVisits = [];
+ }
+ let celldata: {[sessionid: string]: SessionRowCell};
+ try {
+ celldata = JSON.parse(value || '{}');
+ } catch (e) {
+ // This can sometimes happen when we go between Cross-Sectional
+ // and Longitudinal and the data is in an inconsistent state
+ // between renders, so instead of throwing an error (which crashes
+ // the whole app), we just log to the console and return null.
+ console.error('Internal error parsing: "' + value + '"');
+ return null;
+ }
+ const values = displayedVisits.map((visit): LongitudinalExpansion => {
+ if (!value) {
+ return {value: null, dictionary: fielddict};
+ }
+ for (const session in celldata) {
+ if (celldata[session].VisitLabel == visit) {
+ const thissession: SessionRowCell = celldata[session];
+ switch (fielddict.cardinality) {
+ case 'many':
+ if (thissession.values === undefined) {
+ return {value: null, dictionary: fielddict};
}
- for (const session in celldata) {
- if (celldata[session].VisitLabel == visit) {
- const thissession: SessionRowCell = celldata[session];
- switch (fielddict.cardinality) {
- case 'many':
- if (thissession.values === undefined) {
- return {value: null, dictionary: fielddict};
- }
- const thevalues = thissession.values;
- return {value: Object.keys(thevalues)
- .map( (key) => key + '=' + thevalues[key])
- .join(';'), dictionary: fielddict};
- default:
- if (thissession.value !== undefined) {
- return {
- value: thissession.value,
- dictionary: fielddict,
- };
- }
- throw new Error('Value was undefined');
- }
- }
+ const thevalues = thissession.values;
+ return {value: Object.keys(thevalues)
+ .map( (key) => key + '=' + thevalues[key])
+ .join(';'), dictionary: fielddict};
+ default:
+ if (thissession.value !== undefined) {
+ return {
+ value: thissession.value,
+ dictionary: fielddict,
+ };
}
- return {value: null, dictionary: fielddict};
- });
- return values;
- }
+ throw new Error('Value was undefined');
+ }
+ }
+ }
+ return {value: null, dictionary: fielddict};
+ });
+ return values;
+ }
}
/**
@@ -791,339 +791,339 @@ function expandLongitudinalCells(
this data organization
*/
function organizedFormatter(
- resultData: string[][],
- visitOrganization: VisitOrgType,
- fields: APIQueryField[],
- dict: FullDictionary,
- displayEmptyVisits: boolean,
- enumDisplay: EnumDisplayTypes,
+ resultData: string[][],
+ visitOrganization: VisitOrgType,
+ fields: APIQueryField[],
+ dict: FullDictionary,
+ displayEmptyVisits: boolean,
+ enumDisplay: EnumDisplayTypes,
) {
- let callback;
- switch (visitOrganization) {
- case 'raw':
- /**
- * Callback to return the raw JSON data as returned by the API, in
- * table form for the DataTable
- *
- * @param {string} label - The table header
- * @param {string} cell - The cell value
- * @returns {React.ReactElement} - The table cell
- */
- callback = (label: string, cell: string): ReactNode => {
- return {cell} ;
- };
- return callback;
- case 'inline':
- /**
- * Callback to format the data as inline data, with a list for each
- * session inside of a cell for the candidate.
- *
- * @param {string} label - The table header
- * @param {string} cell - The cell value
- * @param {string[]} row - The entire row
- * @param {string[]} headers - The entire row's headers
- * @param {number} fieldNo - the cell index
- * @returns {React.ReactElement} - The table cell
- */
- callback = (
- label: string,
- cell: string,
- row: TableRow,
- headers: string[],
- fieldNo: number
- ): ReactNode => {
- // if candidate -- return directly
- // if session -- get visits from query def, put in
- const fieldobj = fields[fieldNo];
- const fielddict = getDictionary(fieldobj, dict);
- if (fielddict === null) {
+ let callback;
+ switch (visitOrganization) {
+ case 'raw':
+ /**
+ * Callback to return the raw JSON data as returned by the API, in
+ * table form for the DataTable
+ *
+ * @param {string} label - The table header
+ * @param {string} cell - The cell value
+ * @returns {React.ReactElement} - The table cell
+ */
+ callback = (label: string, cell: string): ReactNode => {
+ return {cell} ;
+ };
+ return callback;
+ case 'inline':
+ /**
+ * Callback to format the data as inline data, with a list for each
+ * session inside of a cell for the candidate.
+ *
+ * @param {string} label - The table header
+ * @param {string} cell - The cell value
+ * @param {string[]} row - The entire row
+ * @param {string[]} headers - The entire row's headers
+ * @param {number} fieldNo - the cell index
+ * @returns {React.ReactElement} - The table cell
+ */
+ callback = (
+ label: string,
+ cell: string,
+ row: TableRow,
+ headers: string[],
+ fieldNo: number
+ ): ReactNode => {
+ // if candidate -- return directly
+ // if session -- get visits from query def, put in
+ const fieldobj = fields[fieldNo];
+ const fielddict = getDictionary(fieldobj, dict);
+ if (fielddict === null) {
+ return null;
+ }
+ if (fielddict.scope == 'candidate') {
+ if (cell === '') {
+ return (No data) ;
+ }
+ switch (fielddict.cardinality) {
+ case 'many':
+ return (Not implemented) ;
+ case 'single':
+ case 'unique':
+ case 'optional':
+ return ;
+ default:
+ return (
+ (Internal Error. Unhandled cardinality:
+ {fielddict.cardinality})
+
+ );
+ }
+ }
+ let val: React.ReactNode;
+ if (fielddict.scope == 'session') {
+ let displayedVisits: string[];
+ if (fields[fieldNo] && fields[fieldNo].visits) {
+ // need to explicitly tell typescript it's defined otherwise
+ // it thinks visits is string[]|undefined
+ displayedVisits = fields[fieldNo].visits as string[];
+ } else {
+ // All visits
+ if (fielddict.visits) {
+ displayedVisits = fielddict.visits;
+ } else {
+ displayedVisits = [];
+ }
+ }
+ switch (fielddict.cardinality) {
+ case 'many':
+ val = displayedVisits.map((visit): React.ReactNode => {
+ let hasdata = false;
+ /**
+ * Map the JSON string from the cell returned by the
+ * API to a string to display to the user in the
+ * frontend for this visit.
+ *
+ * @param {string} visit - The visit being displayed
+ * @param {string} cell - The raw cell value
+ * @returns {string|null} - the display string
+ */
+ const visitval = (visit: string, cell: string) => {
+ if (cell === '') {
return null;
- }
- if (fielddict.scope == 'candidate') {
- if (cell === '') {
- return (No data) ;
- }
- switch (fielddict.cardinality) {
- case 'many':
- return (Not implemented) ;
- case 'single':
- case 'unique':
- case 'optional':
- return ;
- default:
- return (
- (Internal Error. Unhandled cardinality:
- {fielddict.cardinality})
-
- );
- }
- }
- let val: React.ReactNode;
- if (fielddict.scope == 'session') {
- let displayedVisits: string[];
- if (fields[fieldNo] && fields[fieldNo].visits) {
- // need to explicitly tell typescript it's defined otherwise
- // it thinks visits is string[]|undefined
- displayedVisits = fields[fieldNo].visits as string[];
- } else {
- // All visits
- if (fielddict.visits) {
- displayedVisits = fielddict.visits;
- } else {
- displayedVisits = [];
- }
- }
- switch (fielddict.cardinality) {
- case 'many':
- val = displayedVisits.map((visit): React.ReactNode => {
- let hasdata = false;
- /**
- * Map the JSON string from the cell returned by the
- * API to a string to display to the user in the
- * frontend for this visit.
- *
- * @param {string} visit - The visit being displayed
- * @param {string} cell - The raw cell value
- * @returns {string|null} - the display string
- */
- const visitval = (visit: string, cell: string) => {
- if (cell === '') {
- return null;
- }
+ }
- try {
- const json = JSON.parse(cell);
- for (const sessionid in json) {
- if (json[sessionid].VisitLabel == visit) {
- const values = json[sessionid].values;
- return ({
- Object.keys(values).map(
- (keyid: string):
+ try {
+ const json = JSON.parse(cell);
+ for (const sessionid in json) {
+ if (json[sessionid].VisitLabel == visit) {
+ const values = json[sessionid].values;
+ return ({
+ Object.keys(values).map(
+ (keyid: string):
React.ReactNode => {
- const val = values[keyid];
- if (val === null) {
- return;
- }
- hasdata = true;
- // Workarounds for line length
- const f = fielddict;
- const e = enumDisplay;
- const dval = (
-
- );
+ const val = values[keyid];
+ if (val === null) {
+ return;
+ }
+ hasdata = true;
+ // Workarounds for line length
+ const f = fielddict;
+ const e = enumDisplay;
+ const dval = (
+
+ );
- return (
-
-
{keyid}
- {dval}
-
- );
- })
- }
- );
- }
- }
- return null;
- } catch (e) {
- console.error(e);
- return (Internal error) ;
- }
- };
- let theval = visitval(visit, cell);
- if (!displayEmptyVisits && !hasdata) {
- return
;
- }
- if (theval === null) {
- theval = (No data) ;
- }
- return (
-
{visit}
-
-
- {theval}
-
-
);
- });
- break;
- default:
- val = displayedVisits.map((visit) => {
- let hasdata = false;
- /**
- * Maps the JSON value from the session to a list of
- * values to display to the user
- *
- * @param {string} visit - The visit label
- * @param {string} cell - The JSON returned by the API
- * for this cell
- * @returns {React.ReactElement} - The HTML list react element
- */
- const visitval = (visit: string, cell: string) => {
- if (cell === '') {
- return null;
- }
- try {
- const json = JSON.parse(cell);
- for (const sessionid in json) {
- if (json[sessionid].VisitLabel == visit) {
- hasdata = true;
- return ;
- }
- }
- } catch (e) {
- return (Internal error) ;
- }
- return null;
- };
- let theval = visitval(visit, cell);
- if (!displayEmptyVisits && !hasdata) {
- return
;
- }
- if (theval === null) {
- theval = (No data) ;
+ return (
+
+
{keyid}
+ {dval}
+
+ );
+ })
}
- return (
-
{visit}
-
-
- {theval}
-
-
);
- });
+ );
+ }
}
+ return null;
+ } catch (e) {
+ console.error(e);
+ return (Internal error) ;
+ }
+ };
+ let theval = visitval(visit, cell);
+ if (!displayEmptyVisits && !hasdata) {
+ return
;
+ }
+ if (theval === null) {
+ theval = (No data) ;
}
- const value = (
+
- {val}
+ }
+ >{visit}
+
+
+ {theval}
+
);
- return {value} ;
- };
- return callback;
- case 'longitudinal':
- /**
- * Callback to organize this data longitudinally
- *
- * @param {string} label - The header label
- * @param {string} cell - the JSON value of the cell
- * @param {string[]} row - the entire row
- * @param {string[]} headers - the headers for the table
- * @param {number} fieldNo - The field number of this cell
- * @returns {React.ReactElement} - The table cell
- */
- callback = (
- label: string,
- cell: string,
- row: TableRow,
- headers: string[],
- fieldNo: number
- ): ReactNode => {
- const cells = expandLongitudinalCells(cell, fieldNo, fields, dict);
- if (cells === null) {
+ });
+ break;
+ default:
+ val = displayedVisits.map((visit) => {
+ let hasdata = false;
+ /**
+ * Maps the JSON value from the session to a list of
+ * values to display to the user
+ *
+ * @param {string} visit - The visit label
+ * @param {string} cell - The JSON returned by the API
+ * for this cell
+ * @returns {React.ReactElement} - The HTML list react element
+ */
+ const visitval = (visit: string, cell: string) => {
+ if (cell === '') {
return null;
- }
- return <>{cells.map((cell: LongitudinalExpansion) => {
- if (cell.value === null) {
- return (No data) ;
+ }
+ try {
+ const json = JSON.parse(cell);
+ for (const sessionid in json) {
+ if (json[sessionid].VisitLabel == visit) {
+ hasdata = true;
+ return ;
+ }
}
-
- return (
-
- );
- })}>;
- };
- return callback;
- case 'crosssection':
- /**
- * Callback that organizes data cross-sectionally
- *
- * @param {string} label - The header label
- * @param {string} cell - the JSON value of the cell
- * @param {string[]} row - the entire row
- * @param {string[]} headers - the headers for the table
- * @param {number} fieldNo - The field number of this cell
- * @returns {React.ReactElement} - The table cell for this cell.
- */
- callback = (
- label: string,
- cell: string,
- row: TableRow,
- headers: string[],
- fieldNo: number
- ): ReactNode => {
- if (cell === null) {
- return No data for visit ;
+ } catch (e) {
+ return (Internal error) ;
+ }
+ return null;
+ };
+ let theval = visitval(visit, cell);
+ if (!displayEmptyVisits && !hasdata) {
+ return
;
}
- if (fieldNo == 0) {
- // automatically added Visit column
- return ;
+ if (theval === null) {
+ theval = (No data) ;
}
+ return (
+
{visit}
+
+
+ {theval}
+
+
);
+ });
+ }
+ }
+ const value = (
+ {val}
+
);
+ return {value} ;
+ };
+ return callback;
+ case 'longitudinal':
+ /**
+ * Callback to organize this data longitudinally
+ *
+ * @param {string} label - The header label
+ * @param {string} cell - the JSON value of the cell
+ * @param {string[]} row - the entire row
+ * @param {string[]} headers - the headers for the table
+ * @param {number} fieldNo - The field number of this cell
+ * @returns {React.ReactElement} - The table cell
+ */
+ callback = (
+ label: string,
+ cell: string,
+ row: TableRow,
+ headers: string[],
+ fieldNo: number
+ ): ReactNode => {
+ const cells = expandLongitudinalCells(cell, fieldNo, fields, dict);
+ if (cells === null) {
+ return null;
+ }
+ return <>{cells.map((cell: LongitudinalExpansion) => {
+ if (cell.value === null) {
+ return (No data) ;
+ }
+
+ return (
+
+ );
+ })}>;
+ };
+ return callback;
+ case 'crosssection':
+ /**
+ * Callback that organizes data cross-sectionally
+ *
+ * @param {string} label - The header label
+ * @param {string} cell - the JSON value of the cell
+ * @param {string[]} row - the entire row
+ * @param {string[]} headers - the headers for the table
+ * @param {number} fieldNo - The field number of this cell
+ * @returns {React.ReactElement} - The table cell for this cell.
+ */
+ callback = (
+ label: string,
+ cell: string,
+ row: TableRow,
+ headers: string[],
+ fieldNo: number
+ ): ReactNode => {
+ if (cell === null) {
+ return No data for visit ;
+ }
+ if (fieldNo == 0) {
+ // automatically added Visit column
+ return ;
+ }
- const fieldobj = fields[fieldNo-1];
- const fielddict = getDictionary(fieldobj, dict);
+ const fieldobj = fields[fieldNo-1];
+ const fielddict = getDictionary(fieldobj, dict);
- return fielddict === null
- ?
- : (
-
- );
- };
- return callback;
- }
+ return fielddict === null
+ ?
+ : (
+
+ );
+ };
+ return callback;
+ }
}
/**
@@ -1134,17 +1134,17 @@ function organizedFormatter(
* @returns {FieldDictionary?} - The field dictionary for this field
*/
function getDictionary(
- fieldobj: APIQueryField,
- dict: FullDictionary,
+ fieldobj: APIQueryField,
+ dict: FullDictionary,
): FieldDictionary|null {
- if (!dict || !fieldobj
+ if (!dict || !fieldobj
|| !dict[fieldobj.module]
|| !dict[fieldobj.module][fieldobj.category]
|| !dict[fieldobj.module][fieldobj.category][fieldobj.field]
- ) {
- return null;
- }
- return dict[fieldobj.module][fieldobj.category][fieldobj.field];
+ ) {
+ return null;
+ }
+ return dict[fieldobj.module][fieldobj.category][fieldobj.field];
}
type VisitOrgType = 'raw' | 'inline' | 'longitudinal' | 'crosssection';
@@ -1162,83 +1162,83 @@ type HeaderDisplayType = 'fieldname' | 'fielddesc' | 'fieldnamedesc';
* in the frontend table
*/
function organizeHeaders(
- fields: APIQueryField[],
- org: VisitOrgType,
- display: HeaderDisplayType,
- fulldict: FullDictionary,
- onProgress: (i: number) => void): Promise {
- /**
- * Format a header according to the selected display type
- *
- * @param {APIQueryField} header - The header to format
- * @returns {string} - The string to display to the user
- */
- const formatHeader = (header: APIQueryField): string => {
- switch (display) {
- case 'fieldname': return header.field;
- case 'fielddesc': return getDictionaryDescription(
- header.module,
- header.category,
- header.field,
- fulldict
- );
- case 'fieldnamedesc': return header.field +
+ fields: APIQueryField[],
+ org: VisitOrgType,
+ display: HeaderDisplayType,
+ fulldict: FullDictionary,
+ onProgress: (i: number) => void): Promise {
+ /**
+ * Format a header according to the selected display type
+ *
+ * @param {APIQueryField} header - The header to format
+ * @returns {string} - The string to display to the user
+ */
+ const formatHeader = (header: APIQueryField): string => {
+ switch (display) {
+ case 'fieldname': return header.field;
+ case 'fielddesc': return getDictionaryDescription(
+ header.module,
+ header.category,
+ header.field,
+ fulldict
+ );
+ case 'fieldnamedesc': return header.field +
': ' + getDictionaryDescription(
- header.module,
- header.category,
- header.field,
- fulldict
- );
- default:
- throw new Error('Unhandled field display type');
- }
- };
- switch (org) {
- case 'raw':
- return Promise.resolve(fields.map((val, i) => {
- onProgress(i);
- return formatHeader(val);
- }));
- case 'inline':
- return Promise.resolve(fields.map((val, i) => {
- onProgress(i);
- return formatHeader(val);
- }));
- case 'longitudinal':
- const headers: string[] = [];
- let i = 0;
- for (const field of fields) {
- i++;
- const dict = getDictionary(field, fulldict);
+ header.module,
+ header.category,
+ header.field,
+ fulldict
+ );
+ default:
+ throw new Error('Unhandled field display type');
+ }
+ };
+ switch (org) {
+ case 'raw':
+ return Promise.resolve(fields.map((val, i) => {
+ onProgress(i);
+ return formatHeader(val);
+ }));
+ case 'inline':
+ return Promise.resolve(fields.map((val, i) => {
+ onProgress(i);
+ return formatHeader(val);
+ }));
+ case 'longitudinal':
+ const headers: string[] = [];
+ let i = 0;
+ for (const field of fields) {
+ i++;
+ const dict = getDictionary(field, fulldict);
- if (dict === null) {
- headers.push('Internal Error');
- } else if (dict.scope == 'candidate') {
- headers.push(formatHeader(field));
- } else {
- if (typeof field.visits !== 'undefined') {
- for (const visit of field.visits) {
- headers.push(formatHeader(field) + ': ' + visit);
- }
- }
- }
- onProgress(i);
+ if (dict === null) {
+ headers.push('Internal Error');
+ } else if (dict.scope == 'candidate') {
+ headers.push(formatHeader(field));
+ } else {
+ if (typeof field.visits !== 'undefined') {
+ for (const visit of field.visits) {
+ headers.push(formatHeader(field) + ': ' + visit);
+ }
}
- // Split session level selections into multiple headers
- return Promise.resolve(headers);
- case 'crosssection':
- return new Promise( (resolve) => {
- setTimeout( () => {
- resolve(['Visit Label',
- ...fields.map((val, i) => {
- onProgress(i);
- return formatHeader(val);
- }),
- ]);
- });
- });
- default: throw new Error('Unhandled visit organization');
+ }
+ onProgress(i);
}
+ // Split session level selections into multiple headers
+ return Promise.resolve(headers);
+ case 'crosssection':
+ return new Promise( (resolve) => {
+ setTimeout( () => {
+ resolve(['Visit Label',
+ ...fields.map((val, i) => {
+ onProgress(i);
+ return formatHeader(val);
+ }),
+ ]);
+ });
+ });
+ default: throw new Error('Unhandled visit organization');
+ }
}
export default ViewData;
diff --git a/modules/dataquery/jsx/welcome.adminquerymodal.tsx b/modules/dataquery/jsx/welcome.adminquerymodal.tsx
index 959750ccf6c..52350901a29 100644
--- a/modules/dataquery/jsx/welcome.adminquerymodal.tsx
+++ b/modules/dataquery/jsx/welcome.adminquerymodal.tsx
@@ -20,76 +20,76 @@ function AdminQueryModal(props: {
onSubmit: (name: string, topQuery: boolean, dashboardQuery: boolean)
=> void,
}) {
- const [queryName, setQueryName] = useState(props.defaultName || '');
- const [topQuery, setTopQuery] = useState(true);
- const [dashboardQuery, setDashboardQuery] = useState(true);
- /**
- * Convert the onSubmit callback to a promise function of the format
- * expected by jsx/Modal.
- *
- * @returns {Promise} - The promise
- */
- const submitPromise = () => {
- let sbmt: Promise = new Promise((resolve, reject) => {
- if (queryName.trim() == '') {
- swal.fire({
- type: 'error',
- text: 'Must provide a query name to pin query as.',
- });
- reject();
- return;
- }
- if (!topQuery && !dashboardQuery) {
- swal.fire({
- type: 'error',
- text: 'Must pin as study query or pin to dashboard.',
- });
- reject();
- return;
- }
- resolve([queryName.trim(), topQuery, dashboardQuery]);
+ const [queryName, setQueryName] = useState(props.defaultName || '');
+ const [topQuery, setTopQuery] = useState(true);
+ const [dashboardQuery, setDashboardQuery] = useState(true);
+ /**
+ * Convert the onSubmit callback to a promise function of the format
+ * expected by jsx/Modal.
+ *
+ * @returns {Promise} - The promise
+ */
+ const submitPromise = () => {
+ let sbmt: Promise = new Promise((resolve, reject) => {
+ if (queryName.trim() == '') {
+ swal.fire({
+ type: 'error',
+ text: 'Must provide a query name to pin query as.',
});
- if (props.onSubmit) {
- sbmt = sbmt.then((val: [string, boolean, boolean]) => {
- const [name, topq, dashq] = val;
- props.onSubmit(name, topq, dashq);
- });
- }
- return sbmt;
- };
- return
-
-
- setQueryName(value)
- }
- />
- setTopQuery(value)
- }
- label='Pin Study Query'
- />
-
- setDashboardQuery(value)
- }
- />
-
-
- ;
+ reject();
+ return;
+ }
+ if (!topQuery && !dashboardQuery) {
+ swal.fire({
+ type: 'error',
+ text: 'Must pin as study query or pin to dashboard.',
+ });
+ reject();
+ return;
+ }
+ resolve([queryName.trim(), topQuery, dashboardQuery]);
+ });
+ if (props.onSubmit) {
+ sbmt = sbmt.then((val: [string, boolean, boolean]) => {
+ const [name, topq, dashq] = val;
+ props.onSubmit(name, topq, dashq);
+ });
+ }
+ return sbmt;
+ };
+ return
+
+
+ setQueryName(value)
+ }
+ />
+ setTopQuery(value)
+ }
+ label='Pin Study Query'
+ />
+
+ setDashboardQuery(value)
+ }
+ />
+
+
+ ;
}
export default AdminQueryModal;
diff --git a/modules/dataquery/jsx/welcome.namequerymodal.tsx b/modules/dataquery/jsx/welcome.namequerymodal.tsx
index 78c28a4c618..138ee981459 100644
--- a/modules/dataquery/jsx/welcome.namequerymodal.tsx
+++ b/modules/dataquery/jsx/welcome.namequerymodal.tsx
@@ -19,47 +19,47 @@ function NameQueryModal(props: {
closeModal: () => void,
onSubmit: (name: string) => void,
}) {
- const [queryName, setQueryName] = useState(props.defaultName || '');
- /**
- * Convert the onSubmit callback function to a promise expected by Modal
- *
- * @returns {Promise} - The submit promise
- */
- const submitPromise = (): Promise => {
- const sbmt = new Promise((resolve, reject) => {
- if (queryName == '') {
- swal.fire({
- type: 'error',
- text: 'Must provide a query name.',
- });
- reject();
- return;
- }
- resolve(queryName);
+ const [queryName, setQueryName] = useState(props.defaultName || '');
+ /**
+ * Convert the onSubmit callback function to a promise expected by Modal
+ *
+ * @returns {Promise} - The submit promise
+ */
+ const submitPromise = (): Promise => {
+ const sbmt = new Promise((resolve, reject) => {
+ if (queryName == '') {
+ swal.fire({
+ type: 'error',
+ text: 'Must provide a query name.',
});
- if (props.onSubmit) {
- return sbmt.then(props.onSubmit);
- }
- return sbmt;
- };
- return
-
-
- setQueryName(value)
- }
- />
-
-
- ;
+ reject();
+ return;
+ }
+ resolve(queryName);
+ });
+ if (props.onSubmit) {
+ return sbmt.then(props.onSubmit);
+ }
+ return sbmt;
+ };
+ return
+
+
+ setQueryName(value)
+ }
+ />
+
+
+ ;
}
export default NameQueryModal;
diff --git a/modules/dataquery/jsx/welcome.tsx b/modules/dataquery/jsx/welcome.tsx
index bcd70584916..40140048675 100644
--- a/modules/dataquery/jsx/welcome.tsx
+++ b/modules/dataquery/jsx/welcome.tsx
@@ -52,119 +52,119 @@ function Welcome(props: {
mapCategoryName: (module: string, category: string) => string,
fulldictionary:FullDictionary,
}) {
- const panels: {
+ const panels: {
title: string,
content: React.ReactElement,
alwaysOpen: boolean,
defaultOpen: boolean,
id: string,
}[] = [];
- if (props.topQueries.length > 0) {
- panels.push({
- title: 'Study Queries',
- content: (
-
-
-
- ),
- alwaysOpen: false,
- defaultOpen: true,
- id: 'p1',
- });
- }
+ if (props.topQueries.length > 0) {
panels.push({
- title: 'Instructions',
- content: 0}
- onContinue={props.onContinue}
- />,
- alwaysOpen: false,
- defaultOpen: true,
- id: 'p2',
+ title: 'Study Queries',
+ content: (
+
+
+
+ ),
+ alwaysOpen: false,
+ defaultOpen: true,
+ id: 'p1',
});
+ }
+ panels.push({
+ title: 'Instructions',
+ content: 0}
+ onContinue={props.onContinue}
+ />,
+ alwaysOpen: false,
+ defaultOpen: true,
+ id: 'p2',
+ });
+ panels.push({
+ title: 'Recent Queries',
+ content: (
+
+
+
+ ),
+ alwaysOpen: false,
+ defaultOpen: true,
+ id: 'p3',
+ });
+
+ if (props.sharedQueries.length > 0) {
panels.push({
- title: 'Recent Queries',
- content: (
-
-
-
- ),
- alwaysOpen: false,
- defaultOpen: true,
- id: 'p3',
- });
+ title: 'Shared Queries',
+ content: (
+
+
0) {
- panels.push({
- title: 'Shared Queries',
- content: (
-
-
-
- ),
- alwaysOpen: false,
- defaultOpen: true,
- id: 'p4',
- });
- }
+ queries={props.sharedQueries}
+ loadQuery={props.loadQuery}
+ defaultCollapsed={true}
- return (
-
-
+ getModuleFields={props.getModuleFields}
+ mapModuleName={props.mapModuleName}
+ mapCategoryName={props.mapCategoryName}
+ fulldictionary={props.fulldictionary}
+
+ queryAdmin={props.queryAdmin}
+ />
+
+ ),
+ alwaysOpen: false,
+ defaultOpen: true,
+ id: 'p4',
+ });
+ }
+
+ return (
+
+
Welcome to the Data Query Tool
-
-
-
- );
+
+
+
+ );
}
/**
@@ -207,402 +207,402 @@ function QueryList(props: {
mapCategoryName: (module: string, category: string) => string,
fulldictionary:FullDictionary,
}) {
- const [nameModalID, setNameModalID] = useState(null);
- const [adminModalID, setAdminModalID] = useState(null);
- const [queryName, setQueryName] = useState(null);
- const [defaultModalQueryName, setDefaultModalQueryName]
+ const [nameModalID, setNameModalID] = useState(null);
+ const [adminModalID, setAdminModalID] = useState(null);
+ const [queryName, setQueryName] = useState(null);
+ const [defaultModalQueryName, setDefaultModalQueryName]
= useState('');
- const [onlyStarred, setOnlyStarred] = useState(false);
- const [onlyShared, setOnlyShared] = useState(false);
- const [onlyNamed, setOnlyNamed] = useState(false);
- const [noDuplicates, setNoDuplicates] = useState(false);
- const [queryFilter, setQueryFilter] = useState('');
- const [fullQuery, setFullQuery]
+ const [onlyStarred, setOnlyStarred] = useState(false);
+ const [onlyShared, setOnlyShared] = useState(false);
+ const [onlyNamed, setOnlyNamed] = useState(false);
+ const [noDuplicates, setNoDuplicates] = useState(false);
+ const [queryFilter, setQueryFilter] = useState('');
+ const [fullQuery, setFullQuery]
= useState(!props.defaultCollapsed);
- const [unpinAdminQuery, setUnpinAdminQuery] = useState(null);
- const [adminPinAction, setAdminPinAction]
+ const [unpinAdminQuery, setUnpinAdminQuery] = useState(null);
+ const [adminPinAction, setAdminPinAction]
= useState<'top'|'dashboard'|'top,dashboard'>('top');
- useEffect(() => {
- const modules = new Set