diff --git a/Automation/src/test/java/Constants.java b/Automation/src/test/java/Constants.java index efd77bf67facffb38475024187eb57a86cde2e59..9241623fffafecc3a4ef334d703ba8bfa53853d1 100644 --- a/Automation/src/test/java/Constants.java +++ b/Automation/src/test/java/Constants.java @@ -1,11 +1,31 @@ +import org.apache.commons.lang3.RandomStringUtils; + public class Constants { - public static final String URL = "http://ec2co-ecsel-m3gy9h288kzs-2054883442.eu-west-1.elb.amazonaws.com/"; + public static final String URL = "http://qa.review.hindawi.com"; + public static final String jurnalID = "4b0eb00f-37cb-4fad-bd0d-7ba8ee600123"; + public static final String customManuscriptID = RandomStringUtils.randomNumeric(7); public static final String email = "adrian.onofrei+"; - public static final String firstname = "Test FN"; - public static final String lastname = "test LN"; + public static final String firstname = "Test Author"; + public static final String lastname = "Test Author"; public static final String affiliation = "TS affiliation"; - public static final String password = "Testing123"; + public static final String password = "password"; public static final String adminEmail = "admin"; public static final String adminPass = "password"; + public static final String randomStr = RandomStringUtils.randomAlphabetic(4); + public static final String firstnameHE = "Test HE"; + public static final String lastnameHE = "Test HE"; + public static final String passwordHE = "Testing123"; + public static final String emailHE = "adrian.onofrei+HE"; + public static final String affiliationHE = "TS HE affiliation"; + public static final String manusName = RandomStringUtils.randomAlphabetic(16); + public static final String manusAbstract = RandomStringUtils.randomAlphanumeric(50); + public static final String fileManuscript = "/Users/adionofrei/Documents/Manuscript.pdf"; + public static final String fileSupplementary = "/Users/adionofrei/Documents/Manuscript.pdf"; + public static final String fileCoverLetter = "/Users/adionofrei/Documents/Manuscript.pdf"; + + + public static String uidAuth = null; + public static String uidHE = null; + } diff --git a/Automation/src/test/java/CreateAccounts.java b/Automation/src/test/java/CreateAccounts.java new file mode 100644 index 0000000000000000000000000000000000000000..a8b5fca1fdd64bc0008eb314627e10ee0c6dcb76 --- /dev/null +++ b/Automation/src/test/java/CreateAccounts.java @@ -0,0 +1,179 @@ +import io.restassured.path.json.JsonPath; +import org.junit.*; +import org.openqa.selenium.*; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import static org.junit.Assert.*; +import io.restassured.RestAssured; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import org.json.simple.JSONObject; +import java.net.URL; + + +public class CreateAccounts { + + private static WebDriver driver = null; + private static WebDriverWait wait = null; + public String URL = Constants.URL; + + @BeforeClass + public static void setUpClass() throws Exception { + } + + @AfterClass + public static void tearDownClass() throws Exception { + } + + @Before + public void setUp() throws Exception { + //driver = new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), DesiredCapabilities.firefox()); + driver = new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), DesiredCapabilities.chrome()); + //driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS); + wait = new WebDriverWait(driver, 10); + String window = driver.getWindowHandle(); + ((JavascriptExecutor) driver).executeScript("alert('Test')"); + driver.switchTo().alert().accept(); + driver.switchTo().window(window); + Utils.disableWarning(); + } + + + @After + public void tearDown() { + driver.quit(); + } + + @Test + public void createNewAuthor() throws Exception { + + Utils.createAuthor(driver,wait, URL, Constants.randomStr, Constants.firstname, Constants.lastname, Constants.affiliation, Constants.email + Constants.randomStr + "@thinslices.com",Constants.password); + + + try { + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("button[data-test='new-manuscript']"))); + } catch (NoSuchElementException e) { + System.out.println(e.toString()); + } + + WebElement element = driver.findElement(By.xpath("//*[@id=\"root\"]/div/div/div[1]/div[1]/div[2]/div/span[2]")); + String username = element.getText(); + + assertEquals(Constants.firstname+Constants.randomStr, username); + + RestAssured.baseURI = URL; + + //Get user token after login with new account + + RequestSpecification request = RestAssured.given().header("Content-Type", "application/json"); + + JSONObject requestParams = new JSONObject(); + requestParams.put("username", Constants.email + Constants.randomStr + "@thinslices.com"); + requestParams.put("password", Constants.password); + request.body(requestParams.toJSONString()); + Response response = request.post("api/users/authenticate"); + + JsonPath jsonPathEvaluator = response.body().jsonPath(); + String token = jsonPathEvaluator.get("token"); + + //Get user ID and confirmationToken + + RequestSpecification requestUid = RestAssured.given().header("Authorization", "Bearer " + token); + Response responseUid = requestUid.get("api/users/authenticate"); + JsonPath jsonPathEvaluatorUid = responseUid.body().jsonPath(); + + String confirmationToken = jsonPathEvaluatorUid.get("confirmationToken"); + + Constants.uidAuth = jsonPathEvaluatorUid.get("id"); + + //Activate user + + driver.get(URL + "confirm-signup?userId=" + Constants.uidAuth + "&confirmationToken=" + confirmationToken); + + try { + wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath(".//button[contains(text(),'" + "Go to Dashboard" + "')]"))); + } catch (NoSuchElementException e) { + System.out.println(e.toString()); + } + + assertEquals("Your account has been successfully confirmed. Welcome to Hindawi!", driver.findElement(By.xpath("//*[@id=\"root\"]/div/div/div[2]/div/div")).getText()); + } + + @Test + public void createNewHE() throws Exception { + + Utils.createAuthor(driver,wait, URL, Constants.randomStr, Constants.firstnameHE, Constants.lastnameHE, Constants.affiliationHE, Constants.emailHE + Constants.randomStr + "@thinslices.com",Constants.passwordHE); + + + try { + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("button[data-test='new-manuscript']"))); + } catch (NoSuchElementException e) { + System.out.println(e.toString()); + } + + WebElement element = driver.findElement(By.xpath("//*[@id=\"root\"]/div/div/div[1]/div[1]/div[2]/div/span[2]")); + String username = element.getText(); + + assertEquals(Constants.firstnameHE + Constants.randomStr, username); + + RestAssured.baseURI = URL; + + //Get user token after login with new account + + RequestSpecification request = RestAssured.given().header("Content-Type", "application/json"); + + JSONObject requestParams = new JSONObject(); + requestParams.put("username", Constants.emailHE + Constants.randomStr + "@thinslices.com"); + requestParams.put("password", Constants.passwordHE); + request.body(requestParams.toJSONString()); + Response response = request.post("api/users/authenticate"); + + JsonPath jsonPathEvaluator = response.body().jsonPath(); + String token = jsonPathEvaluator.get("token"); + + //Get user ID and confirmationToken + + RequestSpecification requestUid = RestAssured.given().header("Authorization", "Bearer " + token); + Response responseUid = requestUid.get("api/users/authenticate"); + JsonPath jsonPathEvaluatorUid = responseUid.body().jsonPath(); + + String confirmationToken = jsonPathEvaluatorUid.get("confirmationToken"); + Constants.uidHE = jsonPathEvaluatorUid.get("id"); + + //Activate user + + driver.get(URL + "confirm-signup?userId=" + Constants.uidHE + "&confirmationToken=" + confirmationToken); + + try { + wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath(".//button[contains(text(),'" + "Go to Dashboard" + "')]"))); + } catch (NoSuchElementException e) { + System.out.println(e.toString()); + } + + assertEquals("Your account has been successfully confirmed. Welcome to Hindawi!", driver.findElement(By.xpath("//*[@id=\"root\"]/div/div/div[2]/div/div")).getText()); + + driver.get(URL+"login?next=/"); + Utils.validLogin( driver, wait, URL, Constants.adminEmail, Constants.adminPass); + driver.get(URL + "admin/users/edit/" + Constants.uidHE); + + try { + wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath(".//button[contains(text(),'" + "Save user" + "')]"))); + } catch (NoSuchElementException e) { + System.out.println(e.toString()); + } + + driver.findElement(By.cssSelector("input[name=handlingEditor] + span")).click(); + driver.findElement(By.cssSelector("button[type=submit]")).click(); + + try { + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("button[data-test='button-add-user']"))); + } catch (NoSuchElementException e) { + System.out.println(e.toString()); + } + + assertEquals(URL + "admin/users", driver.getCurrentUrl() ); + assertEquals(" Add User", driver.findElement(By.cssSelector("button[data-test='button-add-user']")).getText()); + } +} diff --git a/Automation/src/test/java/CreateAuthor.java b/Automation/src/test/java/CreateAuthor.java deleted file mode 100644 index 065b1e9293836ec8c850debbbb240876f80fe07f..0000000000000000000000000000000000000000 --- a/Automation/src/test/java/CreateAuthor.java +++ /dev/null @@ -1,104 +0,0 @@ -import io.restassured.path.json.JsonPath; -import org.apache.commons.lang3.RandomStringUtils; -import org.junit.*; -import org.openqa.selenium.*; -import org.openqa.selenium.remote.DesiredCapabilities; -import org.openqa.selenium.remote.RemoteWebDriver; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; -import static org.junit.Assert.*; -import io.restassured.RestAssured; -import io.restassured.response.Response; -import io.restassured.specification.RequestSpecification; -import org.json.simple.JSONObject; -import java.net.URL; - - -public class CreateAuthor { - - private static WebDriver driver = null; - private static WebDriverWait wait = null; - public String URL = Constants.URL; - - @BeforeClass - public static void setUpClass() throws Exception { - } - - @AfterClass - public static void tearDownClass() throws Exception { - } - - @Before - public void setUp() throws Exception { - //driver = new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), DesiredCapabilities.firefox()); - driver = new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), DesiredCapabilities.chrome()); - //driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS); - wait = new WebDriverWait(driver, 10); - String window = driver.getWindowHandle(); - ((JavascriptExecutor) driver).executeScript("alert('Test')"); - driver.switchTo().alert().accept(); - driver.switchTo().window(window); - Utils.disableWarning(); - } - - - @After - public void tearDown() { - driver.quit(); - } - - @Test - public void createNewAuthor() throws Exception { - - String s = RandomStringUtils.randomAlphabetic(8); - Utils.createAuthor(driver,wait, URL, s, Constants.firstname, Constants.lastname, Constants.affiliation, Constants.email+s+"@thinslices.com",Constants.password); - - - try { - wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("button[data-test='new-manuscript']"))); - } catch (NoSuchElementException e) { - System.out.println(e.toString()); - } - - WebElement element = driver.findElement(By.xpath("//*[@id=\"root\"]/div/div/div[1]/div[1]/div[2]/div/span[2]")); - String username = element.getText(); - - assertEquals(Constants.firstname+s, username); - - RestAssured.baseURI = URL; - - //Get user token after login with new account - - RequestSpecification request = RestAssured.given().header("Content-Type", "application/json"); - - JSONObject requestParams = new JSONObject(); - requestParams.put("username", Constants.email+s+"@thinslices.com"); - requestParams.put("password", Constants.password); - request.body(requestParams.toJSONString()); - Response response = request.post("api/users/authenticate"); - - JsonPath jsonPathEvaluator = response.body().jsonPath(); - String token = jsonPathEvaluator.get("token"); - - //Get user ID and confimationToken - - RequestSpecification requestUid = RestAssured.given().header("Authorization", "Bearer "+token); - Response responseUid = requestUid.get("api/users/authenticate"); - JsonPath jsonPathEvaluatorUid = responseUid.body().jsonPath(); - - String confirmationToken = jsonPathEvaluatorUid.get("confirmationToken"); - String uid = jsonPathEvaluatorUid.get("id"); - - //Activate user - - driver.get(URL+"confirm-signup?userId="+uid+"&confirmationToken="+confirmationToken); - - try { - wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath(".//button[contains(text(),'"+"Go to Dashboard"+"')]"))); - } catch (NoSuchElementException e) { - System.out.println(e.toString()); - } - - assertEquals("Your account has been successfully confirmed. Welcome to Hindawi!", driver.findElement(By.xpath("//*[@id=\"root\"]/div/div/div[2]/div/div")).getText()); - } -} diff --git a/Automation/src/test/java/ManuscriptFlow.java b/Automation/src/test/java/ManuscriptFlow.java new file mode 100644 index 0000000000000000000000000000000000000000..6e211a795c50709c96394d0be1e990f0b69f1bc0 --- /dev/null +++ b/Automation/src/test/java/ManuscriptFlow.java @@ -0,0 +1,250 @@ +import junit.runner.BaseTestRunner; +import org.junit.*; +import org.openqa.selenium.*; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import java.net.URL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class ManuscriptFlow { + private static WebDriver driver = null; + private static WebDriverWait wait = null; + public String URL = Constants.URL; + public static String manuscriptId; + + @BeforeClass + public static void setUpClass() throws Exception { + } + + @AfterClass + public static void tearDownClass() throws Exception { + } + + @Before + public void setUp() throws Exception { + //driver = new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), DesiredCapabilities.firefox()); + driver = new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), DesiredCapabilities.chrome()); + //driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS); + wait = new WebDriverWait(driver, 20); + String window = driver.getWindowHandle(); + ((JavascriptExecutor) driver).executeScript("alert('Test')"); + driver.switchTo().alert().accept(); + driver.switchTo().window(window); + } + + @After + public void tearDown() throws Exception { + driver.quit(); + } + + @Test + public void createNewManuscript() throws Exception { + /* Add constants.random after testing this function separably */ + Utils.validLogin( driver, wait, URL, Constants.email + /*Constants.randomStr*/ "auth1" + "@thinslices.com", Constants.password); + + WebElement validLogin = null; + + try { + validLogin = wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("button[data-test='new-manuscript']"))); + } catch (NoSuchElementException e) { + + } + + assertNotNull(validLogin); + + driver.findElement(By.cssSelector("button[data-test='new-manuscript']")).click(); + + try { + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("button[data-test='submission-next']"))); + } catch (NoSuchElementException e) { + + } + + assertNotNull(driver.findElement(By.cssSelector("button[data-test='submission-next']"))); + + // First step submission flow + driver.findElement(By.cssSelector("[data-test-id='agree-checkbox']")).click(); + driver.findElement(By.cssSelector("button[data-test='submission-next']")).click(); + + // Second step submission flow + try { + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".wizard-step >[data-test-id='row']:nth-of-type(2) span"))); + } catch (NoSuchElementException e) { + + } + + assertEquals("Please provide the details of all the authors of this manuscript, in the order that they appear on the manuscript. Your details have been prefilled as the submitting author.",driver.findElement(By.cssSelector(".wizard-step >[data-test-id='row']:nth-of-type(2) span")).getText()); + assertEquals("MANUSCRIPT TITLE\n*", driver.findElement(By.cssSelector("[data-test-id='submission-title']")).getText()); + + driver.findElement(By.cssSelector("input[name='metadata.title']")).sendKeys(Constants.manusName); + driver.findElement(By.cssSelector("[data-test-id='submission-type']")).click(); + try { + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("[data-test-id='submission-type'] div[role=\"option\"]"))); + } catch (NoSuchElementException e) { + + } + driver.findElement(By.cssSelector("[data-test-id='submission-type'] div[role=\"option\"]:nth-child(1)")).click(); + driver.findElement(By.cssSelector("[data-test-id='submission-abstract'] textarea")).sendKeys(Constants.manusAbstract); + WebElement element = driver.findElement(By.cssSelector("[name='conflicts.hasConflicts'][value='no']")); + + Utils.scrollToElement(driver, element); + element.click(); + + try { + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("[name='conflicts.hasDataAvailability'][value='yes']"))); + } catch (NoSuchElementException e) { + + } + + driver.findElement(By.cssSelector("[name='conflicts.hasDataAvailability'][value='yes']")).click(); + WebElement element1 = driver.findElement(By.cssSelector("[name='conflicts.hasFunding'][value='yes']")); + + Utils.scrollToElement(driver, element1); + element1.click(); + + try { + wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("button[data-test='submission-next']"))); + } catch (NoSuchElementException e) { + + } + + WebElement element2 = driver.findElement(By.cssSelector("button[data-test='submission-next']")); + + Utils.scrollToElement(driver, element2); + element2.click(); + + // Third step submission flow + + try { + wait.until(ExpectedConditions.visibilityOf(driver.findElement(By.cssSelector(".wizard-step [level='2']")))); + } catch (NoSuchElementException e) { + + } + WebElement pageTitle = driver.findElement(By.cssSelector(".wizard-step [level='2']")); + assertEquals("3. Manuscript Files Upload", pageTitle.getText()); + + try { + wait.until(ExpectedConditions.visibilityOf(pageTitle)); + } catch (NoSuchElementException e) { + + } + + String mURL = driver.getCurrentUrl(); + manuscriptId = Utils.mID(mURL); + + driver.findElement(By.cssSelector(/*"[data-test='upload-manuscripts']*/"input[type=\"file\"]")).sendKeys(Constants.fileManuscript); +// driver.findElement(By.cssSelector("[data-test='upload-supplementary'] input[type=\"file\"]")).sendKeys(Constants.fileSupplementary); +// driver.findElement(By.cssSelector("[data-test='upload-coverLetter'] input[type=\"file\"]")).sendKeys(Constants.fileCoverLetter); + + + try { + wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("button[data-test='submission-next']"))); + } catch (NoSuchElementException e) { + + } + + // Confirmation modal + WebElement element3 = driver.findElement(By.cssSelector("button[data-test='submission-next']")); + + try { + wait.until(ExpectedConditions.visibilityOf(element3)); + } catch (NoSuchElementException e) { + + } + + Utils.scrollToElement(driver, element3); + element3.click(); + + try { + wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//button[contains(.,'AGREE & SUBMIT')]"))); + } catch (NoSuchElementException e) { + + } + + driver.findElement(By.xpath("//button[contains(.,'AGREE & SUBMIT')]")).click(); + + try { + wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//button[contains(.,'GO TO DASHBOARD')]"))); + } catch (NoSuchElementException e) { + + } + + driver.findElement(By.xpath("//button[contains(.,'GO TO DASHBOARD')]")).click(); + + try { + wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("[data-test-id='fragment-" + manuscriptId + "']"))); + } catch (NoSuchElementException e) { + + } + + // Check if manuscript is displayed on dashboard + assertNotNull("fragment-" + manuscriptId); + + driver.findElement(By.cssSelector("[data-test-id='fragment-" + manuscriptId + "']")).click(); + + try { + wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("[data-test-id='fragment-status']"))); + } catch (NoSuchElementException e) { + + } + + // Check if manuscript status is submitted for author + assertEquals("Submitted", driver.findElement(By.cssSelector("[data-test-id='fragment-status']")).getText()); + + } + + @Test + public void eqsApproveManuscript() throws Exception{ + String manuscriptDT = "[data-test-id='fragment-" + manuscriptId + "']"; + Utils.validLogin(driver, wait, URL, Constants.adminEmail, Constants.password); + + try { + wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("[data-test-id=\"row\"] button[type=\"button\"]"))); + } catch (NoSuchElementException e) { + + } + + driver.findElement(By.cssSelector("[data-test-id=\"row\"] button[type=\"button\"]")).click(); + driver.findElement(By.cssSelector("[data-test-id=\"row\"] div[role=\"option\"]:nth-child(1)")).click(); + + driver.findElement(By.cssSelector("[data-test-id=\"item\"]:nth-of-type(2)")).click(); + driver.findElement(By.cssSelector("[data-test-id=\"row\"] div[role=\"option\"]:nth-child(1)")).click(); + + driver.findElement(By.cssSelector(manuscriptDT)).click(); + + try { + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("[data-test-id='button-qa-manuscript-" + manuscriptId + "']"))); + } catch (NoSuchElementException e) { + + } + + assertEquals("QA", driver.findElement(By.cssSelector("[data-test-id='fragment-status']")).getText()); + + driver.findElement(By.cssSelector("[data-test-id='button-qa-manuscript-" + manuscriptId + "']")).click(); + + try { + wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//button[contains(.,'YES')]"))); + } catch (NoSuchElementException e) { + + } + + assertTrue(driver.getCurrentUrl().contains("eqs-decision")); + + driver.findElement(By.xpath("//button[contains(.,'YES')]")).click(); + + try { + wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("input[name='customId'"))); + } catch (NoSuchElementException e) { + + } + + driver.findElement(By.cssSelector("input[name='customId'")).sendKeys(Constants.customManuscriptID); + driver.findElement(By.xpath("//button[contains(.,'OK')]")).click(); + } +} diff --git a/Automation/src/test/java/Utils.java b/Automation/src/test/java/Utils.java index cc31dadae8f24c486d4c45c9d146d3543956b3f6..1a7c06793c5566002c48abbc8fcd2f611134c0f3 100644 --- a/Automation/src/test/java/Utils.java +++ b/Automation/src/test/java/Utils.java @@ -4,6 +4,7 @@ import org.openqa.selenium.support.ui.WebDriverWait; import sun.misc.Unsafe; import java.lang.reflect.Field; +import java.util.regex.*; public class Utils { @@ -85,5 +86,36 @@ public class Utils { driver.findElement(By.xpath(".//button[contains(text(),'"+"CONFIRM"+"')]")).click(); } + + public static void scrollToElement(WebDriver driver, WebElement el) { + if (driver instanceof JavascriptExecutor) { + ((JavascriptExecutor) driver) + .executeScript("arguments[0].scrollIntoView(true);", el); + } + } + + public static String mID(String url) + { + String txt=url; + + String fragmentID = txt.split("/versions/")[1].split("/")[0]; + +// String re1=".*?"; // Non-greedy match on filler +// String re2="[A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12}"; // Uninteresting: guid +// String re3=".*?"; // Non-greedy match on filler +// String re4="([A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12})"; // SQL GUID 1 +// +// Pattern p = Pattern.compile(re1+re2+re3+re4,Pattern.CASE_INSENSITIVE | Pattern.DOTALL); +// Matcher m = p.matcher(txt); +// if (m.find()) +// { +// String guid1=m.group(1); +// System.out.print(guid1+"\n"); +// +// } +// String guid=m.group(1); + + return fragmentID; + } } diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js index c90d2fec5616ffce6cfab03a0db65592695c9511..fbfcf2db03ad8928dbf383f8b80c010fb256d2a0 100644 --- a/packages/component-faraday-selectors/src/index.js +++ b/packages/component-faraday-selectors/src/index.js @@ -162,3 +162,27 @@ export const getAdminUsers = state => .map(s => s.user) .reverse() .value() + +export const pendingHEInvitation = (state, collectionId) => + chain(state) + .get('collections', []) + .find(c => c.id === collectionId) + .get('invitations', []) + .find( + i => + i.userId === get(state, 'currentUser.user.id', '') && + i.role === 'handlingEditor' && + !i.hasAnswer, + ) + +const hideCustomIdStatuses = ['draft', 'technicalChecks'] +export const newestFirstParseCustomId = items => + chain(items) + .orderBy(['created'], ['desc']) + .map(item => ({ + ...item, + customId: + !hideCustomIdStatuses.includes(get(item, 'status', 'draft')) && + item.customId, + })) + .value() diff --git a/packages/component-faraday-ui/src/ContextualBox.js b/packages/component-faraday-ui/src/ContextualBox.js index 32ff2966e40f3884d0069955ea65d619a1e8c689..016b6618f4e3bf09e7c92d4c7f246a6ecbfe7a70 100644 --- a/packages/component-faraday-ui/src/ContextualBox.js +++ b/packages/component-faraday-ui/src/ContextualBox.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { Fragment } from 'react' import { has } from 'lodash' import styled from 'styled-components' import { Icon, H3 } from '@pubsweet/ui' @@ -21,12 +21,12 @@ const CustomHeader = ({ onClick={toggle} transparent={transparent} > - <div> + <Fragment> <Icon secondary size={2}> {expanded ? 'minus' : 'plus'} </Icon> <H3>{label}</H3> - </div> + </Fragment> {typeof rightChildren === 'function' ? rightChildren({ ...props, expanded, transparent }) : rightChildren} diff --git a/packages/component-faraday-ui/src/ManuscriptCard.js b/packages/component-faraday-ui/src/ManuscriptCard.js index f06c5557a6e8a5d442d6b230df00cc92b76b9370..269c0401842819645308600decec57ae403e711b 100644 --- a/packages/component-faraday-ui/src/ManuscriptCard.js +++ b/packages/component-faraday-ui/src/ManuscriptCard.js @@ -52,18 +52,23 @@ const ManuscriptCard = ({ </Row> )} <Row alignItems="center" justify="flex-start" mb={1}> - <Text customId mr={1}>{`ID ${customId}`}</Text> + {customId && <Text customId mr={1}>{`ID ${customId}`}</Text>} {submitted && ( <DateParser humanizeThreshold={0} timestamp={submitted}> - {timestamp => <Text mr={3}>Submitted on {timestamp}</Text>} + {(timestamp, timeAgo) => ( + <Text + mr={3} + >{`Submitted on ${timestamp} (${timeAgo} ago)`}</Text> + )} </DateParser> )} <Text>{manuscriptType.label || type}</Text> - {journal && ( - <Text journal ml={1}> - {journal} - </Text> - )} + {journal && + (manuscriptType.label || type) && ( + <Text journal ml={1}> + {journal} + </Text> + )} </Row> <Row alignItems="center" justify="flex-start" mb={1}> <H4>Handling editor</H4> diff --git a/packages/component-faraday-ui/src/PersonInvitation.js b/packages/component-faraday-ui/src/PersonInvitation.js index 563573c38875e01e88d532f813c9f57cb92b522c..89676c26a54b867889d5f10c65ca33741fcc62ea 100644 --- a/packages/component-faraday-ui/src/PersonInvitation.js +++ b/packages/component-faraday-ui/src/PersonInvitation.js @@ -5,6 +5,7 @@ import { compose, withHandlers, defaultProps, setDisplayName } from 'recompose' import { Text, OpenModal, IconButton, marginHelper } from './' const PersonInvitation = ({ + withName, hasAnswer, isFetching, person: { name }, @@ -13,7 +14,7 @@ const PersonInvitation = ({ ...rest }) => ( <Root {...rest}> - <Text>{name}</Text> + {withName && <Text>{name}</Text>} {!hasAnswer && name !== 'Unassigned' && ( <Fragment> diff --git a/packages/component-faraday-ui/src/RemoteOpener.js b/packages/component-faraday-ui/src/RemoteOpener.js index 3cec7f5f23bf907061e008e6e719bee94d85b900..44bcb785bdb7be2d1db3e3a9bab8f951c4e002c2 100644 --- a/packages/component-faraday-ui/src/RemoteOpener.js +++ b/packages/component-faraday-ui/src/RemoteOpener.js @@ -1,7 +1,7 @@ import { withStateHandlers } from 'recompose' const RemoteOpener = ({ expanded, toggle, children }) => - children(expanded, toggle) + children({ expanded, toggle }) export default withStateHandlers( ({ startExpanded }) => ({ expanded: startExpanded }), diff --git a/packages/component-faraday-ui/src/ReviewerBreakdown.js b/packages/component-faraday-ui/src/ReviewerBreakdown.js index 6c64361e64408acca148bbf44e58eedf5d4478b1..7c181f90968c9751a32cc48458241fda72bbd246 100644 --- a/packages/component-faraday-ui/src/ReviewerBreakdown.js +++ b/packages/component-faraday-ui/src/ReviewerBreakdown.js @@ -1,11 +1,8 @@ -import React, { Fragment } from 'react' +import React from 'react' import { get } from 'lodash' -import { H4 } from '@pubsweet/ui' -import styled from 'styled-components' -import { th } from '@pubsweet/ui-toolkit' import { compose, withHandlers, withProps } from 'recompose' -import { Text } from './' +import { Text, Row } from './' const ReviewerBreakdown = ({ getReportBreakdown }) => getReportBreakdown() @@ -26,11 +23,7 @@ export default compose( recommendations: get(fragment, 'recommendations', []), })), withHandlers({ - getReportBreakdown: ({ - invitations, - recommendations, - label = '', - }) => () => { + getReportBreakdown: ({ invitations, recommendations, ...rest }) => () => { const reviewerInvitations = invitations.filter(roleFilter('reviewer')) const invitationsWithRecommendations = reviewerInvitations.map(r => ({ ...r, @@ -41,27 +34,31 @@ export default compose( declined: 0, submitted: 0, }) - return ( - <Fragment> - {!!label && <Label>{label}</Label>} - {reviewerInvitations.length ? ( - <Text> - {`${reviewerInvitations.length} invited, ${ - report.accepted - } agreed, ${report.declined} declined, ${ - report.submitted - } submitted`} - </Text> - ) : ( - <Text> {`${reviewerInvitations.length} invited`}</Text> - )} - </Fragment> + return reviewerInvitations.length ? ( + <Row justify="flex-end" {...rest}> + <Text customId mr={1 / 2}> + {reviewerInvitations.length} + </Text> + <Text mr={1 / 2}> invited,</Text> + + <Text customId mr={1 / 2}> + {report.accepted} + </Text> + <Text mr={1 / 2}> agreed,</Text> + + <Text customId mr={1 / 2}> + {report.declined} + </Text> + <Text mr={1 / 2}> declined,</Text> + + <Text customId mr={1 / 2}> + {report.submitted} + </Text> + <Text mr={1 / 2}> submitted</Text> + </Row> + ) : ( + <Text> {`${reviewerInvitations.length} invited`}</Text> ) }, }), )(ReviewerBreakdown) - -const Label = styled(H4)` - display: inline-block; - margin-right: ${th('gridUnit')}; -` diff --git a/packages/component-faraday-ui/src/ReviewerBreakdown.md b/packages/component-faraday-ui/src/ReviewerBreakdown.md index d14eaaf2ce307b2ebb091b25cd220ac2084906ba..64df75b1fe57fee03dcc52137520dd676415a0d5 100644 --- a/packages/component-faraday-ui/src/ReviewerBreakdown.md +++ b/packages/component-faraday-ui/src/ReviewerBreakdown.md @@ -85,8 +85,3 @@ Without invitations ```js <ReviewerBreakdown fragment={{}} /> ``` - -With label -```js -<ReviewerBreakdown fragment={{}} label='Reviewer Reports' /> -``` diff --git a/packages/component-faraday-ui/src/ReviewerReport.js b/packages/component-faraday-ui/src/ReviewerReport.js new file mode 100644 index 0000000000000000000000000000000000000000..401f3818edffd6781810f1e5f2e7c3456d984f32 --- /dev/null +++ b/packages/component-faraday-ui/src/ReviewerReport.js @@ -0,0 +1,69 @@ +import React from 'react' +import styled from 'styled-components' +import { th } from '@pubsweet/ui-toolkit' +import { DateParser } from '@pubsweet/ui' + +import { Label, Item, FileItem, Row, Text } from './' + +const ReviewerReport = ({ + report: { + report, + files = [], + submittedOn, + recommendation, + confidentialNote, + reviewer: { fullName, reviewerNumber }, + }, +}) => ( + <Root> + <Row justify="space-between" mb={2}> + <Item vertical> + <Label mb={1 / 2}>Recommendation</Label> + <Text>{recommendation}</Text> + </Item> + + <Item justify="flex-end"> + <Text>{fullName}</Text> + <Text customId ml={1} mr={1}> + {`Reviewer ${reviewerNumber}`} + </Text> + <DateParser timestamp={submittedOn}> + {date => <Text>{date}</Text>} + </DateParser> + </Item> + </Row> + + <Row mb={2}> + <Item vertical> + <Label mb={1 / 2}>Report</Label> + <Text>{report}</Text> + </Item> + </Row> + + <Label mb={1 / 2}>Files</Label> + <Row justify="flex-start" mb={2}> + {files.map(file => ( + <Item flex={0} key={file.id} mr={1}> + <FileItem item={file} /> + </Item> + ))} + </Row> + + <Row mb={2}> + <Item vertical> + <Label mb={1 / 2}>Confidential note for the Editorial Team</Label> + <Text>{confidentialNote}</Text> + </Item> + </Row> + </Root> +) + +export default ReviewerReport + +// #region styles +const Root = styled.div` + box-shadow: ${th('boxShadow')}; + padding: calc(${th('gridUnit')} * 2); + margin: ${th('gridUnit')}; +` +// #endregion diff --git a/packages/component-faraday-ui/src/ReviewerReport.md b/packages/component-faraday-ui/src/ReviewerReport.md new file mode 100644 index 0000000000000000000000000000000000000000..e16f2bc1a28bc427029e6c9388d8f8288f031112 --- /dev/null +++ b/packages/component-faraday-ui/src/ReviewerReport.md @@ -0,0 +1,26 @@ +Reviewer report. + +```js +<ReviewerReport + report={{ + submittedOn: Date.now(), + recommendation: 'Reject', + report: `Of all of the celestial bodies that capture our attention and + fascination as astronomers, none has a greater influence on life on + planet Earth than it’s own satellite, the moon. When you think about + it, we regard the moon with such powerful significance that unlike the + moons of other planets which we give names, we only refer to our one + and only orbiting orb as THE moon. It is not a moon. To us, it is the + one and only moon.`, + reviewer: { + fullName: 'Kenny Hudson', + reviewerNumber: 1, + }, + confidentialNote: `First 10 pages feel very familiar, you should check for plagarism.`, + files: [ + { id: 'file1', name: 'file1.pdf', size: 12356 }, + { id: 'file2', name: 'file2.pdf', size: 76421 }, + ], + }} +/> +``` diff --git a/packages/component-faraday-ui/src/ReviewersTable.js b/packages/component-faraday-ui/src/ReviewersTable.js new file mode 100644 index 0000000000000000000000000000000000000000..e0bdabb2722aa52c0e0fcf9bd5382c43bcba6bb9 --- /dev/null +++ b/packages/component-faraday-ui/src/ReviewersTable.js @@ -0,0 +1,207 @@ +import React from 'react' +import styled from 'styled-components' +import { th } from '@pubsweet/ui-toolkit' +import { DateParser } from '@pubsweet/ui' + +import { Label, PersonInvitation, Text } from '../' + +const invitation = { + id: 'b4305ab6-84e6-48a3-9eb9-fbe0ec80c694', + role: 'handlingEditor', + type: 'invitation', + reason: 'because', + userId: 'cb7e3e26-6a09-4b79-a6ff-4d1235ee2381', + hasAnswer: false, + invitedOn: 713919119, + isAccepted: false, + respondedOn: 1533714034932, + person: { + id: 'cb7e3e26-6a09-4b79-a6ff-4d1235ee2381', + name: 'Toto Schilacci', + }, +} + +const reviewers = [ + { + id: 1, + fullName: 'Gica Hagi', + invitedOn: Date.now(), + respondedOn: Date.now(), + submittedOn: Date.now() - 3000000000, + }, + { + id: 2, + fullName: 'Cosmin Contra', + invitedOn: Date.now(), + respondedOn: Date.now(), + submittedOn: Date.now() - 3000000000, + }, + { + id: 11, + fullName: 'Gica Hagi', + invitedOn: Date.now(), + respondedOn: Date.now(), + submittedOn: Date.now() - 3000000000, + }, + { + id: 21, + fullName: 'Cosmin Contra', + invitedOn: Date.now(), + respondedOn: Date.now(), + submittedOn: Date.now() - 3000000000, + }, + { + id: 12, + fullName: 'Gica Hagi', + invitedOn: Date.now(), + respondedOn: Date.now(), + submittedOn: Date.now() - 3000000000, + }, + { + id: 22, + fullName: 'Cosmin Contra', + invitedOn: Date.now(), + respondedOn: Date.now(), + submittedOn: Date.now() - 3000000000, + }, + { + id: 13, + fullName: 'Gica Hagi', + invitedOn: Date.now(), + respondedOn: Date.now(), + submittedOn: Date.now() - 3000000000, + }, + { + id: 23, + fullName: 'Cosmin Contra', + invitedOn: Date.now(), + respondedOn: Date.now(), + submittedOn: Date.now() - 3000000000, + }, + { + id: 113, + fullName: 'Gica Hagi', + invitedOn: Date.now(), + respondedOn: Date.now(), + submittedOn: Date.now() - 3000000000, + }, + { + id: 213, + fullName: 'Cosmin Contra', + invitedOn: Date.now(), + respondedOn: Date.now(), + submittedOn: Date.now() - 3000000000, + }, + { + id: 123, + fullName: 'Gica Hagi', + invitedOn: Date.now(), + respondedOn: Date.now(), + submittedOn: Date.now() - 3000000000, + }, + { + id: 223, + fullName: 'Cosmin Contra', + invitedOn: Date.now(), + respondedOn: Date.now(), + submittedOn: Date.now() - 3000000000, + }, +] + +const ReviewersTable = () => ( + <Table> + <thead> + <tr> + <th> + <Label>Full Name</Label> + </th> + <th> + <Label>Invited on</Label> + </th> + <th> + <Label>Responded on</Label> + </th> + <th> + <Label>Submitted on</Label> + </th> + <th> </th> + </tr> + </thead> + <tbody> + {reviewers.map((r, index) => ( + <TableRow key={r.id}> + <td> + <Text>{r.fullName}</Text> + <Text customId ml={1}>{`Reviewer ${index + 1}`}</Text> + </td> + <td> + <DateParser timestamp={r.invitedOn}> + {timestamp => <Text>{timestamp}</Text>} + </DateParser> + </td> + <td> + <DateParser timestamp={r.respondedOn}> + {timestamp => <Text>{timestamp}</Text>} + </DateParser> + <Text ml={1} secondary> + ACCEPTED + </Text> + </td> + <td> + <DateParser timestamp={r.submittedOn}> + {timestamp => <Text>{timestamp}</Text>} + </DateParser> + </td> + <HiddenCell> + <PersonInvitation {...invitation} /> + </HiddenCell> + </TableRow> + ))} + </tbody> + </Table> +) + +export default ReviewersTable + +// #region styles +const Table = styled.table` + border-collapse: collapse; + padding: ${th('gridUnit')}; + + & thead { + border-bottom: 1px solid ${th('colorBorder')}; + } + + & th, + & td { + border: none; + padding-left: ${th('gridUnit')}; + text-align: start; + vertical-align: middle; + + height: calc(${th('gridUnit')} * 5); + min-width: calc(${th('gridUnit')} * 12); + } +` + +const HiddenCell = styled.td` + opacity: 0; +` + +const TableRow = styled.tr` + background-color: ${th('colorBackgroundHue2')}; + border-bottom: 1px solid ${th('colorBorder')}; + + & td:first-child { + min-width: calc(${th('gridUnit')} * 30); + } + + &:hover { + background-color: #eeeeee; + + ${HiddenCell} { + opacity: 1; + } + } +` +// #endregion diff --git a/packages/component-faraday-ui/src/ReviewersTable.md b/packages/component-faraday-ui/src/ReviewersTable.md new file mode 100644 index 0000000000000000000000000000000000000000..dbab1725a9cb9c5744460e1f95dcd814a378b7fa --- /dev/null +++ b/packages/component-faraday-ui/src/ReviewersTable.md @@ -0,0 +1,5 @@ +A list of reviewers. + +```js +<ReviewersTable /> +``` diff --git a/packages/component-faraday-ui/src/Tabs.js b/packages/component-faraday-ui/src/Tabs.js new file mode 100644 index 0000000000000000000000000000000000000000..76d0699c166690707ae38dc344f0cfa5f0833d44 --- /dev/null +++ b/packages/component-faraday-ui/src/Tabs.js @@ -0,0 +1,15 @@ +import { compose, withStateHandlers } from 'recompose' + +const Tabs = ({ items, selectedTab, changeTab, children }) => + children({ selectedTab, changeTab }) + +export default compose( + withStateHandlers( + { selectedTab: 0 }, + { + changeTab: () => selectedTab => ({ + selectedTab, + }), + }, + ), +)(Tabs) diff --git a/packages/component-faraday-ui/src/Tabs.md b/packages/component-faraday-ui/src/Tabs.md new file mode 100644 index 0000000000000000000000000000000000000000..a213cb4aec658c0d3f50dea84688c2f0ec7c07aa --- /dev/null +++ b/packages/component-faraday-ui/src/Tabs.md @@ -0,0 +1,22 @@ +A component to render tabs. + +```js +const tabItems = [ + { content: () => <div>Tab one content</div> }, + { content: () => <div>Tab two content</div> }, + { content: () => <div>Tab three content</div> }, +] +;<Tabs> + {({ selectedTab, changeTab }) => ( + <div> + <div> + <button onClick={() => changeTab(0)}>Tab 1</button> + <button onClick={() => changeTab(1)}>Tab 2</button> + <button onClick={() => changeTab(2)}>Tab 3</button> + </div> + + {tabItems[selectedTab].content()} + </div> + )} +</Tabs> +``` diff --git a/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js new file mode 100644 index 0000000000000000000000000000000000000000..cca9baae989cda6722630bc6a91e37984aa23b3b --- /dev/null +++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js @@ -0,0 +1,107 @@ +import React, { Fragment } from 'react' +import styled from 'styled-components' +import { th } from '@pubsweet/ui-toolkit' + +import { + Tag, + Tabs, + Label, + marginHelper, + ContextualBox, + ReviewersTable, + ReviewerReport, + ReviewerBreakdown, +} from '../' + +const report = { + submittedOn: Date.now(), + recommendation: 'Reject', + report: `Of all of the celestial bodies that capture our attention and + fascination as astronomers, none has a greater influence on life on + planet Earth than it’s own satellite, the moon. When you think about + it, we regard the moon with such powerful significance that unlike the + moons of other planets which we give names, we only refer to our one + and only orbiting orb as THE moon. It is not a moon. To us, it is the + one and only moon.`, + reviewer: { + fullName: 'Kenny Hudson', + reviewerNumber: 1, + }, + confidentialNote: `First 10 pages feel very familiar, you should check for plagarism.`, + files: [ + { id: 'file1', name: 'file1.pdf', size: 12356 }, + { id: 'file2', name: 'file2.pdf', size: 76421 }, + ], +} + +const ReviewerDetails = ({ fragment }) => ( + <ContextualBox + label="Reviewer details" + rightChildren={<ReviewerBreakdown fitContent fragment={fragment} mr={1} />} + startExpanded + > + <Tabs> + {({ selectedTab, changeTab }) => ( + <Fragment> + <TabsHeader> + <TabButton + ml={1} + mr={1} + onClick={() => changeTab(0)} + selected={selectedTab === 0} + > + <Label>Reviewer Details</Label> + </TabButton> + <TabButton + ml={1} + mr={1} + onClick={() => changeTab(1)} + selected={selectedTab === 1} + > + <Label>Reviewer Reports</Label> + <Tag ml={1}>12</Tag> + </TabButton> + </TabsHeader> + {selectedTab === 0 && <ReviewersTable />} + {selectedTab === 1 && ( + <Fragment> + <ReviewerReport report={report} /> + <ReviewerReport report={report} /> + <ReviewerReport report={report} /> + </Fragment> + )} + </Fragment> + )} + </Tabs> + </ContextualBox> +) + +export default ReviewerDetails + +// #region styles +const TabButton = styled.div` + align-items: center; + border-bottom: ${props => + props.selected + ? `4px solid ${props.theme.colorFurnitureHue}` + : '4px solid transparent'}; + box-sizing: border-box; + cursor: pointer; + display: flex; + justify-content: center; + height: calc(${th('gridUnit')} * 5); + + ${marginHelper}; +` + +const TabsHeader = styled.div` + align-items: center; + border-bottom: 1px solid ${th('colorFurniture')}; + box-sizing: border-box; + display: flex; + justify-content: flex-start; + + margin-bottom: ${th('gridUnit')}; + padding: 0 calc(${th('gridUnit')} * 3); +` +// #endregion diff --git a/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.md b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.md new file mode 100644 index 0000000000000000000000000000000000000000..49bb54a7d348719cc44c2b822edcaf016d163d5c --- /dev/null +++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.md @@ -0,0 +1,83 @@ +The ReviewerDetails contextual box. + +```js +const fragment = { + invitations: [ + { + id: 'a69c1273-4073-4529-9e65-5424ad8034ea', + role: 'reviewer', + type: 'invitation', + userId: '9c79c3bf-ccba-4540-aad8-ce4609325826', + hasAnswer: false, + invitedOn: 1534506511008, + isAccepted: false, + respondedOn: null, + }, + { + id: 'ca1c08bb-4da6-4cb4-972d-d16e3b66b884', + role: 'reviewer', + type: 'invitation', + userId: 'd3ab9a0b-d8e8-41e5-ab3b-72b13f84fba1', + hasAnswer: true, + invitedOn: 1534506542522, + isAccepted: true, + respondedOn: 1534506569565, + }, + ], + recommendations: [ + { + id: '87fb2c45-2685-47cc-9952-d58435ff8f3a', + userId: 'd3ab9a0b-d8e8-41e5-ab3b-72b13f84fba1', + comments: [ + { + files: [], + public: true, + content: 'This is a great manuscript', + }, + ], + createdOn: 1534506583815, + updatedOn: 1534506592527, + submittedOn: 1534506591684, + recommendation: 'publish', + recommendationType: 'review', + }, + { + id: '9853453f-1049-4369-8543-1b812930430d', + userId: 'ede6770d-8dbf-4bf9-bbe2-98facfd0a114', + comments: [ + { + public: true, + content: 'nice job', + }, + { + public: false, + content: 'please publish this', + }, + ], + createdOn: 1534506624583, + updatedOn: 1534506624583, + recommendation: 'publish', + recommendationType: 'editorRecommendation', + }, + { + id: '64c5b596-fbfc-485c-9068-f3a58306efd7', + userId: '5b53da0d-3f88-4e94-b8f9-7eae6a754168', + createdOn: 1534506644873, + updatedOn: 1534506644873, + recommendation: 'publish', + recommendationType: 'editorRecommendation', + }, + { + id: '3d43ff74-9a20-479d-a218-23bf8eac0b6a', + userId: '5b53da0d-3f88-4e94-b8f9-7eae6a754168', + createdOn: 1534506813446, + updatedOn: 1534506813446, + recommendation: 'publish', + recommendationType: 'editorRecommendation', + }, + ], +} +; + +<ReviewerDetails fragment={fragment} /> +``` diff --git a/packages/component-faraday-ui/src/contextualBoxes/index.js b/packages/component-faraday-ui/src/contextualBoxes/index.js index 671406c817a82b42d530a58972edea909458800f..d8e24b485b0ed222d8380682efb5fd3f90b838fc 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/index.js +++ b/packages/component-faraday-ui/src/contextualBoxes/index.js @@ -1 +1,2 @@ export { default as AssignHE } from './AssignHE' +export { default as ReviewerDetails } from './ReviewerDetails' diff --git a/packages/component-faraday-ui/src/gridItems/Row.js b/packages/component-faraday-ui/src/gridItems/Row.js index c39f27891ecc259a8bfc6de114038e673463e9ab..794ed2b3f00faf6c82f720745b3826eed63bc3f2 100644 --- a/packages/component-faraday-ui/src/gridItems/Row.js +++ b/packages/component-faraday-ui/src/gridItems/Row.js @@ -14,7 +14,7 @@ export default styled.div.attrs({ justify-content: ${props => get(props, 'justify', 'space-evenly')}; height: ${props => get(props, 'height', 'auto')}; - width: 100%; + width: ${props => (props.fitContent ? 'fit-content' : '100%')}; ${heightHelper}; ${marginHelper}; diff --git a/packages/component-faraday-ui/src/index.js b/packages/component-faraday-ui/src/index.js index 0d6361a425da1a76ad28d1a2eca5eecc99a32dd3..b186e2a8a42a0312a827520948661dfa5d57c348 100644 --- a/packages/component-faraday-ui/src/index.js +++ b/packages/component-faraday-ui/src/index.js @@ -30,10 +30,13 @@ export { default as PersonInfo } from './PersonInfo' export { default as PersonInvitation } from './PersonInvitation' export { default as PreviewFile } from './PreviewFile' export { default as RadioWithComments } from './RadioWithComments' +export { default as ReviewerReport } from './ReviewerReport' +export { default as ReviewersTable } from './ReviewersTable' export { default as RemoteOpener } from './RemoteOpener' export { default as ShadowedBox } from './ShadowedBox' export { default as SortableList } from './SortableList' export { default as Tag } from './Tag' +export { default as Tabs } from './Tabs' export { default as Text } from './Text' export { default as Textarea } from './Textarea' export { default as UserProfile } from './UserProfile' diff --git a/packages/component-faraday-ui/src/manuscriptDetails/HandlingEditorAnswer.js b/packages/component-faraday-ui/src/manuscriptDetails/HandlingEditorAnswer.js index b35630fb209118e1123edb9f55335db3555a381f..53f4ca16822becf9cc9eeb0927ad6021ec45a9aa 100644 --- a/packages/component-faraday-ui/src/manuscriptDetails/HandlingEditorAnswer.js +++ b/packages/component-faraday-ui/src/manuscriptDetails/HandlingEditorAnswer.js @@ -1,7 +1,8 @@ import React from 'react' import { reduxForm } from 'redux-form' import { get, has, capitalize } from 'lodash' -import { compose, withProps } from 'recompose' +import { required } from 'xpub-validators' +import { compose, withHandlers, withProps } from 'recompose' import { Button, RadioGroup, ValidatedField } from '@pubsweet/ui' import { @@ -22,13 +23,22 @@ const options = [ ] const HandlingEditorAnswer = ({ - disabled, + toggle, + expanded, decision, isFetching, handleSubmit, + onSubmitForm, shouldShowComments, }) => ( - <ContextualBox label="Respond to Editorial Invitation"> + <ContextualBox + expanded={expanded} + highlight + label="Respond to Editorial Invitation" + mb={2} + scrollIntoView + toggle={toggle} + > <RowOverrideAlert justify="flex-start" ml={1} mt={1}> <Item vertical> <Label required> @@ -39,12 +49,13 @@ const HandlingEditorAnswer = ({ <RadioGroup inline name="decision" options={options} {...input} /> )} name="decision" + validate={[required]} /> </Item> </RowOverrideAlert> {shouldShowComments && ( - <RowOverrideAlert ml={1} mt={2}> + <RowOverrideAlert ml={1} mt={2} pr={2}> <Item vertical> <Label> Decline Reason{' '} @@ -66,7 +77,7 @@ const HandlingEditorAnswer = ({ title={`${decision} this invitation?`} > {showModal => ( - <Button disabled={disabled} onClick={showModal} primary size="medium"> + <Button onClick={onSubmitForm(showModal)} primary size="medium"> RESPOND TO INVITATION </Button> )} @@ -84,8 +95,18 @@ export default compose( })), reduxForm({ form: 'he-answer-invitation', + destroyOnUnmount: false, onSubmit: (values, dispatch, { onResponse, setFetching }) => modalProps => { onResponse(values, { ...modalProps, setFetching }) }, }), + withHandlers({ + onSubmitForm: ({ disabled, handleSubmit }) => showModal => () => { + if (!disabled) { + showModal() + } else { + handleSubmit() + } + }, + }), )(HandlingEditorAnswer) diff --git a/packages/component-faraday-ui/src/manuscriptDetails/HandlingEditorAnswer.md b/packages/component-faraday-ui/src/manuscriptDetails/HandlingEditorAnswer.md index fed5d02bf8525c9fe5f7cd0e374c5385c7727332..21d5d620c87fbf2b1d69e8ff8c616e4a8cc75013 100644 --- a/packages/component-faraday-ui/src/manuscriptDetails/HandlingEditorAnswer.md +++ b/packages/component-faraday-ui/src/manuscriptDetails/HandlingEditorAnswer.md @@ -3,12 +3,19 @@ Handling Editor answer invitation. ```js const formValues = { decision: 'decline', -} +}; -;<HandlingEditorAnswer - formValues={formValues} - onResponse={(values, { setFetching }) => { - setFetching(true) - }} -/> +<RemoteOpener> +{ + ({toggle, expanded}) => + <HandlingEditorAnswer + formValues={formValues} + expanded={expanded} + toggle={toggle} + onResponse={(values, { setFetching }) => { + setFetching(true) + }} + /> +} +</RemoteOpener> ``` diff --git a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptHeader.js b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptHeader.js index 267ab5774524680b6d9e435f0a48efe8d7185ca1..6927e7ba41b1ffb70b1158a9382b06651d4c0c46 100644 --- a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptHeader.js +++ b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptHeader.js @@ -115,6 +115,7 @@ export default compose( <PersonInvitation isFetching={isFetching} ml={1} + withName {...pendingInvitation} onResend={resendInvitation} onRevoke={revokeInvitation} diff --git a/packages/component-faraday-ui/src/manuscriptDetails/index.js b/packages/component-faraday-ui/src/manuscriptDetails/index.js index 85b997fa07bced9f74be3d428c2c80772d12f365..f19644c15e01a55ec224789fa52896398f7a4afd 100644 --- a/packages/component-faraday-ui/src/manuscriptDetails/index.js +++ b/packages/component-faraday-ui/src/manuscriptDetails/index.js @@ -1,3 +1,4 @@ +export { default as HandlingEditorAnswer } from './HandlingEditorAnswer' export { default as ManuscriptDetailsTop } from './ManuscriptDetailsTop' export { default as ManuscriptVersion } from './ManuscriptVersion' export { default as ManuscriptHeader } from './ManuscriptHeader' diff --git a/packages/component-manuscript/package.json b/packages/component-manuscript/package.json index 33efbb8b3a9ef3360bb95e0667c272a228ca5961..458f0a07fcdb726ae1724b51f0e63862a8bca9bd 100644 --- a/packages/component-manuscript/package.json +++ b/packages/component-manuscript/package.json @@ -5,7 +5,7 @@ "license": "MIT", "dependencies": { "prop-types": "^15.5.10", - "recompose": "^0.26.0", + "recompose": "^0.30.0", "xpub-edit": "^2.4.1", "xpub-connect": "1.0.0", "xpub-selectors": "^0.1.0" diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index cbe37e432e311717ed6af777c235baf82096ee25..b1fc53ae09426250b0ebf3c4552d6cdd06e8875e 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -3,10 +3,10 @@ import styled from 'styled-components' import { isEmpty, get, last } from 'lodash' import { Text, - RemoteOpener, ManuscriptHeader, ManuscriptAssignHE, ManuscriptMetadata, + HandlingEditorAnswer, ManuscriptDetailsTop, ManuscriptEicDecision, paddingHelper, @@ -41,66 +41,76 @@ const ManuscriptLayout = ({ permissions, isFetching, formValues, + toggleAssignHE, + heExpanded, + toggleHEResponse, + heResponseExpanded, + onHEResponse, }) => ( <Root pb={1}> {!isEmpty(collection) && !isEmpty(fragment) ? ( - <RemoteOpener> - {(expanded, toggle) => ( - <Fragment> - <ManuscriptDetailsTop - collection={collection} - currentUser={currentUser} - fragment={fragment} - getSignedUrl={getSignedUrl} - history={history} - {...permissions} - /> + <Fragment> + <ManuscriptDetailsTop + collection={collection} + currentUser={currentUser} + fragment={fragment} + getSignedUrl={getSignedUrl} + history={history} + {...permissions} + /> - <ManuscriptHeader - collection={collection} - currentUser={currentUser} - editorInChief={editorInChief} - fragment={fragment} - handlingEditors={handlingEditors} - inviteHE={toggle} - isFetching={isFetching.editorsFetching} - journal={journal} - resendInvitation={assignHE} - revokeInvitation={revokeHE} - /> + <ManuscriptHeader + collection={collection} + currentUser={currentUser} + editorInChief={editorInChief} + fragment={fragment} + handlingEditors={handlingEditors} + inviteHE={toggleAssignHE} + isFetching={isFetching.editorsFetching} + journal={journal} + resendInvitation={assignHE} + revokeInvitation={revokeHE} + /> - <ManuscriptMetadata - currentUser={currentUser} - fragment={fragment} - getSignedUrl={getSignedUrl} - /> + <ManuscriptMetadata + currentUser={currentUser} + fragment={fragment} + getSignedUrl={getSignedUrl} + /> - <ManuscriptAssignHE - assignHE={assignHE} - currentUser={currentUser} - expanded={expanded} - handlingEditors={handlingEditors} - isFetching={isFetching.editorsFetching} - toggle={toggle} - /> + {permissions.isInvitedHE && ( + <HandlingEditorAnswer + expanded={heResponseExpanded} + formValues={formValues.heInvitation} + onResponse={onHEResponse} + toggle={toggleHEResponse} + /> + )} + + <ManuscriptAssignHE + assignHE={assignHE} + currentUser={currentUser} + expanded={heExpanded} + handlingEditors={handlingEditors} + isFetching={isFetching.editorsFetching} + toggle={toggleAssignHE} + /> - {permissions.canMakeDecision && ( - <ManuscriptEicDecision - formValues={get(formValues, 'eicDecision')} - isFetching={isFetching.recommendationsFetching} - messagesLabel={messagesLabel} - mt={2} - options={ - get(collection, 'status', 'submitted') === 'submitted' - ? [last(eicDecisions)] - : eicDecisions - } - submitDecision={createRecommendation} - /> - )} - </Fragment> + {permissions.canMakeDecision && ( + <ManuscriptEicDecision + formValues={get(formValues, 'eicDecision')} + isFetching={isFetching.recommendationsFetching} + messagesLabel={messagesLabel} + mt={2} + options={ + get(collection, 'status', 'submitted') === 'submitted' + ? [last(eicDecisions)] + : eicDecisions + } + submitDecision={createRecommendation} + /> )} - </RemoteOpener> + </Fragment> ) : ( <Text>Loading...</Text> )} diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js index 9ab87ac35d061a3f4b0dc6fbf085b6ef08f57633..fb18e691bd87ebc10877a2f9959d71c5e573b368 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -2,10 +2,10 @@ import { connect } from 'react-redux' import { actions } from 'pubsweet-client' import { ConnectPage } from 'xpub-connect' import { withJournal } from 'xpub-journal' -import { head, get, isEmpty } from 'lodash' +import { getFormValues } from 'redux-form' import { replace } from 'react-router-redux' import { withRouter } from 'react-router-dom' -import { getFormValues } from 'redux-form' +import { head, get, isEmpty, isUndefined } from 'lodash' import { selectFragment, selectCollection, @@ -19,6 +19,7 @@ import { withProps, withHandlers, setDisplayName, + fromRenderProps, } from 'recompose' import { getSignedUrl } from 'pubsweet-components-faraday/src/redux/files' import { reviewerDecision } from 'pubsweet-components-faraday/src/redux/reviewers' @@ -33,10 +34,12 @@ import { canMakeRevision, canMakeDecision, canEditManuscript, + pendingHEInvitation, currentUserIsReviewer, canMakeRecommendation, canOverrideTechnicalChecks, } from 'pubsweet-component-faraday-selectors' +import { RemoteOpener } from 'pubsweet-component-faraday-ui' import ManuscriptLayout from './ManuscriptLayout' import { parseEicDecision, parseSearchParams, redirectToError } from './utils' @@ -47,6 +50,7 @@ import { assignHandlingEditor, revokeHandlingEditor, selectHandlingEditors, + handlingEditorDecision, } from '../redux/editors' import { createRecommendation, @@ -69,6 +73,7 @@ export default compose( hasManuscriptFailure: hasManuscriptFailure(state), fragment: selectFragment(state, match.params.version), collection: selectCollection(state, match.params.project), + pendingHEInvitation: pendingHEInvitation(state, match.params.project), editorialRecommendations: selectEditorialRecommendations( state, match.params.version, @@ -87,36 +92,58 @@ export default compose( updateVersion: actions.updateFragment, }, ), - connect((state, { currentUser, match, collection, fragment }) => ({ - currentUser: { - ...currentUser, - token: getUserToken(state), - isEIC: currentUserIs(state, 'adminEiC'), - isHE: currentUserIs(state, 'isHE'), - isReviewer: currentUserIsReviewer(state), - canAssignHE: canAssignHE(state, match.params.project), - }, - isFetching: { - editorsFetching: selectFetching(state), - recommendationsFetching: recommendationsFetching(state), - }, - permissions: { - canMakeRevision: canMakeRevision(state, collection, fragment), - canMakeDecision: canMakeDecision(state, collection, fragment), - canEditManuscript: canEditManuscript(state, collection, fragment), - canOverrideTechChecks: canOverrideTechnicalChecks(state, collection), - canMakeRecommendation: canMakeRecommendation(state, collection, fragment), - }, - formValues: { - eicDecision: getFormValues('eic-decision')(state), - }, - })), + connect( + ( + state, + { pendingHEInvitation, currentUser, match, collection, fragment }, + ) => ({ + currentUser: { + ...currentUser, + token: getUserToken(state), + isEIC: currentUserIs(state, 'adminEiC'), + isHE: currentUserIs(state, 'isHE'), + isReviewer: currentUserIsReviewer(state), + canAssignHE: canAssignHE(state, match.params.project), + }, + isFetching: { + editorsFetching: selectFetching(state), + recommendationsFetching: recommendationsFetching(state), + }, + permissions: { + isInvitedHE: !isUndefined(pendingHEInvitation), + canMakeRevision: canMakeRevision(state, collection, fragment), + canMakeDecision: canMakeDecision(state, collection, fragment), + canEditManuscript: canEditManuscript(state, collection, fragment), + canOverrideTechChecks: canOverrideTechnicalChecks(state, collection), + canMakeRecommendation: canMakeRecommendation( + state, + collection, + fragment, + ), + }, + formValues: { + eicDecision: getFormValues('eic-decision')(state), + heInvitation: getFormValues('he-answer-invitation')(state), + }, + }), + ), ConnectPage(({ currentUser, handlingEditors, collection }) => { if (currentUser.isEIC) { return [getHandlingEditors()] } return [] }), + withHandlers({ + fetchUpdatedCollection: ({ + fragment, + collection, + getFragment, + getCollection, + }) => () => { + getCollection({ id: collection.id }) + getFragment(collection, fragment) + }, + }), withHandlers({ updateManuscript: ({ updateVersion, collection, fragment }) => data => updateVersion(collection, { @@ -130,11 +157,8 @@ export default compose( } }, assignHE: ({ - fragment, - collection, - getFragment, - getCollection, assignHandlingEditor, + fetchUpdatedCollection, collection: { id: collectionId }, }) => (email, modalProps) => assignHandlingEditor({ @@ -142,8 +166,7 @@ export default compose( collectionId, }) .then(() => { - getCollection({ id: collectionId }) - getFragment(collection, fragment) + fetchUpdatedCollection() modalProps.hideModal() }) .catch(() => modalProps.setModalError('Oops! Something went wrong.')), @@ -164,8 +187,7 @@ export default compose( createRecommendation: ({ fragment, collection, - getFragment, - getCollection, + fetchUpdatedCollection, createRecommendation, }) => (values, modalProps) => { const recommendation = parseEicDecision(values) @@ -175,15 +197,50 @@ export default compose( collectionId: collection.id, }) .then(() => { - getCollection({ id: collection.id }) - getFragment(collection, fragment) + fetchUpdatedCollection() modalProps.hideModal() }) .catch(() => { modalProps.setModalError('Oops! Something went wrong.') }) }, + onHEResponse: ({ + history, + collection, + pendingHEInvitation, + fetchUpdatedCollection, + }) => (values, { hideModal, setModalError, setFetching }) => { + const isAccepted = get(values, 'decision', 'decline') === 'accept' + setFetching(true) + return handlingEditorDecision({ + isAccepted, + collectionId: collection.id, + reason: get(values, 'reason', ''), + invitationId: pendingHEInvitation.id, + }) + .then(() => { + setFetching(false) + hideModal() + if (isAccepted) { + fetchUpdatedCollection() + } else { + history.replace('/') + } + }) + .catch(() => { + setFetching(false) + setModalError('Something went wrong...') + }) + }, }), + fromRenderProps(RemoteOpener, ({ toggle, expanded }) => ({ + toggleAssignHE: toggle, + heExpanded: expanded, + })), + fromRenderProps(RemoteOpener, ({ toggle, expanded }) => ({ + toggleHEResponse: toggle, + heResponseExpanded: expanded, + })), lifecycle({ componentDidMount() { const { @@ -197,6 +254,7 @@ export default compose( setEditorInChief, clearCustomError, hasManuscriptFailure, + permissions: { isInvitedHE }, } = this.props if (hasManuscriptFailure) { history.push('/not-found') @@ -219,6 +277,10 @@ export default compose( apiGet(`/users?editorInChief=true`).then(res => setEditorInChief(head(res.users)), ) + + if (isInvitedHE) { + this.props.toggleHEResponse() + } }, }), withProps(({ fragment }) => ({ diff --git a/packages/component-manuscript/src/redux/editors.js b/packages/component-manuscript/src/redux/editors.js index b9b322422845e209260a194a3a0dc9155dd80598..e39a801a48fc179f638dfb1c36d16a50dd664401 100644 --- a/packages/component-manuscript/src/redux/editors.js +++ b/packages/component-manuscript/src/redux/editors.js @@ -17,7 +17,11 @@ const setHandlingEditors = editors => ({ }) export const selectFetching = state => get(state, 'editors.isFetching', false) -export const selectHandlingEditors = state => get(state, 'editors.editors', []) +export const selectHandlingEditors = state => + chain(state) + .get('editors.editors', []) + .filter(editor => editor.isActive && editor.isConfirmed) + .value() export const canAssignHE = (state, collectionId = '') => { const isEIC = currentUserIs(state, 'adminEiC') @@ -74,12 +78,12 @@ export const revokeHandlingEditor = ({ ) } -export const handlingEditorDecision = ( +export const handlingEditorDecision = ({ invitationId, collectionId, isAccepted, reason, -) => +}) => update(`/collections/${collectionId}/invitations/${invitationId}`, { isAccepted, reason, diff --git a/packages/component-sortable-list/package.json b/packages/component-sortable-list/package.json index 8914a5d0f541bf321de91e75057f723d645dae43..d32847a790e669e0afea31fab3b85895b28ace8c 100644 --- a/packages/component-sortable-list/package.json +++ b/packages/component-sortable-list/package.json @@ -7,6 +7,6 @@ "react": "^16.4.2", "react-dnd": "^2.5.4", "react-dom": "^15.6.1", - "recompose": "^0.26.0" + "recompose": "^0.30.0" } } diff --git a/packages/component-wizard/package.json b/packages/component-wizard/package.json index a3d6410b2dd57c3e85be63dae36dbe1c261993d2..42283d163cdb6f874c3e1946fcdce4a0f1755b8d 100644 --- a/packages/component-wizard/package.json +++ b/packages/component-wizard/package.json @@ -19,7 +19,7 @@ "react-router-dom": "^4.2.2", "redux": "^3.6.0", "redux-form": "7.0.3", - "recompose": "^0.26.0", + "recompose": "^0.30.0", "xpub-validators": "^0.0.5", "xpub-connect": "1.0.0", "xpub-journal": "^0.0.6", diff --git a/packages/components-faraday/package.json b/packages/components-faraday/package.json index 6fb598298b6ea6f787d9332b4f96514f4bf616d5..4a1e80ac4b0382d52f4f85b040b63723f1392ab8 100644 --- a/packages/components-faraday/package.json +++ b/packages/components-faraday/package.json @@ -17,7 +17,7 @@ "react-dom": "^16.1.0", "react-router-dom": "^4.2.2", "react-tippy": "^1.2.2", - "recompose": "^0.26.0", + "recompose": "^0.30.0", "redux": "^3.6.0", "redux-form": "7.0.3", "styled-components": "^3.4.2", diff --git a/packages/components-faraday/src/components/Dashboard/Dashboard.js b/packages/components-faraday/src/components/Dashboard/Dashboard.js index 82137f785d294edbe2d8cf5996a492652b6b348b..0c106443fd6d2da18ebbdce662c5d738bc6b8dd8 100644 --- a/packages/components-faraday/src/components/Dashboard/Dashboard.js +++ b/packages/components-faraday/src/components/Dashboard/Dashboard.js @@ -23,6 +23,6 @@ const Dashboard = ({ export default compose( withProps(({ dashboard, filterItems }) => ({ - dashboardItems: filterItems(dashboard.all), + dashboardItems: filterItems(dashboard), })), )(Dashboard) diff --git a/packages/components-faraday/src/components/Dashboard/DashboardPage.js b/packages/components-faraday/src/components/Dashboard/DashboardPage.js index 944329379b27e97f83a5d770b1a50ea49142eaa7..6831dbc0265ec953396fafa62fe49e8bae853af9 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardPage.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardPage.js @@ -5,9 +5,12 @@ import { withJournal } from 'xpub-journal' import { ConnectPage } from 'xpub-connect' import { withRouter } from 'react-router-dom' import { compose, withContext } from 'recompose' -import { newestFirst, selectCurrentUser } from 'xpub-selectors' +import { selectCurrentUser } from 'xpub-selectors' -import { getUserPermissions } from 'pubsweet-component-faraday-selectors' +import { + getUserPermissions, + newestFirstParseCustomId, +} from 'pubsweet-component-faraday-selectors' import { Dashboard } from './' import { priorityFilter, orderFilter, withFiltersHOC } from '../Filters' @@ -18,24 +21,8 @@ export default compose( state => { const { collections, conversion } = state const currentUser = selectCurrentUser(state) - const sortedCollections = newestFirst(collections) + const dashboard = newestFirstParseCustomId(collections) - const dashboard = { - owner: sortedCollections.filter( - collection => - collection.owners && - collection.owners.some(owner => owner.id === currentUser.id), - ), - reviewer: sortedCollections.filter( - collection => - collection.reviewers && - collection.reviewers.some( - reviewer => reviewer && reviewer.user === currentUser.id, - ), - ), - - all: sortedCollections, - } const userPermissions = getUserPermissions(state) return { dashboard, diff --git a/packages/components-faraday/src/components/Login/LoginPage.js b/packages/components-faraday/src/components/Login/LoginPage.js index b2bf3b19c045e197331d8f12204a5c488014fd73..55893a41090cf2a321853163a28edad365e6e8c1 100644 --- a/packages/components-faraday/src/components/Login/LoginPage.js +++ b/packages/components-faraday/src/components/Login/LoginPage.js @@ -1,6 +1,7 @@ import React from 'react' -import styled from 'styled-components' +import { get } from 'lodash' import { connect } from 'react-redux' +import styled from 'styled-components' import { reduxForm } from 'redux-form' import { th } from '@pubsweet/ui-toolkit' import { withProps, lifecycle, compose } from 'recompose' @@ -88,8 +89,9 @@ const LoginPage = compose( reduxForm({ form: 'login', enableReinitialize: false, - onSubmit: (values, dispatch) => { - dispatch(loginUser(values, '/dashboard')) + onSubmit: (values, dispatch, { location }) => { + const redirectTo = get(location, 'state.from.pathname', '/dashboard') + dispatch(loginUser(values, redirectTo)) }, }), )(Login) diff --git a/packages/hindawi-theme/src/elements/Accordion.js b/packages/hindawi-theme/src/elements/Accordion.js index 75707f3e6c145485c7e5385c11ec95c3812a05b5..f6c4a305aa75621270886ac78bccfa32a59b5e6e 100644 --- a/packages/hindawi-theme/src/elements/Accordion.js +++ b/packages/hindawi-theme/src/elements/Accordion.js @@ -1,14 +1,33 @@ import { css } from 'styled-components' import { th } from '@pubsweet/ui-toolkit' +const boxShadow = props => { + if (props.transparent) { + return css` + box-shadow: transparent; + ` + } + + if (props.highlight) { + return css` + border: 1px solid ${th('colorPrimary')}; + box-shadow: 0 0 2px 0 ${th('colorPrimary')}; + ` + } + + return css` + box-shadow: ${th('boxShadow')}; + ` +} + export default { Root: css` background-color: ${props => props.transparent ? 'transparent' : th('accordion.backgroundColor')}; border-radius: ${th('borderRadius')}; - box-shadow: ${props => - props.transparent ? 'transparent' : th('boxShadow')}; flex: 1; + + ${boxShadow}; `, Header: { Root: css` diff --git a/packages/xpub-faraday/package.json b/packages/xpub-faraday/package.json index 62a5b6e75d408ff3e8e47b45b3e80f4aebb5c2c5..4e30ddde8745944893f2ed9601603be229ba84e8 100644 --- a/packages/xpub-faraday/package.json +++ b/packages/xpub-faraday/package.json @@ -36,7 +36,7 @@ "react-dnd-html5-backend": "^2.5.4", "react-dom": "^16.2.0", "react-router-dom": "^4.2.2", - "recompose": "^0.26.0", + "recompose": "^0.30.0", "redux": "^3.6.0", "redux-form": "7.0.3", "redux-logger": "^3.0.1", diff --git a/scripts/wait-for-it.sh b/scripts/wait-for-it.sh new file mode 100755 index 0000000000000000000000000000000000000000..ea896a848ceabb0234802257d1fb78ba6eb1b4cb --- /dev/null +++ b/scripts/wait-for-it.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +cmdname=$(basename $0) + +echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $TIMEOUT -gt 0 ]]; then + echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" + else + echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" + fi + start_ts=$(date +%s) + while : + do + if [[ $ISBUSY -eq 1 ]]; then + nc -z $HOST $PORT + result=$? + else + (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1 + result=$? + fi + if [[ $result -eq 0 ]]; then + end_ts=$(date +%s) + echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" + break + fi + sleep 1 + done + return $result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $QUIET -eq 1 ]]; then + timeout $BUSYTIMEFLAG $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & + else + timeout $BUSYTIMEFLAG $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & + fi + PID=$! + trap "kill -INT -$PID" INT + wait $PID + RESULT=$? + if [[ $RESULT -ne 0 ]]; then + echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" + fi + return $RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + hostport=(${1//:/ }) + HOST=${hostport[0]} + PORT=${hostport[1]} + shift 1 + ;; + --child) + CHILD=1 + shift 1 + ;; + -q | --quiet) + QUIET=1 + shift 1 + ;; + -s | --strict) + STRICT=1 + shift 1 + ;; + -h) + HOST="$2" + if [[ $HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + HOST="${1#*=}" + shift 1 + ;; + -p) + PORT="$2" + if [[ $PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + PORT="${1#*=}" + shift 1 + ;; + -t) + TIMEOUT="$2" + if [[ $TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$HOST" == "" || "$PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +TIMEOUT=${TIMEOUT:-15} +STRICT=${STRICT:-0} +CHILD=${CHILD:-0} +QUIET=${QUIET:-0} + +# check to see if timeout is from busybox? +# check to see if timeout is from busybox? +TIMEOUT_PATH=$(realpath $(which timeout)) +if [[ $TIMEOUT_PATH =~ "busybox" ]]; then + ISBUSY=1 + BUSYTIMEFLAG="-t" +else + ISBUSY=0 + BUSYTIMEFLAG="" +fi + +if [[ $CHILD -gt 0 ]]; then + wait_for + RESULT=$? + exit $RESULT +else + if [[ $TIMEOUT -gt 0 ]]; then + wait_for_wrapper + RESULT=$? + else + wait_for + RESULT=$? + fi +fi + +if [[ $CLI != "" ]]; then + if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then + echoerr "$cmdname: strict mode, refusing to execute subprocess" + exit $RESULT + fi + exec "${CLI[@]}" +else + exit $RESULT +fi + diff --git a/yarn.lock b/yarn.lock index 246baf960a5f324070ad21e75f203c5701510145..fe67b55014801df66d22fb2c6483d23c18545666 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,6 +40,12 @@ esutils "^2.0.2" js-tokens "^3.0.0" +"@babel/runtime@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0.tgz#adeb78fedfc855aa05bc041640f3f6f98e85424c" + dependencies: + regenerator-runtime "^0.12.0" + "@babel/runtime@^7.0.0-beta.38": version "7.0.0-beta.41" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.41.tgz#776ce13391b8154ccfdea71018a47b63e4d97e74" @@ -9357,7 +9363,7 @@ react-is@^16.3.1: version "16.3.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22" -react-lifecycles-compat@^3.0.4: +react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -9678,6 +9684,17 @@ recompose@^0.26.0: hoist-non-react-statics "^2.3.1" symbol-observable "^1.0.4" +recompose@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.30.0.tgz#82773641b3927e8c7d24a0d87d65aeeba18aabd0" + dependencies: + "@babel/runtime" "^7.0.0" + change-emitter "^0.1.2" + fbjs "^0.8.1" + hoist-non-react-statics "^2.3.1" + react-lifecycles-compat "^3.0.2" + symbol-observable "^1.0.4" + recursive-readdir@2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99" @@ -9778,6 +9795,10 @@ regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1: version "0.11.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" +regenerator-runtime@^0.12.0: + version "0.12.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" + regenerator-transform@^0.10.0: version "0.10.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"