diff --git a/.eslintrc.json b/.eslintrc.json index a4741cc5a0..e4776f503f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -66,7 +66,8 @@ "delimiter": "semi", "requireLast": false } - }] + }], + "@typescript-eslint/no-loss-of-precision": "off" }, "ignorePatterns": [ ".github/**", diff --git a/.github/workflows/test-files-on-demand.yml b/.github/workflows/test-files-on-demand.yml index 296b371f3e..5e5093284e 100644 --- a/.github/workflows/test-files-on-demand.yml +++ b/.github/workflows/test-files-on-demand.yml @@ -24,7 +24,6 @@ jobs: - name: Cypress run uses: cypress-io/github-action@v2 env: - REACT_APP_API_URL: ${{ secrets.GH_REACT_APP_API_URL }} REACT_APP_BLOCKNATIVE_ID: ${{ secrets.GH_REACT_APP_BLOCKNATIVE_ID }} REACT_APP_FILES_VERIFIER_NAME: ${{ secrets.GH_REACT_APP_FILES_VERIFIER_NAME }} REACT_APP_FILES_UUID_VERIFIER_NAME: 'chainsafe-uuid-testnet' diff --git a/.github/workflows/test-files.yml b/.github/workflows/test-files.yml index e292cd49ca..f90cbac1c2 100644 --- a/.github/workflows/test-files.yml +++ b/.github/workflows/test-files.yml @@ -33,7 +33,6 @@ jobs: - name: Cypress run uses: cypress-io/github-action@v2 env: - REACT_APP_API_URL: ${{ secrets.GH_REACT_APP_API_URL }} REACT_APP_BLOCKNATIVE_ID: ${{ secrets.GH_REACT_APP_BLOCKNATIVE_ID }} REACT_APP_FILES_VERIFIER_NAME: ${{ secrets.GH_REACT_APP_FILES_VERIFIER_NAME }} REACT_APP_FILES_UUID_VERIFIER_NAME: 'chainsafe-uuid-testnet' diff --git a/.github/workflows/test-storage.yml b/.github/workflows/test-storage.yml index a8734d3111..dbf77b10b8 100644 --- a/.github/workflows/test-storage.yml +++ b/.github/workflows/test-storage.yml @@ -33,7 +33,6 @@ jobs: - name: Cypress run uses: cypress-io/github-action@v2 env: - REACT_APP_API_URL: ${{ secrets.GH_REACT_APP_API_URL }} REACT_APP_BLOCKNATIVE_ID: ${{ secrets.GH_REACT_APP_BLOCKNATIVE_ID }} REACT_APP_FILES_VERIFIER_NAME: ${{ secrets.GH_REACT_APP_FILES_VERIFIER_NAME }} REACT_APP_FILES_UUID_VERIFIER_NAME: 'chainsafe-uuid-testnet' diff --git a/package.json b/package.json index 703957bf2c..5933f3b967 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dependencies": {}, "devDependencies": { "@sentry/cli": "1.60.1", - "@typescript-eslint/eslint-plugin": "^4.15.2", + "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^4.15.2", "chalk": "^4.1.0", "eslint": "^6.8.0", diff --git a/packages/common-components/src/FileInput/FileInput.tsx b/packages/common-components/src/FileInput/FileInput.tsx index 1eff0ed230..458134a5fe 100644 --- a/packages/common-components/src/FileInput/FileInput.tsx +++ b/packages/common-components/src/FileInput/FileInput.tsx @@ -51,6 +51,8 @@ const useStyles = makeStyles(({ constants, palette, overrides }: ITheme) => }, error: { color: palette.error.main, + backgroundColor: palette.additional["gray"][3], + padding: "4px 40px", ...overrides?.FileInput?.error }, item: { diff --git a/packages/common-components/src/SearchBar/SearchBar.tsx b/packages/common-components/src/SearchBar/SearchBar.tsx index 1924192ca7..ae8972c0a2 100644 --- a/packages/common-components/src/SearchBar/SearchBar.tsx +++ b/packages/common-components/src/SearchBar/SearchBar.tsx @@ -83,17 +83,15 @@ const useStyles = makeStyles( "& input": { fontSize: 16, lineHeight: "24px", - padding: `${constants.generalUnit}px ${ - constants.generalUnit * 1.5 - }px` + padding: `${constants.generalUnit}px ${constants.generalUnit * 1.5}px`, + paddingRight: constants.generalUnit * 4.25 }, ...overrides?.SearchBar?.inputArea?.large }, "&.medium": { "& input": { - padding: `${constants.generalUnit * 0.625}px ${ - constants.generalUnit * 1.5 - }px` + padding: `${constants.generalUnit * 0.625}px ${constants.generalUnit * 1.5}px`, + paddingRight: constants.generalUnit * 4.25 }, ...overrides?.SearchBar?.inputArea?.medium }, diff --git a/packages/files-ui/.env.example b/packages/files-ui/.env.example index cc96080ef7..45101c9bd5 100644 --- a/packages/files-ui/.env.example +++ b/packages/files-ui/.env.example @@ -1,7 +1,7 @@ PORT=3000 HTTPS=false -REACT_APP_API_URL=https://stage.imploy.site/api/v1 +REACT_APP_API_URL=https://stage-api.chainsafe.io/api/v1 REACT_APP_STRIPE_PK= REACT_APP_SENTRY_DSN_URL= diff --git a/packages/files-ui/cypress/fixtures/cardData.ts b/packages/files-ui/cypress/fixtures/cardData.ts index 6e012c8cc3..c95bfe7e41 100644 --- a/packages/files-ui/cypress/fixtures/cardData.ts +++ b/packages/files-ui/cypress/fixtures/cardData.ts @@ -1,3 +1,5 @@ +import dayjs from "dayjs" + export const visaNumber = "4242424242424242" export const visaCvc = "123" export const visaExpiry = "12/30" @@ -6,4 +8,5 @@ export const mastercardCvc = "456" export const mastercardExpiry = "01/31" export const invalidCardNumber = "6242424242424255" export const invalidCvc = "11" -export const invalidExpiry = "02/21" \ No newline at end of file +export const invalidExpiry = "02/21" +export const currentDateExpiry = dayjs().format("MM/YY") \ No newline at end of file diff --git a/packages/files-ui/cypress/support/commands.ts b/packages/files-ui/cypress/support/commands.ts index cdbe4a98bd..c8f9e4b0e6 100644 --- a/packages/files-ui/cypress/support/commands.ts +++ b/packages/files-ui/cypress/support/commands.ts @@ -46,6 +46,7 @@ export interface Web3LoginOptions { clearCSFBucket?: boolean clearTrashBucket?: boolean deleteShareBucket?: boolean + withNewSession?: boolean withNewUser?: boolean deleteCreditCard? : boolean resetToFreePlan?: boolean @@ -60,6 +61,7 @@ Cypress.Commands.add( clearTrashBucket = false, deleteShareBucket = false, withNewUser = true, + withNewSession = false, deleteCreditCard = false, resetToFreePlan = false }: Web3LoginOptions = {}) => { @@ -78,8 +80,9 @@ Cypress.Commands.add( }) }) - if (withNewUser){ - cy.session("web3loginNewUser", () => { + if (withNewUser || withNewSession){ + const sessionName = `web3loginNewUser-${withNewSession ? new Date().toString() : "0"}` + cy.session(sessionName, () => { cy.visit(url) authenticationPage.web3Button().click() authenticationPage.showMoreButton().click() @@ -213,6 +216,7 @@ declare global { * @param {Boolean} options.clearTrashBucket - (default: false) - whether any file in the trash bucket should be deleted. * @param {Boolean} options.deleteShareBucket - (default: false) - whether any shared bucket should be deleted. * @param {Boolean} options.withNewUser - (default: true) - whether to create a new user for this session. + * @param {Boolean} options.withNewSession - (default: false) - whether to create a new session. * @param {Boolean} options.deleteCreditCard - (default: false) - whether to delete the default credit card associate to the account. * @param {Boolean} options.resetToFreePlan - (default false) - whether to cancel any plan to make sure the user is on the free one. * @example cy.web3Login({saveBrowser: true, url: 'http://localhost:8080'}) diff --git a/packages/files-ui/cypress/support/index.ts b/packages/files-ui/cypress/support/index.ts index 80de34037f..5463959dd3 100644 --- a/packages/files-ui/cypress/support/index.ts +++ b/packages/files-ui/cypress/support/index.ts @@ -16,11 +16,8 @@ // Import commands.js using ES2015 syntax: import "./commands" -// the following gets rid of the exception "ResizeObserver loop limit exceeded" -// which someone on the internet says we can safely ignore -// source https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded +// returning false prevents Cypress from failing the test automatically Cypress.on("uncaught:exception", () => { - /* returning false here prevents Cypress from failing the test */ return false }) diff --git a/packages/files-ui/cypress/support/page-objects/basePage.ts b/packages/files-ui/cypress/support/page-objects/basePage.ts index d530cefd7d..5fa35e2db0 100644 --- a/packages/files-ui/cypress/support/page-objects/basePage.ts +++ b/packages/files-ui/cypress/support/page-objects/basePage.ts @@ -5,6 +5,14 @@ export const basePage = { searchInput: () => cy.get("[data-testid=input-search-bar]"), signOutDropdown: () => cy.get("[data-testid=dropdown-title-sign-out-dropdown]"), signOutMenuOption: () => cy.get("[data-cy=menu-sign-out]"), + notificationButton: () => cy.get("[data-testid=dropdown-title-notifications]"), + notificationsHeader: () => cy.get("[data-cy=label-notifications-header]"), + notificationsThisWeekHeader: () => cy.get("[data-cy=label-notifications-this-week]"), + notificationsOlderHeader: () => cy.get("[data-cy=label-notifications-older]"), + notificationContainer: () => cy.get("[data-cy=container-notification]"), + notificationTitle: () => cy.get("[data-cy=label-notification-title]"), + notificationTime: () => cy.get("[data-cy=label-notification-time]"), + // Mobile view only element hamburgerMenuButton: () => cy.get("[data-testid=icon-hamburger-menu]"), diff --git a/packages/files-ui/cypress/support/page-objects/homePage.ts b/packages/files-ui/cypress/support/page-objects/homePage.ts index 2e2470d796..73f5be81d9 100644 --- a/packages/files-ui/cypress/support/page-objects/homePage.ts +++ b/packages/files-ui/cypress/support/page-objects/homePage.ts @@ -25,6 +25,7 @@ export const homePage = { renameMenuOption: () => cy.get("[data-cy=menu-rename]"), moveMenuOption: () => cy.get("[data-cy=menu-move]"), deleteMenuOption: () => cy.get("[data-cy=menu-delete]"), + shareMenuOption: () => cy.get("[data-cy=menu-share]"), // helpers and convenience functions uploadFile(filePath: string) { diff --git a/packages/files-ui/cypress/support/page-objects/modals/editSharedFolderModal.ts b/packages/files-ui/cypress/support/page-objects/modals/editSharedFolderModal.ts index e0f9396e53..a1ae38445d 100644 --- a/packages/files-ui/cypress/support/page-objects/modals/editSharedFolderModal.ts +++ b/packages/files-ui/cypress/support/page-objects/modals/editSharedFolderModal.ts @@ -1,6 +1,6 @@ export const editSharedFolderModal = { body: () => cy.get("[data-testid=modal-container-edit-shared-folder]", { timeout: 10000 }), - cancelButton: () => cy.get("[data-cy=button-cancel-create-shared-folder]"), + closeButton: () => cy.get("[data-cy=button-close-manage-shared-folder]"), createButton: () => cy.get("[data-cy=button-create-shared-folder]", { timeout: 10000 }), editPermissionInput: () => cy.get("[data-cy=input-edit-permission]"), folderNameInput: () => cy.get("[data-cy=input-shared-folder-name]"), diff --git a/packages/files-ui/cypress/support/page-objects/modals/shareFileModal.ts b/packages/files-ui/cypress/support/page-objects/modals/shareFileModal.ts new file mode 100644 index 0000000000..56ba5aa65c --- /dev/null +++ b/packages/files-ui/cypress/support/page-objects/modals/shareFileModal.ts @@ -0,0 +1,10 @@ +export const shareFileModal = { + body: () => cy.get("[data-testid=modal-container-share-file]", { timeout: 10000 }), + shareNameInput: () => cy.get("[data-cy=input-new-share-name]"), + selectFolderInput: () => cy.get("[data-cy=input-select-existing-folder]"), + existingFolderInputOption: () => cy.get("[data-cy=input-select-existing-folder-option]"), + keepOriginalFilesCheckbox: () => cy.get("[data-testid=checkbox-keep-original-files]"), + cancelButton: () => cy.get("[data-testid=button-cancel-share-file]"), + copyOverButton: () => cy.get("[data-testid=button-copy-over]"), + moveOverButton: () => cy.get("[data-testid=button-move-over]") +} diff --git a/packages/files-ui/cypress/support/page-objects/settingsPage.ts b/packages/files-ui/cypress/support/page-objects/settingsPage.ts index 36545a222e..eeb62881d8 100644 --- a/packages/files-ui/cypress/support/page-objects/settingsPage.ts +++ b/packages/files-ui/cypress/support/page-objects/settingsPage.ts @@ -1,5 +1,5 @@ import { basePage } from "./basePage" -import { visaNumber, visaExpiry, visaCvc } from "../../fixtures/cardData" +import { visaNumber, visaExpiry, visaCvc, currentDateExpiry } from "../../fixtures/cardData" import { cardAddedToast } from "../../support/page-objects/toasts/cardAddedToast" import { selectPlanModal } from "../../support/page-objects/modals/billing/selectPlanModal" import { planDetailsModal } from "../../support/page-objects/modals/billing/planDetailsModal" @@ -40,8 +40,9 @@ export const settingsPage = { payNowButton: () => cy.get("[data-testid=button-pay-invoice]"), // use this convenience function when an upgraded account is required as a test requisite - upgradeSubscription(plan: "pro" | "max") { - const planContainer = plan === "pro" ? "@filesProBox" : "@filesMaxBox" + upgradeSubscription(subDetails: {plan: "pro"|"max"; isCardExpiring?: boolean}) { + const planContainer = subDetails.plan === "pro" ? "@filesProBox" : "@filesMaxBox" + const cardExpiry = subDetails.isCardExpiring === true ? currentDateExpiry : visaExpiry this.subscriptionTabButton().click() this.changePlanButton().click() @@ -57,7 +58,7 @@ export const settingsPage = { .click() cy.awaitStripeElementReady() selectPaymentMethodModal.cardNumberInput().type(visaNumber) - selectPaymentMethodModal.expiryDateInput().type(visaExpiry) + selectPaymentMethodModal.expiryDateInput().type(cardExpiry) selectPaymentMethodModal.cvcNumberInput().type(visaCvc) selectPaymentMethodModal.useThisCardButton().click() cy.awaitStripeConfirmation() diff --git a/packages/files-ui/cypress/support/page-objects/sharedPage.ts b/packages/files-ui/cypress/support/page-objects/sharedPage.ts index dea2155d46..6912f13cfd 100644 --- a/packages/files-ui/cypress/support/page-objects/sharedPage.ts +++ b/packages/files-ui/cypress/support/page-objects/sharedPage.ts @@ -11,7 +11,7 @@ export const sharedPage = { createSharedFolderButton: () => cy.get("[data-cy=button-create-a-shared-folder]"), sharedFolderItemRow: () => cy.get("[data-cy=row-shared-folder-item]", { timeout: 20000 }), sharedFolderIcon: () => cy.get("[data-cy=cell-shared-folder-icon]"), - sharedFolderItemName: () => cy.get("[data-cy=cell-shared-folder-item-name]"), + sharedFolderItemName: () => cy.get("[data-cy=cell-shared-folder-item-name]", { timeout: 10000 }), shareOwnerCell: () => cy.get("[data-cy=cell-share-owner]"), sharedWithCell: () => cy.get("[data-cy=cell-shared-with]"), sharedFolderSizeCell: () => cy.get("[data-cy=cell-shared-folder-size]"), @@ -30,7 +30,7 @@ export const sharedPage = { createSharedFolderModal.body().should("be.visible") createSharedFolderModal.folderNameInput().type(sharedFolderName) createSharedFolderModal.createButton().safeClick() - editSharedFolderModal.cancelButton().safeClick() + editSharedFolderModal.closeButton().safeClick() editSharedFolderModal.body().should("not.exist") sharedPage.sharedFolderItemRow().should("have.length", 1) } diff --git a/packages/files-ui/cypress/support/page-objects/toasts/shareSuccessToast.ts b/packages/files-ui/cypress/support/page-objects/toasts/shareSuccessToast.ts new file mode 100644 index 0000000000..c6b0ad281b --- /dev/null +++ b/packages/files-ui/cypress/support/page-objects/toasts/shareSuccessToast.ts @@ -0,0 +1,4 @@ +export const shareSuccessToast = { + body: () => cy.get("[data-testid=toast-share-success]", { timeout: 10000 }), + closeButton: () => cy.get("[data-testid=button-close-toast-share-success]") +} diff --git a/packages/files-ui/cypress/support/page-objects/toasts/sharingProgressToast.ts b/packages/files-ui/cypress/support/page-objects/toasts/sharingProgressToast.ts new file mode 100644 index 0000000000..76c455c7b4 --- /dev/null +++ b/packages/files-ui/cypress/support/page-objects/toasts/sharingProgressToast.ts @@ -0,0 +1,4 @@ +export const sharingProgressToast = { + body: () => cy.get("[data-testid=toast-sharing-progress]", { timeout: 10000 }), + closeButton: () => cy.get("[data-testid=button-close-toast-sharing-progress]") +} \ No newline at end of file diff --git a/packages/files-ui/cypress/support/utils/apiTestHelper.ts b/packages/files-ui/cypress/support/utils/apiTestHelper.ts index d81765cf99..2d848a892f 100644 --- a/packages/files-ui/cypress/support/utils/apiTestHelper.ts +++ b/packages/files-ui/cypress/support/utils/apiTestHelper.ts @@ -4,7 +4,7 @@ import { BucketType } from "@chainsafe/files-api-client" import { navigationMenu } from "../page-objects/navigationMenu" import { homePage } from "../page-objects/homePage" -const API_BASE_URL = "https://stage.imploy.site/api/v1" +const API_BASE_URL = "https://stage-api.chainsafe.io/api/v1" const REFRESH_TOKEN_KEY = "csf.refreshToken" const FREE_PLAN_ID = "prod_JwRu6Ph25b1f2O" @@ -55,10 +55,10 @@ export const apiTestHelper = { } resolve() - } catch (e){ - cy.log("Something wrong happened during the subscription cancelation") - console.log(e) + } catch (e: any){ + console.error(e) reject(e) + throw new Error("Something wrong happened during the subscription cancelation") } }) }) @@ -132,6 +132,7 @@ export const apiTestHelper = { } catch(e){ console.error(e) reject(e) + throw new Error("Something wrong happened when creating a folder") } navigationMenu.binNavButton().click() @@ -141,7 +142,5 @@ export const apiTestHelper = { homePage.fileItemName().contains(firstFolderName) }) }) - - } } diff --git a/packages/files-ui/cypress/tests/file-management-spec.ts b/packages/files-ui/cypress/tests/file-management-spec.ts index 183e19ebc8..331bcbc4fd 100644 --- a/packages/files-ui/cypress/tests/file-management-spec.ts +++ b/packages/files-ui/cypress/tests/file-management-spec.ts @@ -50,7 +50,7 @@ describe("File management", () => { it("can move a file in and out of a folder", () => { cy.web3Login({ clearCSFBucket: true }) - // upload a file and save it's name as a cypress alias + // upload a file and save its name as a cypress alias homePage.uploadFile("../fixtures/uploadedFiles/text-file.txt") homePage.fileItemRow().should("have.length", 1) homePage.fileItemName().invoke("text").as("fileName") @@ -58,9 +58,9 @@ describe("File management", () => { // create a folder apiTestHelper.createFolder(folderPath) - cy.get("@fileName").then(($fileName) => { + cy.get("@fileName").then((fileName) => { // select the file and move it to the folder - homePage.fileItemName().contains(`${$fileName}`) + homePage.fileItemName().contains(fileName) .should("be.visible") .click() homePage.moveSelectedButton().click() @@ -79,10 +79,10 @@ describe("File management", () => { .should("be.visible") .dblclick() homePage.fileItemRow().should("have.length", 1) - homePage.fileItemName().should("contain.text", $fileName) + homePage.fileItemName().should("contain.text", fileName) // move the file back to the home root - homePage.fileItemName().contains(`${$fileName}`) + homePage.fileItemName().contains(fileName) .should("be.visible") .click() homePage.moveSelectedButton().click() @@ -98,10 +98,10 @@ describe("File management", () => { .should("be.visible") .should("have.length", 2) homePage.fileItemName().should("contain.text", folderName) - homePage.fileItemName().should("contain.text", $fileName) + homePage.fileItemName().should("contain.text", fileName) // ensure file already in the root cannot be moved to Home - homePage.fileItemName().contains(`${$fileName}`) + homePage.fileItemName().contains(fileName) .should("be.visible") .click() homePage.moveSelectedButton().click() @@ -259,8 +259,8 @@ describe("File management", () => { binPage.fileItemName().invoke("text").as("binFile") // ensure file in bin matches the name of the deleted file - cy.get("@originalFile").then(($originalFile) => { - cy.get("@binFile").should("equals", $originalFile) + cy.get("@originalFile").then((originalFile) => { + cy.get("@binFile").should("equals", originalFile) }) // recover the file via the menu option @@ -281,8 +281,8 @@ describe("File management", () => { binPage.fileItemRow().should("have.length", 1) // ensure file moved from the bin matches the name of the recovered file - cy.get("@recoveredFile").then(($recoveredFile) => { - cy.get("@binFile").should("equals", $recoveredFile) + cy.get("@recoveredFile").then((recoveredFile) => { + cy.get("@binFile").should("equals", recoveredFile) }) // permanently delete the file @@ -437,12 +437,12 @@ describe("File management", () => { // return home and ensure both of the files were recovered navigationMenu.homeNavButton().click() - cy.get("@fileNameA").then(($fileNameA) => { - homePage.fileItemName().should("contain.text", $fileNameA) + cy.get("@fileNameA").then((fileNameA) => { + homePage.fileItemName().should("contain.text", fileNameA) }) - cy.get("@fileNameB").then(($fileNameB) => { - homePage.fileItemName().should("contain.text", $fileNameB) + cy.get("@fileNameB").then((fileNameB) => { + homePage.fileItemName().should("contain.text", fileNameB) }) }) }) diff --git a/packages/files-ui/cypress/tests/file-preview-spec.ts b/packages/files-ui/cypress/tests/file-preview-spec.ts index 2dc4696353..b19486c934 100644 --- a/packages/files-ui/cypress/tests/file-preview-spec.ts +++ b/packages/files-ui/cypress/tests/file-preview-spec.ts @@ -23,24 +23,24 @@ describe("File Preview", () => { previewModal.body().should("exist") // ensure the correct file is being previewed - cy.get("@fileNameA").then(($fileNameA) => { - previewModal.fileNameLabel().should("contain.text", $fileNameA) + cy.get("@fileNameA").then((fileNameA) => { + previewModal.fileNameLabel().should("contain.text", fileNameA) previewModal.contentContainer().should("be.visible") previewModal.unsupportedFileLabel().should("not.exist") }) // browse to the next file previewModal.nextFileButton().click() - cy.get("@fileNameB").then(($fileNameB) => { - previewModal.fileNameLabel().should("contain.text", $fileNameB) + cy.get("@fileNameB").then((fileNameB) => { + previewModal.fileNameLabel().should("contain.text", fileNameB) previewModal.contentContainer().should("be.visible") previewModal.unsupportedFileLabel().should("not.exist") }) // return to the previous file previewModal.previousFileButton().click() - cy.get("@fileNameA").then(($fileNameA) => { - previewModal.fileNameLabel().should("contain.text", $fileNameA) + cy.get("@fileNameA").then((fileNameA) => { + previewModal.fileNameLabel().should("contain.text", fileNameA) previewModal.contentContainer().should("be.visible") previewModal.unsupportedFileLabel().should("not.exist") }) @@ -71,8 +71,8 @@ describe("File Preview", () => { previewModal.body().should("exist") // ensure the correct file is being previewed - cy.get("@fileNameA").then(($fileNameA) => { - previewModal.fileNameLabel().should("contain.text", $fileNameA) + cy.get("@fileNameA").then((fileNameA) => { + previewModal.fileNameLabel().should("contain.text", fileNameA) previewModal.contentContainer() .should("be.visible") .should("not.have.text", "Loading preview") @@ -81,8 +81,8 @@ describe("File Preview", () => { // browse to the 2nd file via the right arrow key cy.get("body").type("{rightarrow}") - cy.get("@fileNameB").then(($fileNameB) => { - previewModal.fileNameLabel().should("contain.text", $fileNameB) + cy.get("@fileNameB").then((fileNameB) => { + previewModal.fileNameLabel().should("contain.text", fileNameB) previewModal.contentContainer() .should("be.visible") .should("not.have.text", "Loading preview") @@ -91,8 +91,8 @@ describe("File Preview", () => { // browse to the 3rd file via the right arrow key cy.get("body").type("{rightarrow}") - cy.get("@fileNameC").then(($fileNameC) => { - previewModal.fileNameLabel().should("contain.text", $fileNameC) + cy.get("@fileNameC").then((fileNameC) => { + previewModal.fileNameLabel().should("contain.text", fileNameC) previewModal.contentContainer() .should("be.visible") .should("not.have.text", "Loading preview") @@ -101,8 +101,8 @@ describe("File Preview", () => { // return to the 2nd file via the left arrow key cy.get("body").type("{leftarrow}") - cy.get("@fileNameB").then(($fileNameB) => { - previewModal.fileNameLabel().should("contain.text", $fileNameB) + cy.get("@fileNameB").then((fileNameB) => { + previewModal.fileNameLabel().should("contain.text", fileNameB) previewModal.contentContainer() .should("be.visible") .should("not.have.text", "Loading preview") @@ -111,8 +111,8 @@ describe("File Preview", () => { // return to the 1st file via the left arrow key cy.get("body").type("{leftarrow}") - cy.get("@fileNameA").then(($fileNameA) => { - previewModal.fileNameLabel().should("contain.text", $fileNameA) + cy.get("@fileNameA").then((fileNameA) => { + previewModal.fileNameLabel().should("contain.text", fileNameA) previewModal.contentContainer() .should("be.visible") .should("not.have.text", "Loading preview") diff --git a/packages/files-ui/cypress/tests/file-sharing-spec.ts b/packages/files-ui/cypress/tests/file-sharing-spec.ts index 27505dad31..0174734a9e 100644 --- a/packages/files-ui/cypress/tests/file-sharing-spec.ts +++ b/packages/files-ui/cypress/tests/file-sharing-spec.ts @@ -1,6 +1,7 @@ import { createSharedFolderModal } from "../support/page-objects/modals/createSharedFolderModal" import { deleteSharedFolderModal } from "../support/page-objects/modals/deleteSharedFolderModal" import { fileUploadModal } from "../support/page-objects/modals/fileUploadModal" +import { homePage } from "../support/page-objects/homePage" import { navigationMenu } from "../support/page-objects/navigationMenu" import { sharedFolderName, sharedFolderEditedName } from "../fixtures/filesTestData" import { sharingExplainerKey, validUsernameA, validEthAddress, validShareKey } from "../fixtures/filesTestData" @@ -10,6 +11,9 @@ import { viewOnlyShareLink } from "../fixtures/linkData" import { leaveSharedFolderModal } from "../support/page-objects/modals/leaveSharedFolderModal" import { linkSharingConfirmation } from "../support/page-objects/linkSharingConfirmation" import { editSharedFolderModal } from "../support/page-objects/modals/editSharedFolderModal" +import { shareFileModal } from "../support/page-objects/modals/shareFileModal" +import { sharingProgressToast } from "../support/page-objects/toasts/sharingProgressToast" +import { shareSuccessToast } from "../support/page-objects/toasts/shareSuccessToast" describe("File Sharing", () => { @@ -166,5 +170,152 @@ describe("File Sharing", () => { editSharedFolderModal.body().should("be.visible") editSharedFolderModal.userPermissionDropDown().should("contain.text", "edit") }) + + it("can copy a file to a new shared folder and preserve original", () => { + cy.web3Login({ deleteShareBucket: true, clearCSFBucket: true }) + + // upload a file and store its name as cypress alias + homePage.uploadFile("../fixtures/uploadedFiles/logo.png") + homePage.fileItemName().invoke("text").as("fileName") + + // share file to newly created shared folder + homePage.fileItemKebabButton().click() + homePage.shareMenuOption().click() + shareFileModal.body().should("be.visible") + shareFileModal.shareNameInput().type(sharedFolderName) + shareFileModal.copyOverButton().click() + sharingProgressToast.body().should("be.visible") + shareSuccessToast.body().should("be.visible") + shareSuccessToast.closeButton().click() + editSharedFolderModal.closeButton().click() + + // ensure file remained in drive after copying + homePage.fileItemRow().should("have.length", 1) + + // go to the new share and ensure file was copied there + navigationMenu.sharedNavButton().click() + sharedPage.sharedFolderItemName().contains(sharedFolderName) + .should("be.visible") + .dblclick() + sharedPage.fileItemRow().should("have.length", 1) + + // ensure file name of copied file is correct + cy.get("@fileName").then((fileName) => { + sharedPage.fileItemName().contains(fileName).should("be.visible") + }) + }) + + it("can move a file to a new shared folder and delete original", () => { + cy.web3Login({ deleteShareBucket: true, clearCSFBucket: true }) + + // upload a file and store its name as cypress alias + homePage.uploadFile("../fixtures/uploadedFiles/logo.png") + homePage.fileItemName().invoke("text").as("fileName") + + // share file to newly created shared folder + homePage.fileItemKebabButton().click() + homePage.shareMenuOption().click() + shareFileModal.body().should("be.visible") + shareFileModal.shareNameInput().type(sharedFolderName) + // click to unmark checkbox, selected by default + shareFileModal.keepOriginalFilesCheckbox().click() + shareFileModal.moveOverButton().click() + sharingProgressToast.body().should("be.visible") + shareSuccessToast.body().should("be.visible") + shareSuccessToast.closeButton().click() + editSharedFolderModal.closeButton().click() + + // ensure the file did not remain in the drive after moving + homePage.fileItemRow().should("have.length", 0) + + // go to the new share and ensure the file was moved there + navigationMenu.sharedNavButton().click() + sharedPage.sharedFolderItemName().contains(sharedFolderName) + .should("be.visible") + .dblclick() + sharedPage.fileItemRow().should("have.length", 1) + + // ensure file name of moved file is correct + cy.get("@fileName").then((fileName) => { + sharedPage.fileItemName().contains(fileName).should("be.visible") + }) + }) + + it("can copy a file to a pre-existing shared folder and preserve original", () => { + cy.web3Login({ deleteShareBucket: true, clearCSFBucket: true }) + + navigationMenu.sharedNavButton().click() + sharedPage.createSharedFolder() + + // upload a file and store its name as cypress alias + navigationMenu.homeNavButton().click() + homePage.uploadFile("../fixtures/uploadedFiles/logo.png") + homePage.fileItemName().invoke("text").as("fileName") + + // share file to newly created shared folder + homePage.fileItemKebabButton().click() + homePage.shareMenuOption().click() + shareFileModal.body().should("be.visible") + shareFileModal.selectFolderInput().click() + shareFileModal.selectFolderInput().type(`${sharedFolderName}{enter}`) + shareFileModal.copyOverButton().click() + sharingProgressToast.body().should("be.visible") + shareSuccessToast.body().should("be.visible") + shareSuccessToast.closeButton().click() + + // ensure file remained in drive after copying + homePage.fileItemRow().should("have.length", 1) + + // go to the new share and ensure file was copied there + navigationMenu.sharedNavButton().click() + sharedPage.sharedFolderItemName().contains(sharedFolderName) + .should("be.visible") + .dblclick() + sharedPage.fileItemRow().should("have.length", 1) + + // ensure file name of copied file is correct + cy.get("@fileName").then((fileName) => { + sharedPage.fileItemName().contains(fileName).should("be.visible") + }) + }) + + it("can move a file to a pre-existing shared folder and delete original", () => { + cy.web3Login({ deleteShareBucket: true, clearCSFBucket: true }) + + navigationMenu.sharedNavButton().click() + sharedPage.createSharedFolder() + + // upload a file and store its name as cypress alias + navigationMenu.homeNavButton().click() + homePage.uploadFile("../fixtures/uploadedFiles/logo.png") + homePage.fileItemName().invoke("text").as("fileName") + + // share file to newly created shared folder + homePage.fileItemKebabButton().click() + homePage.shareMenuOption().click() + shareFileModal.body().should("be.visible") + shareFileModal.selectFolderInput().click() + shareFileModal.selectFolderInput().type(`${sharedFolderName}{enter}`) + shareFileModal.keepOriginalFilesCheckbox().click() + shareFileModal.moveOverButton().click() + sharingProgressToast.body().should("be.visible") + shareSuccessToast.body().should("be.visible") + shareSuccessToast.closeButton().click() + + // ensure file was deleted from drive after moving + homePage.fileItemRow().should("have.length", 0) + + // go to the new share and ensure file was moved there + navigationMenu.sharedNavButton().click() + sharedPage.sharedFolderItemName().contains(sharedFolderName) + .should("be.visible") + .dblclick() + sharedPage.fileItemRow().should("have.length", 1) + + // ensure file name of moved file is correct + cy.get("@fileName").then((fileName) => { + sharedPage.fileItemName().contains(fileName).should("be.visible") + }) + }) }) }) diff --git a/packages/files-ui/cypress/tests/notifications-spec.ts b/packages/files-ui/cypress/tests/notifications-spec.ts new file mode 100644 index 0000000000..d9679f5b26 --- /dev/null +++ b/packages/files-ui/cypress/tests/notifications-spec.ts @@ -0,0 +1,88 @@ +import { homePage } from "../support/page-objects/homePage" +import { navigationMenu } from "../support/page-objects/navigationMenu" +import { settingsPage } from "../support/page-objects/settingsPage" +import { selectPlanModal } from "../support/page-objects/modals/billing/selectPlanModal" +import { planDetailsModal } from "../support/page-objects/modals/billing/planDetailsModal" +import { selectPaymentMethodModal } from "../support/page-objects/modals/billing/selectPaymentMethodModal" +import { planChangeConfirmationModal } from "../support/page-objects/modals/billing/planChangeConfirmationModal" +import { cryptoPaymentModal } from "../support/page-objects/modals/billing/cryptoPaymentModal" + +describe("Notifications", () => { + beforeEach(() => { + cy.intercept("GET", "**/billing/eligibilities", { + body: { is_eligible: true } + }) + }) + context("desktop", () => { + + it("can see and interact with a notification when a card is expiring soon", () => { + cy.web3Login({ deleteCreditCard: true, resetToFreePlan: true }) + + // upgrade subscription with a card expiring this month + navigationMenu.settingsNavButton().click() + settingsPage.upgradeSubscription({ plan: "max", isCardExpiring: true }) + + // access and inspect notification menu + settingsPage.notificationButton().click() + settingsPage.notificationsHeader().should("be.visible") + settingsPage.notificationsThisWeekHeader().should("be.visible") + settingsPage.notificationsOlderHeader().should("not.exist") + settingsPage.notificationContainer().should("have.length", 1) + + // ensure individual notification has title and date + settingsPage.notificationContainer().within(() => { + settingsPage.notificationTitle().should("be.visible") + settingsPage.notificationTime().should("be.visible") + }) + + // click notification button to dismiss + settingsPage.notificationButton().click() + settingsPage.notificationContainer().should("not.be.visible") + + // navigate away and return to plan page from notification + navigationMenu.homeNavButton().click() + cy.url().should("include", "/drive") + homePage.notificationButton().click() + homePage.notificationContainer().click() + cy.url().should("include", "/settings/plan") + }) + + it("can see and interact with a notification when crypto invoice is open", () => { + cy.web3Login({ withNewSession: true }) + navigationMenu.settingsNavButton().click() + + // initiate crypto payment then exit upgrade flow + settingsPage.subscriptionTabButton().click() + settingsPage.changePlanButton().click() + selectPlanModal.createPlanCypressAliases() + cy.get("@filesProBox").parent().within(() => { + selectPlanModal.selectPlanButton().click() + }) + planDetailsModal.durationToggleSwitch().click() + planDetailsModal.selectThisPlanButton().click() + selectPaymentMethodModal.cryptoRadioInput() + .should("be.visible") + .click() + selectPaymentMethodModal.selectPaymentButton().click() + planChangeConfirmationModal.confirmPlanChangeButton().click() + cryptoPaymentModal.closeButton().click() + + // access and inspect notification menu + settingsPage.notificationButton().click() + settingsPage.notificationContainer().should("have.length", 1) + + // ensure individual notification has title and date + settingsPage.notificationContainer().within(() => { + settingsPage.notificationTitle().should("be.visible") + settingsPage.notificationTime().should("be.visible") + }) + + // navigate away and return to the plan page from notification + navigationMenu.homeNavButton().click() + cy.url().should("include", "/drive") + homePage.notificationButton().click() + homePage.notificationContainer().click() + cy.url().should("include", "/settings/plan") + }) + }) +}) \ No newline at end of file diff --git a/packages/files-ui/cypress/tests/search-spec.ts b/packages/files-ui/cypress/tests/search-spec.ts index 39582a193e..46962ba8fe 100644 --- a/packages/files-ui/cypress/tests/search-spec.ts +++ b/packages/files-ui/cypress/tests/search-spec.ts @@ -37,12 +37,12 @@ describe("Search", () => { cy.go("back") // search for a specific file, ensure only 1 result is found - cy.get("@fileName").then(($fileName) => { - homePage.searchInput().type(`{selectall}{del}${$fileName}{enter}`) + cy.get("@fileName").then((fileName) => { + homePage.searchInput().type(`{selectall}{del}${fileName}{enter}`) searchPage.fileItemRow() .should("be.visible") .should("have.length", 1) - searchPage.fileItemName().should("contain.text", $fileName) + searchPage.fileItemName().should("contain.text", fileName) }) }) diff --git a/packages/files-ui/cypress/tests/subscription-plan-spec.ts b/packages/files-ui/cypress/tests/subscription-plan-spec.ts index 7a0a15dc8c..a23c2bae7b 100644 --- a/packages/files-ui/cypress/tests/subscription-plan-spec.ts +++ b/packages/files-ui/cypress/tests/subscription-plan-spec.ts @@ -79,8 +79,8 @@ describe("Subscription Plan", () => { settingsPage.defaultCardLabel().invoke("text").as("partialMaskedMastercard") // ensure the card number was updated by comparing cypress aliases - cy.get("@partialMaskedVisa").then(($partialMaskedVisa) => { - cy.get("@partialMaskedMastercard").should("not.equal", $partialMaskedVisa) + cy.get("@partialMaskedVisa").then((partialMaskedVisa) => { + cy.get("@partialMaskedMastercard").should("not.equal", partialMaskedVisa) }) // remove the card @@ -219,7 +219,7 @@ describe("Subscription Plan", () => { // upgrade to a Max plan first navigationMenu.settingsNavButton().click() - settingsPage.upgradeSubscription("max") + settingsPage.upgradeSubscription({ plan: "max" }) // setup intercepter, stub the used products response to disallow update cy.intercept("GET", "**/billing/products", (req) => { @@ -262,8 +262,8 @@ describe("Subscription Plan", () => { planDetailsModal.totalCostLabel().invoke("text").as("yearlyBillingPrice") // price should update when switching to annual billing - cy.get("@monthlyBillingPrice").then(($monthlyBillingPrice) => { - cy.get("@yearlyBillingPrice").should("not.equal", $monthlyBillingPrice) + cy.get("@monthlyBillingPrice").then((monthlyBillingPrice) => { + cy.get("@yearlyBillingPrice").should("not.equal", monthlyBillingPrice) }) }) @@ -401,7 +401,7 @@ describe("Subscription Plan", () => { // upgrade to a max plan first using convenience function navigationMenu.settingsNavButton().click() - settingsPage.upgradeSubscription("max") + settingsPage.upgradeSubscription({ plan: "max" }) // store the upgraded plan name for later comparison settingsPage.planNameLabel() @@ -442,8 +442,8 @@ describe("Subscription Plan", () => { .invoke("text").as("proPlanName") // ensure the downgraded plan name is not the same as the previously upgraded plan - cy.get("@premiumPlanName").then(($premiumPlanName) => { - cy.get("@proPlanName").should("not.equal", $premiumPlanName) + cy.get("@premiumPlanName").then((premiumPlanName) => { + cy.get("@proPlanName").should("not.equal", premiumPlanName) }) }) @@ -452,7 +452,7 @@ describe("Subscription Plan", () => { // upgrade to a Pro plan first navigationMenu.settingsNavButton().click() - settingsPage.upgradeSubscription("pro") + settingsPage.upgradeSubscription({ plan: "pro" }) // store the Pro plan name for later comparison settingsPage.planNameLabel() @@ -486,13 +486,13 @@ describe("Subscription Plan", () => { .invoke("text").as("freePlanName") // ensure the downgraded plan name is not the same as the previously upgraded plan - cy.get("@proPlanName").then(($standardPlanName) => { - cy.get("@freePlanName").should("not.equal", $standardPlanName) + cy.get("@proPlanName").then((standardPlanName) => { + cy.get("@freePlanName").should("not.equal", standardPlanName) }) }) it("can initiate and return to a crypto payment flow within 60 minutes", () => { - cy.web3Login({ deleteCreditCard: true, resetToFreePlan: true }) + cy.web3Login({ deleteCreditCard: true, withNewSession: true }) navigationMenu.settingsNavButton().click() settingsPage.subscriptionTabButton().click() settingsPage.changePlanButton().click() diff --git a/packages/files-ui/cypress/tests/survey-banner-spec.ts b/packages/files-ui/cypress/tests/survey-banner-spec.ts index eb9c08a074..6e02ec0d6d 100644 --- a/packages/files-ui/cypress/tests/survey-banner-spec.ts +++ b/packages/files-ui/cypress/tests/survey-banner-spec.ts @@ -47,6 +47,12 @@ describe("Survey Banner", () => { }) it("User should see banner if account age is greater than 7 days and api response is empty", () => { + cy.intercept("GET", "**/user/profile", (req) => { + req.on("response", (res) => { + res.body.created_at = profileCreatedDate + }) + }) + cy.intercept("GET", "**/user/store", { body: {} }) diff --git a/packages/files-ui/cypress/tsconfig.json b/packages/files-ui/cypress/tsconfig.json index c58896952f..a591bc253d 100644 --- a/packages/files-ui/cypress/tsconfig.json +++ b/packages/files-ui/cypress/tsconfig.json @@ -4,7 +4,8 @@ "target": "es5", "jsx": "react", "lib": ["es5", "dom"], - "types": ["cypress", "cypress-file-upload"] + "types": ["cypress", "cypress-file-upload"], + "esModuleInterop": true, }, "include": ["**/*.ts"] } \ No newline at end of file diff --git a/packages/files-ui/package.json b/packages/files-ui/package.json index bd4d672d64..bfb3f33509 100644 --- a/packages/files-ui/package.json +++ b/packages/files-ui/package.json @@ -6,7 +6,7 @@ "@babel/core": "^7.12.10", "@babel/runtime": "^7.0.0", "@chainsafe/browser-storage-hooks": "^1.0.1", - "@chainsafe/files-api-client": "1.18.29", + "@chainsafe/files-api-client": "1.18.34", "@chainsafe/web3-context": "1.3.1", "@emeraldpay/hashicon-react": "^0.5.1", "@lingui/core": "^3.7.2", @@ -18,10 +18,10 @@ "@tkey/default": "5.1.0", "@tkey/security-questions": "5.1.0", "@tkey/web-storage": "5.1.0", - "@toruslabs/customauth":"7.0.2", + "@toruslabs/customauth": "7.0.2", "@types/filesystem": "^0.0.32", "@types/uuid": "^8.3.0", - "axios": "0.21.4", + "axios": "0.26.1", "babel-loader": "8.1.0", "babel-plugin-macros": "^2.8.0", "babel-preset-env": "^1.7.0", @@ -108,4 +108,4 @@ "last 1 safari version" ] } -} +} \ No newline at end of file diff --git a/packages/files-ui/src/App.tsx b/packages/files-ui/src/App.tsx index 9d3f3c4e1e..8373ea0c94 100644 --- a/packages/files-ui/src/App.tsx +++ b/packages/files-ui/src/App.tsx @@ -18,6 +18,8 @@ import { BillingProvider } from "./Contexts/BillingContext" import { PosthogProvider } from "./Contexts/PosthogContext" import { NotificationsProvider } from "./Contexts/NotificationsContext" import { StylesProvider, createGenerateClassName } from "@material-ui/styles" +import { HelmetProvider } from "react-helmet-async" + import ErrorModal from "./Components/Modules/ErrorModal" // making material and jss use one className generator @@ -74,68 +76,70 @@ const onboardConfig = { const App = () => { const { canUseLocalStorage } = useLocalStorage() - const apiUrl = process.env.REACT_APP_API_URL || "https://stage.imploy.site/api/v1" + const apiUrl = process.env.REACT_APP_API_URL || "https://stage-api.chainsafe.io/api/v1" // This will default to testnet unless mainnet is specifically set in the ENV const directAuthNetwork = (process.env.REACT_APP_DIRECT_AUTH_NETWORK === "mainnet") ? "mainnet" : "testnet" return ( - - - window.location.reload()} + + + - - - - window.location.reload()} + > + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/packages/files-ui/src/Components/Elements/Notifications/NotificationLine.tsx b/packages/files-ui/src/Components/Elements/Notifications/NotificationLine.tsx index f403808def..73d4366b5b 100644 --- a/packages/files-ui/src/Components/Elements/Notifications/NotificationLine.tsx +++ b/packages/files-ui/src/Components/Elements/Notifications/NotificationLine.tsx @@ -39,11 +39,13 @@ const NotificationLine = ({ notification }: Props) => { return
{notification.title} @@ -51,6 +53,7 @@ const NotificationLine = ({ notification }: Props) => { variant="body2" className={classes.notificationTime} component="p" + data-cy="label-notification-time" > {dayjs.unix(notification.createdAt).fromNow()} diff --git a/packages/files-ui/src/Components/Elements/Notifications/NotificationList.tsx b/packages/files-ui/src/Components/Elements/Notifications/NotificationList.tsx index 3d80a7e006..e64ec2357a 100644 --- a/packages/files-ui/src/Components/Elements/Notifications/NotificationList.tsx +++ b/packages/files-ui/src/Components/Elements/Notifications/NotificationList.tsx @@ -78,6 +78,7 @@ const NotificationList = ({ notifications }: INotificationListProps) => { Notifications @@ -88,6 +89,7 @@ const NotificationList = ({ notifications }: INotificationListProps) => { variant="h5" component="p" className={classes.timeHeader} + data-cy="label-notifications-this-week" > This week @@ -102,6 +104,7 @@ const NotificationList = ({ notifications }: INotificationListProps) => { variant="h5" component="p" className={classes.timeHeader} + data-cy="label-notifications-older" > Older notifications diff --git a/packages/files-ui/src/Components/Elements/Notifications/NotificationsDropdown.tsx b/packages/files-ui/src/Components/Elements/Notifications/NotificationsDropdown.tsx index f9f3ba2bde..61485236f7 100644 --- a/packages/files-ui/src/Components/Elements/Notifications/NotificationsDropdown.tsx +++ b/packages/files-ui/src/Components/Elements/Notifications/NotificationsDropdown.tsx @@ -93,6 +93,7 @@ const NotificationsDropdown = () => { autoclose classNames={{ options: classes.optionsOpen }} onClose={() => setIsActive(false)} + testId="notifications" >
{ exact path={ROUTE_LINKS.SharedFolders} isAuthorized={isAuthorized} - component={SharedFoldersOverview} + component={SharedFoldersPage} redirectPath={ROUTE_LINKS.Landing} /> void } +type AppNavTab = "home" | "shared" | "bin" | "settings" + const AppNav = ({ navOpen, setNavOpen }: IAppNav) => { const { desktop } = useThemeSwitcher() const classes = useStyles() @@ -252,6 +259,7 @@ const AppNav = ({ navOpen, setNavOpen }: IAppNav) => { const { isLoggedIn, secured } = useFilesApi() const { publicKey, isNewDevice, shouldInitializeAccount, logout } = useThresholdKey() const { removeUser, getProfileTitle, profile } = useUser() + const location = useLocation() const signOut = useCallback(() => { logout() @@ -266,6 +274,18 @@ const AppNav = ({ navOpen, setNavOpen }: IAppNav) => { const profileTitle = getProfileTitle() + const appNavTab: AppNavTab | undefined = useMemo(() => { + const firstPathParam = location.pathname.split("/")[1] + switch(firstPathParam) { + case "drive": return "home" + case "shared": return "shared" + case "shared-overview": return "shared" + case "bin": return "bin" + case "settings": return "settings" + default: return + } + }, [location]) + return (
{ Home @@ -355,13 +374,12 @@ const AppNav = ({ navOpen, setNavOpen }: IAppNav) => { Shared @@ -369,13 +387,12 @@ const AppNav = ({ navOpen, setNavOpen }: IAppNav) => { Bin @@ -388,13 +405,12 @@ const AppNav = ({ navOpen, setNavOpen }: IAppNav) => { Settings diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/CSFFileBrowser.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/CSFFileBrowser.tsx index 6d0847287a..5e49c928bd 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/CSFFileBrowser.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/CSFFileBrowser.tsx @@ -24,9 +24,10 @@ import { DISMISSED_SURVEY_KEY } from "../../SurveyBanner" import { FileBrowserContext } from "../../../Contexts/FileBrowserContext" import { parseFileContentResponse } from "../../../Utils/Helpers" import getFilesFromDataTransferItems from "../../../Utils/getFilesFromDataTransferItems" +import { Helmet } from "react-helmet-async" const CSFFileBrowser: React.FC = () => { - const { downloadFile, uploadFiles, buckets } = useFiles() + const { downloadFile, uploadFiles, buckets, storageSummary } = useFiles() const { accountRestricted } = useFilesApi() const { filesApiClient } = useFilesApi() const { addToast } = useToasts() @@ -106,7 +107,7 @@ const CSFFileBrowser: React.FC = () => { const itemToRename = pathContents.find(i => i.cid === cid) if (!bucket || !itemToRename) return - filesApiClient.moveBucketObjects(bucket.id, { + return filesApiClient.moveBucketObjects(bucket.id, { paths: [getPathWithFile(currentPath, itemToRename.name)], new_path: getPathWithFile(currentPath, newName) }) .then(() => refreshContents()) @@ -116,7 +117,6 @@ const CSFFileBrowser: React.FC = () => { const moveItems = useCallback(async (cids: string[], newPath: string) => { if (!bucket) return - const pathsToMove = getAbsolutePathsFromCids(cids, currentPath, pathContents) filesApiClient.moveBucketObjects(bucket.id, { @@ -157,6 +157,10 @@ const CSFFileBrowser: React.FC = () => { path: joinArrayOfPaths(arrayOfPaths.slice(0, index + 1)) }}), [arrayOfPaths, redirect]) + const currentFolder = useMemo(() => { + return !!arrayOfPaths.length && arrayOfPaths[arrayOfPaths.length - 1] + }, [arrayOfPaths]) + const handleUploadOnDrop = useCallback(async (files: File[], fileItems: DataTransferItemList, path: string) => { if (!bucket) return @@ -164,17 +168,28 @@ const CSFFileBrowser: React.FC = () => { addToast({ type:"error", title: t`Uploads disabled`, - subtitle: t`Oops! You need to pay for this month to upload more content.` + subtitle: t`Your account is restricted. Until you've settled up, you can't upload any new content.` }) return } + const availableStorage = storageSummary?.available_storage || 0 + const uploadSize = files?.reduce((total: number, file: File) => total += file.size, 0) || 0 + + if (uploadSize > availableStorage) { + addToast({ + type: "error", + title: t`Upload size exceeds plan capacity`, + subtitle: t`Please select fewer files to upload` + }) + return + } const flattenedFiles = await getFilesFromDataTransferItems(fileItems) const paths = [...new Set(flattenedFiles.map(f => f.filepath))] paths.forEach(p => { uploadFiles(bucket, flattenedFiles.filter(f => f.filepath === p), getPathWithFile(path, p)) }) - }, [uploadFiles, bucket, accountRestricted, addToast]) + }, [bucket, accountRestricted, storageSummary, addToast, uploadFiles]) const viewFolder = useCallback((cid: string) => { const fileSystemItem = pathContents.find(f => f.cid === cid) @@ -210,7 +225,7 @@ const CSFFileBrowser: React.FC = () => { deleteItems: moveItemsToBin, downloadFile: handleDownload, moveItems, - renameItem: renameItem, + renameItem, viewFolder, handleUploadOnDrop, loadingCurrentPath, @@ -222,6 +237,11 @@ const CSFFileBrowser: React.FC = () => { itemOperations, withSurvey: showSurvey && olderThanOneWeek }}> + {!!currentFolder && + + {currentFolder} - Chainsafe Files + + } diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/ManageSharedFolder.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/ManageSharedFolder.tsx index a868faded4..cf89efa82e 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/ManageSharedFolder.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/ManageSharedFolder.tsx @@ -417,7 +417,7 @@ const ManageSharedFolder = ({ onClose, bucketToEdit }: ICreateOrManageSharedFold size="large" variant="outline" type="button" - data-cy="button-cancel-create-shared-folder" + data-cy="button-close-manage-shared-folder" > Close diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/ShareModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/ShareModal.tsx index 4361700925..b4f410fdf7 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/ShareModal.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/ShareModal.tsx @@ -255,6 +255,7 @@ const ShareModal = ({ onClose, fileSystemItems }: IShareModalProps) => { active={true} closePosition="none" maxWidth={500} + testId="share-file" > {bucketToUpload ? {
{isUsingExistingBucket ? ( -
+
setDestinationBucket(buckets.find((bu) => bu.id === val))} + data-cy="input-select-existing-folder-option" />
) @@ -300,6 +305,7 @@ const ShareModal = ({ onClose, fileSystemItems }: IShareModalProps) => { autoFocus onChange={onNameChange} state={nameError ? "error" : "normal"} + data-cy="input-new-share-name" /> {!!nameError && ( { setKeepOriginalFile(!keepOriginalFile) }} label={t`Keep original files`} + testId="keep-original-files" />
)} @@ -348,6 +355,7 @@ const ShareModal = ({ onClose, fileSystemItems }: IShareModalProps) => { variant="outline" onClick={onClose} className={classes.cancelButton} + testId="cancel-share-file" > Cancel @@ -361,6 +369,7 @@ const ShareModal = ({ onClose, fileSystemItems }: IShareModalProps) => { ? !destinationBucket?.id : !sharedFolderName || !!nameError } + testId={keepOriginalFile ? "copy-over" : "move-over"} > {keepOriginalFile ? Copy over diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFileBrowser.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFileBrowser.tsx index 6ad90d9000..236e03331c 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFileBrowser.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFileBrowser.tsx @@ -23,6 +23,7 @@ import FilesList from "./views/FilesList" import { createStyles, makeStyles } from "@chainsafe/common-theme" import { CSFTheme } from "../../../Themes/types" import getFilesFromDataTransferItems from "../../../Utils/getFilesFromDataTransferItems" +import { Helmet } from "react-helmet-async" const useStyles = makeStyles(({ constants, palette }: CSFTheme) => createStyles({ @@ -43,7 +44,7 @@ const useStyles = makeStyles(({ constants, palette }: CSFTheme) => })) const SharedFileBrowser = () => { - const { downloadFile, uploadFiles, buckets, getStorageSummary, refreshBuckets } = useFiles() + const { downloadFile, uploadFiles, buckets, getStorageSummary, refreshBuckets, storageSummary } = useFiles() const { filesApiClient, accountRestricted } = useFilesApi() const classes = useStyles() const { addToast } = useToasts() @@ -87,6 +88,9 @@ const SharedFileBrowser = () => { } }) }), [arrayOfPaths, bucket, redirect]) + const currentFolder = useMemo(() => { + return !!arrayOfPaths.length && arrayOfPaths[arrayOfPaths.length - 1] + }, [arrayOfPaths]) const refreshContents = useCallback((showLoading?: boolean) => { if (!bucket) return @@ -155,7 +159,7 @@ const SharedFileBrowser = () => { const itemToRename = pathContents.find(i => i.cid === cid) if (!bucket || !itemToRename) return - filesApiClient.moveBucketObjects(bucket.id, { + return filesApiClient.moveBucketObjects(bucket.id, { paths: [getPathWithFile(currentPath, itemToRename.name)], new_path: getPathWithFile(currentPath, newName) }).then(() => refreshContents()) .catch(console.error) @@ -201,11 +205,22 @@ const SharedFileBrowser = () => { const handleUploadOnDrop = useCallback(async (files: File[], fileItems: DataTransferItemList, path: string) => { if (!bucket) return - if (accountRestricted) { + if (accountRestricted && bucket.permission === "owner") { addToast({ type:"error", title: t`Uploads disabled`, - subtitle: t`Oops! You need to pay for this month to upload more content.` + subtitle: t`Your account is restricted. Until you've settled up, you can't upload any new content.` + }) + return + } + const availableStorage = storageSummary?.available_storage || 0 + const uploadSize = files?.reduce((total: number, file: File) => total += file.size, 0) || 0 + + if (bucket.permission === "owner" && uploadSize > availableStorage) { + addToast({ + type: "error", + title: t`Upload size exceeds plan capacity`, + subtitle: t`Please select fewer files to upload` }) return } @@ -214,7 +229,7 @@ const SharedFileBrowser = () => { paths.forEach(p => { uploadFiles(bucket, flattenedFiles.filter(f => f.filepath === p), getPathWithFile(path, p)) }) - }, [uploadFiles, bucket, accountRestricted, addToast]) + }, [bucket, accountRestricted, storageSummary, addToast, uploadFiles]) const bulkOperations: IBulkOperations = useMemo(() => ({ [CONTENT_TYPES.Directory]: ["download", "move", "delete", "share"], @@ -308,6 +323,11 @@ const SharedFileBrowser = () => { itemOperations, withSurvey: false }}> + {(!!currentFolder || bucket.name) && + + {currentFolder || bucket.name} - Chainsafe Files + + } diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFoldersOverview.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFoldersOverview.tsx index 7a12b8ee5e..bc22e609d7 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFoldersOverview.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFoldersOverview.tsx @@ -178,7 +178,7 @@ const SharedFolderOverview = () => { }, [column, direction]) const handleRename = useCallback((bucket: BucketKeyPermission, newName: string) => { - filesApiClient.updateBucket(bucket.id, { + return filesApiClient.updateBucket(bucket.id, { ...bucket, name: newName }).then(() => refreshBuckets(false)) diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/UploadFileModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/UploadFileModal.tsx index 8c633a88e4..3a13b94088 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/UploadFileModal.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/UploadFileModal.tsx @@ -1,8 +1,8 @@ import { Button, FileInput } from "@chainsafe/common-components" import { useFiles } from "../../../Contexts/FilesContext" import { createStyles, makeStyles } from "@chainsafe/common-theme" -import React, { useCallback, useState } from "react" -import { Formik, Form } from "formik" +import React, { useCallback, useMemo, useState } from "react" +import { Form, useFormik, FormikProvider } from "formik" import { array, object } from "yup" import CustomModal from "../../Elements/CustomModal" import { Trans, t } from "@lingui/macro" @@ -83,8 +83,21 @@ const UploadFileModule = ({ modalOpen, close }: IUploadFileModuleProps) => { const [isDoneDisabled, setIsDoneDisabled] = useState(true) const { uploadFiles } = useFiles() const { currentPath, refreshContents, bucket } = useFileBrowser() + const { storageSummary } = useFiles() - const UploadSchema = object().shape({ files: array().required(t`Please select a file to upload`) }) + const UploadSchema = useMemo(() => object().shape({ + files: array().required(t`Please select a file to upload`) + .test("Upload Size", + t`Upload size exceeds plan capacity`, + (files) => { + // no validation if the user isn't the owner + if(bucket?.permission !== "owner") return true + + const availableStorage = storageSummary?.available_storage || 0 + const uploadSize = files?.reduce((total: number, file: File) => total += file.size, 0) || 0 + return uploadSize < availableStorage + }) + }), [bucket, storageSummary]) const onFileNumberChange = useCallback((filesNumber: number) => { setIsDoneDisabled(filesNumber === 0) @@ -106,7 +119,13 @@ const UploadFileModule = ({ modalOpen, close }: IUploadFileModuleProps) => { console.error(error) } helpers.setSubmitting(false) - }, [close, currentPath, uploadFiles, refreshContents, bucket]) + }, [bucket, close, refreshContents, uploadFiles, currentPath]) + + const formik = useFormik({ + initialValues: { files: [] }, + validationSchema: UploadSchema, + onSubmit: onSubmit + }) return ( { inner: classes.modalInner }} > - +
{ type="submit" variant="primary" className={clsx(classes.okButton, "wide")} - disabled={isDoneDisabled} + disabled={isDoneDisabled || !formik.isValid} > Start Upload
-
+
) } diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemGridItem.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemGridItem.tsx index 69191e86ac..adac7f1ab1 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemGridItem.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemGridItem.tsx @@ -1,10 +1,11 @@ -import React, { useCallback, useEffect, useMemo, useRef } from "react" +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { makeStyles, createStyles, useThemeSwitcher, useOnClickOutside, LongPressEvents } from "@chainsafe/common-theme" import { t } from "@lingui/macro" import clsx from "clsx" import { FormikTextInput, IMenuItem, + Loading, MoreIcon, Typography } from "@chainsafe/common-components" @@ -118,6 +119,10 @@ const useStyles = makeStyles(({ breakpoints, constants, palette }: CSFTheme) => }, focusVisible: { backgroundColor: "transparent !important" + }, + loadingIcon: { + marginLeft: constants.generalUnit, + verticalAlign: "middle" } }) }) @@ -133,7 +138,7 @@ interface IFileSystemTableItemProps { icon: React.ReactNode preview: ConnectDragPreview setEditing: (editing: string | undefined) => void - handleRename?: (path: string, newPath: string) => Promise + handleRename?: (path: string, newPath: string) => Promise | undefined currentPath: string | undefined menuItems: IMenuItem[] resetSelectedFiles: () => void @@ -161,11 +166,9 @@ const FileSystemGridItem = React.forwardRef( const { name, cid } = file const { desktop } = useThemeSwitcher() const formRef = useRef(null) + const [isEditingLoading, setIsEditingLoading] = useState(false) - const { - fileName, - extension - } = useMemo(() => { + const { fileName, extension } = useMemo(() => { if (isFolder) { return { fileName : name, @@ -196,8 +199,11 @@ const FileSystemGridItem = React.forwardRef( onSubmit: (values: { name: string }) => { const newName = extension !== "" ? `${values.name.trim()}.${extension}` : values.name.trim() - if (newName !== name) { - newName && handleRename && handleRename(file.cid, newName) + if (newName !== name && !!newName && handleRename) { + setIsEditingLoading(true) + + handleRename(cid, newName) + ?.then(() => setIsEditingLoading(false)) } else { stopEditing() } @@ -232,7 +238,7 @@ const FileSystemGridItem = React.forwardRef( formik.resetForm() }, [formik, setEditing]) - useOnClickOutside(formRef, stopEditing) + useOnClickOutside(formRef, formik.submitForm) return (
{ !isFolder && extension !== "" && ( @@ -291,7 +297,13 @@ const FileSystemGridItem = React.forwardRef( ) - :
{name}
+ :
+ {name}{isEditingLoading && } +
}
diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemItem.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemItem.tsx index 9ba00d687d..1612e8ed54 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemItem.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemItem.tsx @@ -109,7 +109,7 @@ interface IFileSystemItemProps { handleAddToSelectedItems(selectedItems: FileSystemItemType): void editing: string | undefined setEditing(editing: string | undefined): void - handleRename?: (cid: string, newName: string) => Promise + handleRename?: (cid: string, newName: string) => Promise | undefined handleMove?: (cid: string, newPath: string) => Promise deleteFile?: () => void recoverFile?: () => void @@ -524,7 +524,7 @@ const FileSystemItem = ({ className={classes.renameInput} name="name" placeholder={isFolder ? t`Please enter a folder name` : t`Please enter a file name`} - autoFocus={editing === cid} + autoFocus /> { !isFolder && extension !== "" && ( diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemTableItem.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemTableItem.tsx index 8349139346..50df6ac46c 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemTableItem.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemTableItem.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useRef } from "react" +import React, { useCallback, useMemo, useRef, useState } from "react" import { makeStyles, createStyles, useThemeSwitcher, useOnClickOutside, LongPressEvents } from "@chainsafe/common-theme" import { t } from "@lingui/macro" import clsx from "clsx" @@ -7,6 +7,7 @@ import { formatBytes, FormikTextInput, IMenuItem, + Loading, MoreIcon, TableCell, TableRow, @@ -104,6 +105,10 @@ const useStyles = makeStyles(({ breakpoints, constants, palette }: CSFTheme) => }, focusVisible: { backgroundColor: "transparent !important" + }, + loadingIcon: { + marginLeft: constants.generalUnit, + verticalAlign: "middle" } }) }) @@ -114,13 +119,13 @@ interface IFileSystemTableItemProps { isOverUpload: boolean selectedCids: string[] file: FileSystemItem - editing: string | undefined + editing?: string handleAddToSelectedItems: (selected: FileSystemItem) => void onFolderOrFileClicks: (e?: React.MouseEvent) => void icon: React.ReactNode preview: ConnectDragPreview setEditing: (editing: string | undefined) => void - handleRename?: (path: string, newPath: string) => Promise + handleRename?: (path: string, newPath: string) => Promise | undefined currentPath: string | undefined menuItems: IMenuItem[] longPressEvents?: LongPressEvents @@ -147,11 +152,9 @@ const FileSystemTableItem = React.forwardRef( const { name, cid, created_at, size } = file const { desktop } = useThemeSwitcher() const formRef = useRef(null) + const [isEditingLoading, setIsEditingLoading] = useState(false) - const { - fileName, - extension - } = useMemo(() => { + const { fileName, extension } = useMemo(() => { if (isFolder) { return { fileName : name, @@ -178,10 +181,14 @@ const FileSystemTableItem = React.forwardRef( initialValues: { name: fileName }, validationSchema: nameValidator, onSubmit: (values: { name: string }) => { + const newName = extension !== "" ? `${values.name.trim()}.${extension}` : values.name.trim() - if (newName !== name) { - newName && handleRename && handleRename(file.cid, newName) + if (newName !== name && !!newName && handleRename) { + setIsEditingLoading(true) + + handleRename(file.cid, newName) + ?.then(() => setIsEditingLoading(false)) } else { stopEditing() } @@ -194,7 +201,7 @@ const FileSystemTableItem = React.forwardRef( formik.resetForm() }, [formik, setEditing]) - useOnClickOutside(formRef, stopEditing) + useOnClickOutside(formRef, formik.submitForm) return ( !editing && onFolderOrFileClicks(e)} {...longPressEvents} > - {editing === cid && desktop + { editing === cid && desktop ? (
{ !isFolder && extension !== "" && ( @@ -264,14 +271,19 @@ const FileSystemTableItem = React.forwardRef(
) - : {name}} + : <> + {name} + {isEditingLoading && } + } {desktop && ( <> - { - !isFolder && !!created_at && dayjs.unix(created_at).format("DD MMM YYYY h:mm a") - } + {!isFolder && !!created_at && dayjs.unix(created_at).format("DD MMM YYYY h:mm a")} {!isFolder && formatBytes(size, 2)} @@ -296,3 +308,4 @@ const FileSystemTableItem = React.forwardRef( FileSystemTableItem.displayName = "FileSystemTableItem" export default FileSystemTableItem + diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/SharedFolderRow.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/SharedFolderRow.tsx index 66ba0e2dab..52fd715ca4 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/SharedFolderRow.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/SharedFolderRow.tsx @@ -9,6 +9,7 @@ import { formatBytes, FormikTextInput, IMenuItem, + Loading, MoreIcon, TableCell, TableRow, @@ -139,13 +140,17 @@ const useStyles = makeStyles(({ breakpoints, constants, palette }: CSFTheme) => }, okButton: { marginLeft: constants.generalUnit + }, + loadingIcon: { + marginLeft: constants.generalUnit, + verticalAlign: "middle" } }) }) interface Props { bucket: BucketKeyPermission - handleRename: (bucket: BucketKeyPermission, newName: string) => void + handleRename: (bucket: BucketKeyPermission, newName: string) => Promise openSharedFolder: (bucketId: string) => void onEditSharedFolder: () => void handleDeleteSharedFolder: () => void @@ -160,6 +165,7 @@ const SharedFolderRow = ({ bucket, handleRename, openSharedFolder, handleDeleteS const formRef = useRef(null) const isOwner = useMemo(() => bucket.permission === "owner", [bucket.permission]) const [ownerName, setOwnerName] = useState("") + const [isEditingLoading, setIsEditingLoading] = useState(false) useEffect(() => { if (isOwner) { @@ -246,15 +252,20 @@ const SharedFolderRow = ({ bucket, handleRename, openSharedFolder, handleDeleteS } const formik = useFormik({ - initialValues:{ - name - }, + initialValues:{ name }, enableReinitialize: true, validationSchema: nameValidator, onSubmit:(values, { resetForm }) => { const newName = values.name?.trim() - newName && handleRename && handleRename(bucket, newName) + if (newName !== name && !!newName && handleRename) { + setIsEditingLoading(true) + + handleRename(bucket, newName) + .then(() => setIsEditingLoading(false)) + } else { + stopEditing() + } setIsRenaming(false) resetForm() } @@ -265,7 +276,7 @@ const SharedFolderRow = ({ bucket, handleRename, openSharedFolder, handleDeleteS formik.resetForm() }, [formik, setIsRenaming]) - useOnClickOutside(formRef, stopEditing) + useOnClickOutside(formRef, formik.submitForm) return ( <> @@ -290,7 +301,14 @@ const SharedFolderRow = ({ bucket, handleRename, openSharedFolder, handleDeleteS onClick={(e) => onFolderClick(e)} > {!isRenaming || !desktop - ? {name} + ? <> + {name} + {isEditingLoading && } + : (
- {name} + <> + {name} + {isEditingLoading && } + ) } diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FilesList.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FilesList.tsx index 657bdf5f28..f7366a18c4 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FilesList.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FilesList.tsx @@ -895,6 +895,7 @@ const FilesList = ({ isShared = false }: Props) => { onClick={() => setCreateFolderModalOpen(true)} variant="outline" size="large" + disabled={accountRestricted} > @@ -906,6 +907,7 @@ const FilesList = ({ isShared = false }: Props) => { onClick={() => setIsUploadModalOpen(true)} variant="outline" size="large" + disabled={accountRestricted} > @@ -1196,9 +1198,9 @@ const FilesList = ({ isShared = false }: Props) => { handleAddToSelectedItems={handleAddToSelectedItems} editing={editing} setEditing={setEditing} - handleRename={async (cid: string, newName: string) => { - handleRename && (await handleRename(cid, newName)) + handleRename={(cid: string, newName: string) => { setEditing(undefined) + return handleRename && handleRename(cid, newName) }} deleteFile={() => { setSelectedItems([file]) @@ -1255,9 +1257,9 @@ const FilesList = ({ isShared = false }: Props) => { handleAddToSelectedItems={handleAddToSelectedItems} editing={editing} setEditing={setEditing} - handleRename={async (path: string, newPath: string) => { - handleRename && (await handleRename(path, newPath)) + handleRename={(path: string, newPath: string) => { setEditing(undefined) + return handleRename && handleRename(path, newPath) }} deleteFile={() => { setSelectedItems([file]) diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/DowngradeDetails.tsx b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/DowngradeDetails.tsx index 01d4a1a461..cb6a55f282 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/DowngradeDetails.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/DowngradeDetails.tsx @@ -122,7 +122,7 @@ const DowngradeDetails = ({ const { currentSubscription, cancelCurrentSubscription, invoices } = useBilling() const currentStorage = formatBytes(Number(currentSubscription?.product?.price.metadata?.storage_size_bytes), 2) const [isCancelingPlan, setIsCancellingPlan] = useState(false) - const lastInvoicePaymentMethod = invoices && invoices[invoices.length - 1].payment_method + const lastInvoicePaymentMethod = invoices?.length && invoices[invoices.length - 1].payment_method const onCancelPlan = useCallback(() => { setIsCancellingPlan(true) diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PaymentMethodSelector.tsx b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PaymentMethodSelector.tsx index b53e76df7b..c09b21dd81 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PaymentMethodSelector.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PaymentMethodSelector.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react" +import React, { useEffect, useMemo, useState } from "react" import { makeStyles, createStyles } from "@chainsafe/common-theme" import { CSFTheme } from "../../../../../Themes/types" import { Button, Divider, RadioInput, Typography } from "@chainsafe/common-components" @@ -88,6 +88,7 @@ const PaymentMethodSelector = ({ selectedProductPrice, goBack, onSelectPaymentMe const [paymentMethod, setPaymentMethod] = useState<"creditCard" | "crypto" | undefined>() const [isCardFormOpen, setIsCardFormOpen] = useState(false) const { defaultCard } = useBilling() + const isMonthlyPayment = useMemo(() => selectedProductPrice.recurring.interval === "month", [selectedProductPrice]) useEffect(() => { if (defaultCard) { @@ -138,15 +139,23 @@ const PaymentMethodSelector = ({ selectedProductPrice, goBack, onSelectPaymentMe
- setPaymentMethod("crypto")} - checked={paymentMethod === "crypto"} - labelClassName={classes.radioLabel} - disabled={selectedProductPrice.recurring.interval !== "year"} - testId="crypto" - /> +
+ setPaymentMethod("crypto")} + checked={paymentMethod === "crypto"} + labelClassName={classes.radioLabel} + disabled={isMonthlyPayment} + testId="crypto" + /> + {isMonthlyPayment && + Only with annual billing + } +
} {isCardFormOpen &&
diff --git a/packages/files-ui/src/Components/Pages/BillingHistory.tsx b/packages/files-ui/src/Components/Pages/BillingHistory.tsx index a9cfab9fff..5ebf9f36a6 100644 --- a/packages/files-ui/src/Components/Pages/BillingHistory.tsx +++ b/packages/files-ui/src/Components/Pages/BillingHistory.tsx @@ -2,9 +2,10 @@ import React, { useState } from "react" import { makeStyles, createStyles } from "@chainsafe/common-theme" import { CSFTheme } from "../../Themes/types" import { Typography } from "@chainsafe/common-components" -import { Trans } from "@lingui/macro" +import { Trans, t } from "@lingui/macro" import InvoiceLines from "../Elements/InvoiceLines" import PayInvoiceModal from "../Modules/Settings/SubscriptionTab/PayInvoice/PayInvoiceModal" +import { Helmet } from "react-helmet-async" const useStyles = makeStyles( ({ constants, breakpoints }: CSFTheme) => @@ -34,20 +35,25 @@ const BillingHistory = () => { const [invoiceToPay, setInvoiceToPay] = useState() return ( -
- - Billing history - - setInvoiceToPay(invoiceId)} /> - {invoiceToPay && setInvoiceToPay(undefined)} - />} -
+ <> + + {t`Billing history`} - Chainsafe Files + +
+ + Billing history + + setInvoiceToPay(invoiceId)} /> + {invoiceToPay && setInvoiceToPay(undefined)} + />} +
+ ) } diff --git a/packages/files-ui/src/Components/Pages/BinPage.tsx b/packages/files-ui/src/Components/Pages/BinPage.tsx index cb5e01957f..6867823f09 100644 --- a/packages/files-ui/src/Components/Pages/BinPage.tsx +++ b/packages/files-ui/src/Components/Pages/BinPage.tsx @@ -1,11 +1,18 @@ import React from "react" import { usePageTrack } from "../../Contexts/PosthogContext" import BinFileBrowser from "../Modules/FileBrowsers/BinFileBrowser" +import { Helmet } from "react-helmet-async" +import { t } from "@lingui/macro" const BinPage = () => { usePageTrack() - return + return <> + + {t`Bin`} - Chainsafe Files + + + } export default BinPage diff --git a/packages/files-ui/src/Components/Pages/DrivePage.tsx b/packages/files-ui/src/Components/Pages/DrivePage.tsx index 18c87bb322..09dd020cf4 100644 --- a/packages/files-ui/src/Components/Pages/DrivePage.tsx +++ b/packages/files-ui/src/Components/Pages/DrivePage.tsx @@ -1,11 +1,18 @@ import React from "react" import { usePageTrack } from "../../Contexts/PosthogContext" import CSFFileBrowser from "../Modules/FileBrowsers/CSFFileBrowser" +import { Helmet } from "react-helmet-async" +import { t } from "@lingui/macro" const DrivePage = () => { usePageTrack() - return + return <> + + {t`Home`} - Chainsafe Files + + + } export default DrivePage diff --git a/packages/files-ui/src/Components/Pages/LinkSharingLanding.tsx b/packages/files-ui/src/Components/Pages/LinkSharingLanding.tsx index 17a5f585b0..0a30818588 100644 --- a/packages/files-ui/src/Components/Pages/LinkSharingLanding.tsx +++ b/packages/files-ui/src/Components/Pages/LinkSharingLanding.tsx @@ -1,11 +1,18 @@ import React from "react" import { usePageTrack } from "../../Contexts/PosthogContext" import LinkSharingModule from "../Modules/LinkSharingModule" +import { Helmet } from "react-helmet-async" +import { t } from "@lingui/macro" const LinkSharingLanding = () => { usePageTrack() - return + return <> + + {t`Shared link`} - Chainsafe Files + + + } export default LinkSharingLanding diff --git a/packages/files-ui/src/Components/Pages/SearchPage.tsx b/packages/files-ui/src/Components/Pages/SearchPage.tsx index 4bab829acf..31461bcea6 100644 --- a/packages/files-ui/src/Components/Pages/SearchPage.tsx +++ b/packages/files-ui/src/Components/Pages/SearchPage.tsx @@ -1,11 +1,18 @@ import React from "react" import { usePageTrack } from "../../Contexts/PosthogContext" import SearchFileBrowser from "../Modules/FileBrowsers/SearchFileBrowser" +import { Helmet } from "react-helmet-async" +import { t } from "@lingui/macro" const SearchPage = () => { usePageTrack() - return + return <> + + {t`Search results`} - Chainsafe Files + + + } export default SearchPage diff --git a/packages/files-ui/src/Components/Pages/SettingsPage.tsx b/packages/files-ui/src/Components/Pages/SettingsPage.tsx index 703e9399c9..907161a082 100644 --- a/packages/files-ui/src/Components/Pages/SettingsPage.tsx +++ b/packages/files-ui/src/Components/Pages/SettingsPage.tsx @@ -1,11 +1,18 @@ import React from "react" import { usePageTrack } from "../../Contexts/PosthogContext" import Settings from "../Modules/Settings" +import { Helmet } from "react-helmet-async" +import { t } from "@lingui/macro" const SettingsPage = () => { usePageTrack() - return + return <> + + {t`Settings`} - Chainsafe Files + + + } export default SettingsPage diff --git a/packages/files-ui/src/Components/Pages/SharedFoldersPage.tsx b/packages/files-ui/src/Components/Pages/SharedFoldersPage.tsx new file mode 100644 index 0000000000..cf9c5f9d44 --- /dev/null +++ b/packages/files-ui/src/Components/Pages/SharedFoldersPage.tsx @@ -0,0 +1,18 @@ +import React from "react" +import { usePageTrack } from "../../Contexts/PosthogContext" +import SharedFoldersOverview from "../Modules/FileBrowsers/SharedFoldersOverview" +import { Helmet } from "react-helmet-async" +import { t } from "@lingui/macro" + +const SharedFoldersPage = () => { + usePageTrack() + + return <> + + {t`Shared folders`} - Chainsafe Files + + + +} + +export default SharedFoldersPage diff --git a/packages/files-ui/src/Contexts/FilesContext.tsx b/packages/files-ui/src/Contexts/FilesContext.tsx index cfc9f017e4..b76d65c1dc 100644 --- a/packages/files-ui/src/Contexts/FilesContext.tsx +++ b/packages/files-ui/src/Contexts/FilesContext.tsx @@ -803,7 +803,8 @@ const FilesProvider = ({ children }: FilesContextProps) => { updateToast(toastId, { title: t`${inSharedBucket ? "Copying" : "Sharing"} ${fileProgress} - ${item.name}`, type: "success", - progress: currentProgress + progress: currentProgress, + testId: "sharing-progress" }) }, cancelToken @@ -829,6 +830,7 @@ const FilesProvider = ({ children }: FilesContextProps) => { : t`${inSharedBucket ? "Copying" : "Sharing"} failed`, type: successCount ? "success" : "error", progress: undefined, + testId: "share-success", isClosable: true }, true) setTransfersInProgress(false) diff --git a/packages/files-ui/src/Contexts/ThresholdKeyContext.tsx b/packages/files-ui/src/Contexts/ThresholdKeyContext.tsx index 6002aa0d29..a2e36fa857 100644 --- a/packages/files-ui/src/Contexts/ThresholdKeyContext.tsx +++ b/packages/files-ui/src/Contexts/ThresholdKeyContext.tsx @@ -157,8 +157,10 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f ...bs, ...bowser.parse(bs.userAgent) } as BrowserShare)), [parsedShares]) - const hasMnemonicShare = useMemo(() => (keyDetails && (keyDetails.totalShares - parsedShares.length > 1)) || false, - [keyDetails, parsedShares.length]) + + const hasMnemonicShare = useMemo(() => parsedShares.filter((s) => s.module === SHARE_SERIALIZATION_MODULE_NAME).length > 0, + [parsedShares]) + const hasPasswordShare = useMemo(() => parsedShares.filter((s) => s.module === SECURITY_QUESTIONS_MODULE_NAME).length > 0, [parsedShares]) @@ -254,7 +256,6 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f // cached may be stale, resulting in a failure to reconstruct the key. This is // identified through the nonce. Manually refreshing the metadata cache solves this problem if (error.message.includes("nonce")) { - await TKeySdk._syncShareMetadata() const { privKey } = await TKeySdk.reconstructKey(false) const privKeyString = privKey.toString("hex") if (privKeyString.length < 64) { @@ -360,8 +361,9 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f console.log("Share transfer request created. Starting request status poller") shareEncPubKeyX = currentEncPubKeyX await shareTransferModule.startRequestStatusCheck(currentEncPubKeyX, true) - const resultKey = await TKeySdk?.getKeyDetails() - await TKeySdk.syncLocalMetadataTransitions() + await TKeySdk.reconstructKey() + const resultKey = TKeySdk?.getKeyDetails() + setKeyDetails(resultKey) shareEncPubKeyX = undefined } catch (error) { @@ -625,6 +627,10 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f requiredShareStore.share.share, "mnemonic" )) as string + await TKeySdk.addShareDescription( + shareCreated.newShareIndex.toString("hex"), + JSON.stringify({ module: SHARE_SERIALIZATION_MODULE_NAME }), + true) const keyDetails = await TKeySdk.getKeyDetails() setKeyDetails(keyDetails) return result @@ -664,7 +670,9 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f try { const storageModule = TKeySdk.modules[WEB_STORAGE_MODULE_NAME] as WebStorageModule - await TKeySdk._syncShareMetadata() + await TKeySdk.initialize() // should reinitialize internally + await TKeySdk.reconstructKey() + const newDeviceShare = await TKeySdk.generateNewShare() const newDeviceShareStore = newDeviceShare.newShareStores[newDeviceShare.newShareIndex.toString("hex")] @@ -684,9 +692,9 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f try { const shareTransferModule = TKeySdk.modules[SHARE_TRANSFER_MODULE_NAME] as ShareTransferModule - await shareTransferModule.approveRequest(encPubKeyX) - await TKeySdk._syncShareMetadata() - const newKeyDetails = await TKeySdk.getKeyDetails() + const result = await TKeySdk.generateNewShare() + await shareTransferModule.approveRequest(encPubKeyX, result.newShareStores[result.newShareIndex.toString("hex")]) + const newKeyDetails = TKeySdk.getKeyDetails() setKeyDetails(newKeyDetails) } catch (e) { console.error(e) diff --git a/packages/files-ui/src/Themes/Constants.ts b/packages/files-ui/src/Themes/Constants.ts index 5ae9fd8019..1396e76477 100644 --- a/packages/files-ui/src/Themes/Constants.ts +++ b/packages/files-ui/src/Themes/Constants.ts @@ -61,6 +61,7 @@ export interface CsfColors extends IConstants { itemColorHover: string itemIconColor: string itemIconColorHover: string + mobileSelectedBackground: string profileButtonShadow: string } createFolder: { diff --git a/packages/files-ui/src/Themes/DarkTheme.ts b/packages/files-ui/src/Themes/DarkTheme.ts index 17f07a86e2..d4bec80484 100644 --- a/packages/files-ui/src/Themes/DarkTheme.ts +++ b/packages/files-ui/src/Themes/DarkTheme.ts @@ -373,9 +373,10 @@ export const darkTheme = createTheme({ mobileBackgroundColor: "var(--gray2)", headingColor: "var(--gray9)", itemColor: "var(--gray9)", - itemColorHover: "var(--gray9)", itemIconColor: "var(--gray9)", + itemColorHover: "var(--gray9)", itemIconColorHover: "var(--gray9)", + mobileSelectedBackground: "var(--gray9)", profileButtonShadow: "0px 1px 2px rgba(0, 0, 0, 0.25)" }, createFolder: { diff --git a/packages/files-ui/src/Themes/LightTheme.ts b/packages/files-ui/src/Themes/LightTheme.ts index 41acea571c..367c2e71ca 100644 --- a/packages/files-ui/src/Themes/LightTheme.ts +++ b/packages/files-ui/src/Themes/LightTheme.ts @@ -63,9 +63,10 @@ export const lightTheme = createTheme({ mobileBackgroundColor: "var(--gray9)", headingColor: "inherit", itemColor: "inherit", - itemColorHover: "var(--gray7)", itemIconColor: "inherit", - itemIconColorHover: "var(--gray7)", + itemColorHover: "var(--gray6)", + itemIconColorHover: "var(--gray6)", + mobileSelectedBackground: "var(--gray9)", profileButtonShadow: "0px 1px 2px rgba(0, 0, 0, 0.25)" }, createFolder: { diff --git a/packages/files-ui/src/locales/de/messages.po b/packages/files-ui/src/locales/de/messages.po index 39df0ebec4..b270d56250 100644 --- a/packages/files-ui/src/locales/de/messages.po +++ b/packages/files-ui/src/locales/de/messages.po @@ -619,7 +619,7 @@ msgstr "" msgid "Only send the exact amount of {0} to this address" msgstr "" -msgid "Oops! You need to pay for this month to upload more content." +msgid "Only with annual billing" msgstr "" msgid "Operating system:" @@ -697,6 +697,9 @@ msgstr "Bitte geben Sie ein Passwort an" msgid "Please select a file to upload" msgstr "Bitte wählen Sie eine Datei zum Hochladen" +msgid "Please select fewer files to upload" +msgstr "" + msgid "Preview" msgstr "Vorschau" @@ -853,6 +856,9 @@ msgstr "" msgid "Shared folders" msgstr "" +msgid "Shared link" +msgstr "" + msgid "Shared with" msgstr "" @@ -1012,6 +1018,9 @@ msgstr "Erneut versuchen" msgid "Try another method" msgstr "Eine andere Methode versuchen" +msgid "USDC, DAI, BTC, or ETH" +msgstr "" + msgid "Update" msgstr "Aktualisieren" @@ -1030,6 +1039,9 @@ msgstr "" msgid "Upload" msgstr "" +msgid "Upload size exceeds plan capacity" +msgstr "" + msgid "Uploads cancelled" msgstr "" @@ -1141,6 +1153,9 @@ msgstr "" msgid "You've got a payment due. Until you've settled up, we've placed your account in restricted mode" msgstr "" +msgid "Your account is restricted. Until you've settled up, you can't upload any new content." +msgstr "" + msgid "Your content exceeds the {planStorageCapacity} storage capacity for this plan." msgstr "" diff --git a/packages/files-ui/src/locales/en/messages.po b/packages/files-ui/src/locales/en/messages.po index 10c39e1325..5042749363 100644 --- a/packages/files-ui/src/locales/en/messages.po +++ b/packages/files-ui/src/locales/en/messages.po @@ -622,8 +622,8 @@ msgstr "One sec, getting files ready…" msgid "Only send the exact amount of {0} to this address" msgstr "Only send the exact amount of {0} to this address" -msgid "Oops! You need to pay for this month to upload more content." -msgstr "Oops! You need to pay for this month to upload more content." +msgid "Only with annual billing" +msgstr "Only with annual billing" msgid "Operating system:" msgstr "Operating system:" @@ -700,6 +700,9 @@ msgstr "Please provide a password" msgid "Please select a file to upload" msgstr "Please select a file to upload" +msgid "Please select fewer files to upload" +msgstr "Please select fewer files to upload" + msgid "Preview" msgstr "Preview" @@ -856,6 +859,9 @@ msgstr "Shared folder name" msgid "Shared folders" msgstr "Shared folders" +msgid "Shared link" +msgstr "Shared link" + msgid "Shared with" msgstr "Shared with" @@ -1015,6 +1021,9 @@ msgstr "Try again" msgid "Try another method" msgstr "Try another method" +msgid "USDC, DAI, BTC, or ETH" +msgstr "USDC, DAI, BTC, or ETH" + msgid "Update" msgstr "Update" @@ -1033,6 +1042,9 @@ msgstr "Update your credit card" msgid "Upload" msgstr "Upload" +msgid "Upload size exceeds plan capacity" +msgstr "Upload size exceeds plan capacity" + msgid "Uploads cancelled" msgstr "Uploads cancelled" @@ -1144,6 +1156,9 @@ msgstr "You will need to sign a message in your wallet to complete sign in." msgid "You've got a payment due. Until you've settled up, we've placed your account in restricted mode" msgstr "You've got a payment due. Until you've settled up, we've placed your account in restricted mode" +msgid "Your account is restricted. Until you've settled up, you can't upload any new content." +msgstr "Your account is restricted. Until you've settled up, you can't upload any new content." + msgid "Your content exceeds the {planStorageCapacity} storage capacity for this plan." msgstr "Your content exceeds the {planStorageCapacity} storage capacity for this plan." diff --git a/packages/files-ui/src/locales/es/messages.po b/packages/files-ui/src/locales/es/messages.po index 50b9c306ca..bfcff03fbe 100644 --- a/packages/files-ui/src/locales/es/messages.po +++ b/packages/files-ui/src/locales/es/messages.po @@ -623,7 +623,7 @@ msgstr "" msgid "Only send the exact amount of {0} to this address" msgstr "" -msgid "Oops! You need to pay for this month to upload more content." +msgid "Only with annual billing" msgstr "" msgid "Operating system:" @@ -701,6 +701,9 @@ msgstr "Por favor ingrese una contraseña" msgid "Please select a file to upload" msgstr "" +msgid "Please select fewer files to upload" +msgstr "" + msgid "Preview" msgstr "Avance" @@ -857,6 +860,9 @@ msgstr "" msgid "Shared folders" msgstr "" +msgid "Shared link" +msgstr "" + msgid "Shared with" msgstr "" @@ -1016,6 +1022,9 @@ msgstr "Intentar otra vez" msgid "Try another method" msgstr "Prueba con otro método" +msgid "USDC, DAI, BTC, or ETH" +msgstr "" + msgid "Update" msgstr "Actualizar" @@ -1034,6 +1043,9 @@ msgstr "" msgid "Upload" msgstr "" +msgid "Upload size exceeds plan capacity" +msgstr "" + msgid "Uploads cancelled" msgstr "" @@ -1145,6 +1157,9 @@ msgstr "Deberá firmar un mensaje en su billetera para completar el inicio de se msgid "You've got a payment due. Until you've settled up, we've placed your account in restricted mode" msgstr "" +msgid "Your account is restricted. Until you've settled up, you can't upload any new content." +msgstr "" + msgid "Your content exceeds the {planStorageCapacity} storage capacity for this plan." msgstr "" diff --git a/packages/files-ui/src/locales/fr/messages.po b/packages/files-ui/src/locales/fr/messages.po index 96692e72bc..4aeff95518 100644 --- a/packages/files-ui/src/locales/fr/messages.po +++ b/packages/files-ui/src/locales/fr/messages.po @@ -3,15 +3,15 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-04-23 11:05+0200\n" -"PO-Revision-Date: 2022-02-28 19:31+0000\n" -"Last-Translator: Thibaut \n" +"PO-Revision-Date: 2022-03-26 13:08+0000\n" +"Last-Translator: J. Lavoie \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.11.1-dev\n" +"X-Generator: Weblate 4.12-dev\n" "Mime-Version: 1.0\n" msgid "(Awaiting payment)" @@ -54,7 +54,7 @@ msgid "Account is restricted" msgstr "Ce compte est limité" msgid "Account visibility" -msgstr "" +msgstr "Visibilité du compte" msgid "Add Card" msgstr "Ajouter une carte" @@ -264,7 +264,7 @@ msgid "Credit Card is expiring soon" msgstr "La carte de crédit va bientôt expirer" msgid "Crypto payments may take a few minutes to be processed. The subscription update will reflect on next login." -msgstr "" +msgstr "Le traitement des paiements en cryptomonnaie peut prendre quelques minutes. La mise à jour de l'abonnement sera prise en compte lors de la prochaine connexion." msgid "DAI, USDC, ETH or BTC" msgstr "DAI, USDC, ETH ou BTC" @@ -408,13 +408,13 @@ msgid "Files" msgstr "Fichiers" msgid "Files Free" -msgstr "" +msgstr "Files gratuits" msgid "Files Max" -msgstr "" +msgstr "Files Max" msgid "Files Pro" -msgstr "" +msgstr "Files Pro" msgid "Files sharing key" msgstr "Clé de partage des fichiers" @@ -623,8 +623,8 @@ msgstr "Les fichiers sont presque prêts…" msgid "Only send the exact amount of {0} to this address" msgstr "N'envoyez que la somme exacte de {0} à cette adresse" -msgid "Oops! You need to pay for this month to upload more content." -msgstr "Oups ! Vous devez payer pour ce mois-ci pour ajouter plus de contenu." +msgid "Only with annual billing" +msgstr "Uniquement avec paiement annuel" msgid "Operating system:" msgstr "Système d’exploitation :" @@ -701,6 +701,9 @@ msgstr "Merci de donner un mot de passe" msgid "Please select a file to upload" msgstr "Merci de sélectionner un fichier à téléverser" +msgid "Please select fewer files to upload" +msgstr "" + msgid "Preview" msgstr "Aperçu" @@ -857,6 +860,9 @@ msgstr "Nom du dossier partagé" msgid "Shared folders" msgstr "Dossiers partagés" +msgid "Shared link" +msgstr "" + msgid "Shared with" msgstr "Partagé avec" @@ -1016,6 +1022,9 @@ msgstr "Essayer de nouveau" msgid "Try another method" msgstr "Essayer une autre méthode" +msgid "USDC, DAI, BTC, or ETH" +msgstr "USDC, DAI, BTC ou ETH" + msgid "Update" msgstr "Mettre à jour" @@ -1034,6 +1043,9 @@ msgstr "Mettre à jour votre carte de crédit" msgid "Upload" msgstr "Téléverser" +msgid "Upload size exceeds plan capacity" +msgstr "" + msgid "Uploads cancelled" msgstr "Téléversements annulés" @@ -1145,6 +1157,9 @@ msgstr "Vous devrez signer un message avec votre wallet pour terminer la procéd msgid "You've got a payment due. Until you've settled up, we've placed your account in restricted mode" msgstr "Vous avez un paiement en retard. Tant qu'il n'a pas été réglé, l'accès à votre compte a été restreint" +msgid "Your account is restricted. Until you've settled up, you can't upload any new content." +msgstr "Votre compte est restreint. Merci de consulter les paramètres de paiement.Vous ne pouvez pas téléverser de nouveau contenu." + msgid "Your content exceeds the {planStorageCapacity} storage capacity for this plan." msgstr "Votre contenu dépasse la capacité de stockage de {planStorageCapacity} de cette formule." diff --git a/packages/files-ui/src/locales/no/messages.po b/packages/files-ui/src/locales/no/messages.po index 5f20f111a9..2c27ff8884 100644 --- a/packages/files-ui/src/locales/no/messages.po +++ b/packages/files-ui/src/locales/no/messages.po @@ -619,7 +619,7 @@ msgstr "" msgid "Only send the exact amount of {0} to this address" msgstr "" -msgid "Oops! You need to pay for this month to upload more content." +msgid "Only with annual billing" msgstr "" msgid "Operating system:" @@ -697,6 +697,9 @@ msgstr "Angi et passord" msgid "Please select a file to upload" msgstr "Velg en fil å laste opp" +msgid "Please select fewer files to upload" +msgstr "" + msgid "Preview" msgstr "Forhåndsvis" @@ -853,6 +856,9 @@ msgstr "" msgid "Shared folders" msgstr "" +msgid "Shared link" +msgstr "" + msgid "Shared with" msgstr "" @@ -1012,6 +1018,9 @@ msgstr "Prøv igjen" msgid "Try another method" msgstr "Prøv annen metode" +msgid "USDC, DAI, BTC, or ETH" +msgstr "" + msgid "Update" msgstr "Oppdater" @@ -1030,6 +1039,9 @@ msgstr "" msgid "Upload" msgstr "" +msgid "Upload size exceeds plan capacity" +msgstr "" + msgid "Uploads cancelled" msgstr "" @@ -1141,6 +1153,9 @@ msgstr "" msgid "You've got a payment due. Until you've settled up, we've placed your account in restricted mode" msgstr "" +msgid "Your account is restricted. Until you've settled up, you can't upload any new content." +msgstr "" + msgid "Your content exceeds the {planStorageCapacity} storage capacity for this plan." msgstr "" diff --git a/packages/storage-ui/.env.example b/packages/storage-ui/.env.example index d7feb8796a..5f61e2ed06 100644 --- a/packages/storage-ui/.env.example +++ b/packages/storage-ui/.env.example @@ -1,7 +1,7 @@ PORT=3000 HTTPS=false -REACT_APP_API_URL=https://stage.imploy.site/api/v1 +REACT_APP_API_URL=https://stage-api.chainsafe.io/api/v1 REACT_APP_IPFS_GATEWAY=https://ipfs.chainsafe.io/ipfs REACT_APP_STRIPE_PK= diff --git a/packages/storage-ui/cypress.json b/packages/storage-ui/cypress.json index 591ee687a6..be2883e932 100644 --- a/packages/storage-ui/cypress.json +++ b/packages/storage-ui/cypress.json @@ -1,5 +1,9 @@ { "integrationFolder": "cypress/tests", "video": false, - "experimentalSessionSupport": true + "experimentalSessionSupport": true, + "retries": { + "runMode": 2, + "openMode": 0 + } } diff --git a/packages/storage-ui/cypress/fixtures/storageTestData.ts b/packages/storage-ui/cypress/fixtures/storageTestData.ts index d6bf8ae06f..5deaffcd91 100644 --- a/packages/storage-ui/cypress/fixtures/storageTestData.ts +++ b/packages/storage-ui/cypress/fixtures/storageTestData.ts @@ -1,2 +1,3 @@ export const bucketName = "test bucket" export const testCid = "QmZEE7Ymh2mRMURLnFLipJTovb44AoUYDzx7aipYZwvxX5" +export const testCidName = "cute cat" diff --git a/packages/storage-ui/cypress/support/commands.ts b/packages/storage-ui/cypress/support/commands.ts index 9d7203a494..71d3ed8fe1 100644 --- a/packages/storage-ui/cypress/support/commands.ts +++ b/packages/storage-ui/cypress/support/commands.ts @@ -43,6 +43,7 @@ export interface Web3LoginOptions { withNewUser?: boolean clearPins?: boolean deleteFpsBuckets?: boolean + withNewSession?: boolean } Cypress.Commands.add("clearPins", apiTestHelper.clearPins) @@ -57,7 +58,8 @@ Cypress.Commands.add( url = localHost, clearPins = false, withNewUser = true, - deleteFpsBuckets = false + deleteFpsBuckets = false, + withNewSession = false }: Web3LoginOptions = {}) => { cy.on("window:before:load", (win) => { @@ -74,16 +76,26 @@ Cypress.Commands.add( }) }) - // with nothing in localstorage (and in session storage) - // the whole login flow should kick in - cy.session("web3loginNewUser", () => { - cy.visit(url) - authenticationPage.web3Button().click() - authenticationPage.showMoreButton().click() - authenticationPage.detectedWallet().click() - authenticationPage.web3SignInButton().safeClick() - bucketsPage.bucketsHeaderLabel().should("be.visible") - }) + if (withNewUser || withNewSession){ + const sessionName = `web3loginNewUser-${withNewSession ? new Date().toString() : "0"}` + cy.session(sessionName, () => { + cy.visit(url) + authenticationPage.web3Button().click() + authenticationPage.showMoreButton().click() + authenticationPage.detectedWallet().click() + authenticationPage.web3SignInButton().safeClick() + bucketsPage.bucketsHeaderLabel().should("be.visible") + }) + } else { + cy.session("web3loginTestUser", () => { + cy.visit(url) + authenticationPage.web3Button().click() + authenticationPage.showMoreButton().click() + authenticationPage.detectedWallet().click() + authenticationPage.web3SignInButton().safeClick() + bucketsPage.bucketsHeaderLabel().should("be.visible") + }) + } cy.visit(url) bucketsPage.bucketsHeaderLabel().should("be.visible") @@ -120,6 +132,7 @@ declare global { * @param {String} options.url - (default: "http://localhost:3000") - what url to visit. * @param {Boolean} options.withNewUser - (default: true) - whether to create a new user for this session. * @param {Boolean} options.clearCSFBucket - (default: false) - whether any file in the csf bucket should be deleted. + * @param {Boolean} options.withNewSession - (default: false) - whether to create a new session. */ web3Login: (options?: Web3LoginOptions) => void diff --git a/packages/storage-ui/cypress/support/index.ts b/packages/storage-ui/cypress/support/index.ts index 764ed85986..19366f75fe 100644 --- a/packages/storage-ui/cypress/support/index.ts +++ b/packages/storage-ui/cypress/support/index.ts @@ -16,16 +16,9 @@ // Import commands.js using ES2015 syntax: import "./commands" -// the following gets rid of the exception "ResizeObserver loop limit exceeded" -// which someone on the internet says we can safely ignore -// source https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded -Cypress.on("uncaught:exception", (err) => { - /* returning false here prevents Cypress from failing the test */ - if (err.message.includes("ResizeObserver loop limit exceeded")) { - // returning false here prevents Cypress from - // failing the test - return false - } +// returning false prevents Cypress from failing the test automatically +Cypress.on("uncaught:exception", () => { + return false }) // Hide fetch/XHR requests diff --git a/packages/storage-ui/cypress/support/page-objects/cidsPage.ts b/packages/storage-ui/cypress/support/page-objects/cidsPage.ts index 0d61f89942..3a19c53a15 100644 --- a/packages/storage-ui/cypress/support/page-objects/cidsPage.ts +++ b/packages/storage-ui/cypress/support/page-objects/cidsPage.ts @@ -2,6 +2,7 @@ import { basePage } from "./basePage" import { testCid } from "../../fixtures/storageTestData" +import { addCidModal } from "./modals/addCidModal" export const cidsPage = { ...basePage, @@ -16,22 +17,17 @@ export const cidsPage = { sizeTableHeader: () => cy.get("[data-cy=table-header-size]"), statusTableHeader: () => cy.get("[data-cy=table-header-status]"), cidItemRow: () => cy.get("[data-cy=row-cid-item]", { timeout: 20000 }), + cidNameCell: () => cy.get("[data-cy=cell-pin-name]"), cidRowKebabButton: () => cy.get("[data-testid=dropdown-title-cid-kebab]"), - // pin cid modal elements - pinCidForm: () => cy.get("[data-testid=form-create-bucket]"), - cidInput: () => cy.get("[data-cy=input-cid]"), - pinCancelButton: () => cy.get("[data-cy=button-cancel-add-pin]"), - pinSubmitButton: () => cy.get("[data-cy=button-submit-pin]"), - // menu elements unpinMenuOption: () => cy.get("[data-cy=menu-unpin]"), // helpers and convenience functions addPinnedCid() { this.pinButton().click() - this.cidInput().type(testCid) - this.pinSubmitButton().click() + addCidModal.cidInput().type(testCid) + addCidModal.pinSubmitButton().click() this.cidItemRow().should("have.length", 1) } } \ No newline at end of file diff --git a/packages/storage-ui/cypress/support/page-objects/modals/addCidModal.ts b/packages/storage-ui/cypress/support/page-objects/modals/addCidModal.ts new file mode 100644 index 0000000000..411261b759 --- /dev/null +++ b/packages/storage-ui/cypress/support/page-objects/modals/addCidModal.ts @@ -0,0 +1,8 @@ +export const addCidModal = { + body: () => cy.get("[data-testid=modal-container-add-cid]", { timeout: 10000 }), + nameInput: () => cy.get("[data-cy=input-cid-name]"), + cidInput: () => cy.get("[data-cy=input-cid]"), + cidPinnedWarningLabel: () => cy.get("[data-cy=label-cid-pinned-warning]", { timeout: 2000 }), + pinCancelButton: () => cy.get("[data-cy=button-cancel-add-pin]"), + pinSubmitButton: () => cy.get("[data-cy=button-submit-pin]") +} diff --git a/packages/storage-ui/cypress/support/utils/apiTestHelper.ts b/packages/storage-ui/cypress/support/utils/apiTestHelper.ts index a7762eab81..fddfee4bf1 100644 --- a/packages/storage-ui/cypress/support/utils/apiTestHelper.ts +++ b/packages/storage-ui/cypress/support/utils/apiTestHelper.ts @@ -3,7 +3,7 @@ import { FilesApiClient } from "@chainsafe/files-api-client" import { BucketType } from "@chainsafe/files-api-client" const REFRESH_TOKEN_KEY = "css.refreshToken" -const API_BASE_URL = "https://stage.imploy.site/api/v1" +const API_BASE_URL = "https://stage-api.chainsafe.io/api/v1" const getApiClient = () => { // Disable the internal Axios JSON deserialization as this is handled by the client diff --git a/packages/storage-ui/cypress/tests/cid-management-spec.ts b/packages/storage-ui/cypress/tests/cid-management-spec.ts index 9247d9911b..56452c2c04 100644 --- a/packages/storage-ui/cypress/tests/cid-management-spec.ts +++ b/packages/storage-ui/cypress/tests/cid-management-spec.ts @@ -1,31 +1,39 @@ import { cidsPage } from "../support/page-objects/cidsPage" import { navigationMenu } from "../support/page-objects/navigationMenu" -import { testCid } from "../fixtures/storageTestData" +import { testCid, testCidName } from "../fixtures/storageTestData" +import { addCidModal } from "../support/page-objects/modals/addCidModal" describe("CID management", () => { context("desktop", () => { it("can pin a CID", () => { - cy.web3Login({ clearPins: true }) + cy.web3Login({ withNewSession: true }) navigationMenu.cidsNavButton().click() // pin a cid and see it in the pinned items table cidsPage.pinButton().click() - cidsPage.cidInput().type(testCid) - cidsPage.pinSubmitButton().safeClick() + addCidModal.body().should("be.visible") + addCidModal.nameInput().type(testCidName) + addCidModal.cidInput().type(testCid) + addCidModal.pinSubmitButton().safeClick() cidsPage.cidItemRow().should("have.length", 1) + cidsPage.cidItemRow().within(() => { + cidsPage.cidNameCell().should("have.text", testCidName) + }) // open the pin cid modal and cancel it cidsPage.pinButton().click() - cidsPage.pinCancelButton().click() - cidsPage.pinCidForm().should("not.exist") + addCidModal.body().should("be.visible") + addCidModal.pinCancelButton().click() + addCidModal.body().should("not.exist") }) // this is unreliable since the pin from the previous // test is still in the "queued" state while being unpinned. - it.skip("can unpin a cid", () => { - cy.web3Login({ clearPins: true }) + it("can unpin a cid", () => { + cy.web3Login({ withNewSession: true }) + navigationMenu.cidsNavButton().click() // pin and then unpin a CID cidsPage.addPinnedCid() @@ -34,4 +42,18 @@ describe("CID management", () => { cidsPage.cidItemRow().should("contain.text", "queued") }) }) + + it("can see a warning when attempting to pin the same CID twice", () => { + cy.web3Login({ withNewSession: true }) + navigationMenu.cidsNavButton().click() + + // add a cid + cidsPage.addPinnedCid() + + // see warning if attempting to pin the cid again + cidsPage.pinButton().click() + addCidModal.body().should("be.visible") + addCidModal.cidInput().type(testCid) + addCidModal.cidPinnedWarningLabel().should("be.visible") + }) }) \ No newline at end of file diff --git a/packages/storage-ui/package.json b/packages/storage-ui/package.json index 6f398fe780..0ce787974a 100644 --- a/packages/storage-ui/package.json +++ b/packages/storage-ui/package.json @@ -6,14 +6,14 @@ "@babel/core": "^7.12.10", "@babel/runtime": "^7.0.0", "@chainsafe/browser-storage-hooks": "^1.0.1", - "@chainsafe/files-api-client": "1.18.29", + "@chainsafe/files-api-client": "1.18.34", "@chainsafe/web3-context": "1.3.0", "@emeraldpay/hashicon-react": "0.5.2", "@lingui/core": "^3.7.2", "@lingui/react": "^3.7.2", "@sentry/react": "^5.28.0", "@toruslabs/torus-direct-web-sdk": "4.15.1", - "axios": "0.21.4", + "axios": "0.26.1", "babel-loader": "8.1.0", "babel-plugin-macros": "^2.8.0", "babel-preset-env": "^1.7.0", @@ -95,4 +95,4 @@ "last 1 safari version" ] } -} +} \ No newline at end of file diff --git a/packages/storage-ui/src/App.tsx b/packages/storage-ui/src/App.tsx index c972a8b343..2755ae95f8 100644 --- a/packages/storage-ui/src/App.tsx +++ b/packages/storage-ui/src/App.tsx @@ -16,6 +16,8 @@ import { StorageProvider } from "./Contexts/StorageContext" import { UserProvider } from "./Contexts/UserContext" import { BillingProvider } from "./Contexts/BillingContext" import { NotificationsProvider } from "./Contexts/NotificationsContext" +import { PosthogProvider } from "./Contexts/PosthogContext" +import { HelmetProvider } from "react-helmet-async" if ( process.env.NODE_ENV === "production" && @@ -62,7 +64,7 @@ const App = () => { const { initHotjar } = useHotjar() const { canUseLocalStorage } = useLocalStorage() const hotjarId = process.env.REACT_APP_HOTJAR_ID - const apiUrl = process.env.REACT_APP_API_URL || "https://stage.imploy.site/api/v1" + const apiUrl = process.env.REACT_APP_API_URL || "https://stage-api.chainsafe.io/api/v1" // This will default to testnet unless mainnet is specifically set in the ENV useEffect(() => { @@ -95,47 +97,51 @@ const App = () => { ), []) return ( - - window.location.reload()} + + - - - - - window.location.reload()} + > + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/packages/storage-ui/src/Components/Elements/BucketRow.tsx b/packages/storage-ui/src/Components/Elements/BucketRow.tsx index 54aa2874d6..5d7406b852 100644 --- a/packages/storage-ui/src/Components/Elements/BucketRow.tsx +++ b/packages/storage-ui/src/Components/Elements/BucketRow.tsx @@ -83,6 +83,9 @@ const BucketRow = ({ bucket }: Props) => { onClick={() => redirect(ROUTE_LINKS.Bucket(bucket.id, "/"))}> {bucket.name || bucket.id} + + {bucket.file_system_type === "ipfs" ? "IPFS MFS" : "Chainsafe" } + {formatBytes(bucket.size, 2)} diff --git a/packages/storage-ui/src/Components/Elements/CidRow.tsx b/packages/storage-ui/src/Components/Elements/CidRow.tsx index e1bcc29086..64b742d2d9 100644 --- a/packages/storage-ui/src/Components/Elements/CidRow.tsx +++ b/packages/storage-ui/src/Components/Elements/CidRow.tsx @@ -1,6 +1,6 @@ import React from "react" import { makeStyles, createStyles } from "@chainsafe/common-theme" -import { DeleteSvg, formatBytes, MenuDropdown, MoreIcon, TableCell, TableRow } from "@chainsafe/common-components" +import { DeleteSvg, ExternalIcon, formatBytes, MenuDropdown, MoreIcon, TableCell, TableRow } from "@chainsafe/common-components" import { Trans } from "@lingui/macro" import dayjs from "dayjs" import { PinStatus } from "@chainsafe/files-api-client" @@ -9,7 +9,7 @@ import { useStorage } from "../../Contexts/StorageContext" import { desktopGridSettings, mobileGridSettings } from "../Pages/CidsPage" import { trimChar } from "../../Utils/pathUtils" -const useStyles = makeStyles(({ animation, constants, breakpoints }: CSSTheme) => +const useStyles = makeStyles(({ animation, constants, breakpoints, palette }: CSSTheme) => createStyles({ dropdownIcon: { "& svg": { @@ -52,6 +52,13 @@ const useStyles = makeStyles(({ animation, constants, breakpoints }: CSSTheme) = [breakpoints.down("md")]: { gridTemplateColumns: mobileGridSettings } + }, + icon: { + stroke: palette.additional["gray"][8], + cursor: "pointer" + }, + externalIconCell: { + padding: "0 !important" } }) ) @@ -59,7 +66,7 @@ interface Props { pinStatus: PinStatus } -const IPFS_GATEWAY = process.env.REACT_APP_IPFS_GATEWAY || "" +const IPFS_GATEWAY = process.env.REACT_APP_IPFS_GATEWAY || "https://ipfs.io/ipfs/" const CidRow = ({ pinStatus }: Props) => { const classes = useStyles() @@ -73,7 +80,15 @@ const CidRow = ({ pinStatus }: Props) => { > + align='left' + data-cy="cell-pin-name" + > + {pinStatus.pin?.name || "-"} + + {pinStatus.pin?.cid} @@ -85,14 +100,11 @@ const CidRow = ({ pinStatus }: Props) => { {pinStatus.status} - - - Open on Gateway - + + window.open(`${trimChar(IPFS_GATEWAY, "/")}/${pinStatus.pin?.cid}`)} + /> { + return createStyles({ + accountRestrictedNotification: { + position: "fixed", + bottom: 0, + backgroundColor: palette.additional["gray"][10], + color: palette.additional["gray"][1], + padding: `${constants.generalUnit * 2}px ${constants.generalUnit * 3}px`, + left: 0, + width: "100vw", + [breakpoints.up("md")]: { + left: `${constants.navWidth}px`, + width:`calc(100vw - ${constants.navWidth}px)`, + display: "flex", + justifyContent: "space-between", + alignItems: "center" + } + } + }) + } +) + +const RestrictedModeBanner = () => { + const classes = useStyles() + const { desktop } = useThemeSwitcher() + const { redirect } = useHistory() + + return ( +
+ + You've got a payment due. Until you've settled up, we've placed your account in restricted mode + + +
) +} + +export default RestrictedModeBanner \ No newline at end of file diff --git a/packages/storage-ui/src/Components/Layouts/AppNav.tsx b/packages/storage-ui/src/Components/Layouts/AppNav.tsx index 8cb8224cb8..3fb78fe4a2 100644 --- a/packages/storage-ui/src/Components/Layouts/AppNav.tsx +++ b/packages/storage-ui/src/Components/Layouts/AppNav.tsx @@ -3,7 +3,7 @@ import { makeStyles, useThemeSwitcher } from "@chainsafe/common-theme" -import React, { useCallback } from "react" +import React, { useCallback, useMemo } from "react" import clsx from "clsx" import { Link, @@ -16,7 +16,8 @@ import { SettingSvg, DocumentSvg, Button, - PowerDownIcon + PowerDownIcon, + useLocation } from "@chainsafe/common-components" import { ROUTE_LINKS } from "../StorageRoutes" import { Trans } from "@lingui/macro" @@ -137,43 +138,37 @@ const useStyles = makeStyles( flexDirection: "row", alignItems: "center", cursor: "pointer", - padding: `${constants.generalUnit * 1.5}px 0`, + margin: `${constants.generalUnit * 0.5}px 0`, + padding: `${constants.generalUnit}px ${constants.generalUnit * 1.5}px`, + borderRadius: "4px", transitionDuration: `${animation.transform}ms`, "& span": { - transitionDuration: `${animation.transform}ms`, - [breakpoints.up("md")]: { - color: constants.nav.itemColor - }, + color: constants.nav.itemColor, [breakpoints.down("md")]: { - color: constants.nav.itemColorHover + color: palette.additional["gray"][3] } }, "& svg": { - transitionDuration: `${animation.transform}ms`, width: Number(constants.svgWidth), marginRight: constants.generalUnit * 2, - [breakpoints.up("md")]: { - fill: constants.nav.itemIconColor - }, + fill: constants.nav.itemIconColor, [breakpoints.down("md")]: { - fill: constants.nav.itemIconColorHover + fill: palette.additional["gray"][3] } }, "&:hover": { - "& span": { - color: constants.nav.itemColorHover - }, - "& svg": { - fill: constants.nav.itemIconColorHover - } + backgroundColor: palette.additional["gray"][5] }, - [breakpoints.down("md")]: { - minWidth: Number(constants.mobileNavWidth) - } - }, - navItemText: { - [breakpoints.down("md")]: { - color: palette.additional["gray"][3] + "&.selected": { + backgroundColor: palette.additional["gray"][5], + [breakpoints.down("md")]: { + "& span": { + color: palette.additional["gray"][9] + }, + "& svg": { + fill: palette.additional["gray"][9] + } + } } }, menuItem: { @@ -202,15 +197,15 @@ interface IAppNav { setNavOpen: (state: boolean) => void } +type AppNavTab = "buckets" | "cids" | "settings" + const AppNav: React.FC = ({ navOpen, setNavOpen }: IAppNav) => { const { desktop } = useThemeSwitcher() const classes = useStyles() - + const location = useLocation() const { storageSummary } = useStorage() - const { isLoggedIn, logout } = useStorageApi() - const signOut = useCallback(() => { logout() }, [logout]) @@ -221,6 +216,17 @@ const AppNav: React.FC = ({ navOpen, setNavOpen }: IAppNav) => { } }, [desktop, navOpen, setNavOpen]) + const appNavTab: AppNavTab | undefined = useMemo(() => { + const firstPathParam = location.pathname.split("/")[1] + switch(firstPathParam) { + case "cids": return "cids" + case "buckets": return "buckets" + case "bucket": return "buckets" + case "settings": return "settings" + default: return + } + }, [location]) + return (
= ({ navOpen, setNavOpen }: IAppNav) => { Buckets { - handleOnClick() - }} - className={classes.navItem} + onClick={handleOnClick} + className={clsx(classes.navItem, appNavTab === "cids" && "selected")} to={ROUTE_LINKS.Cids} > CIDs @@ -281,13 +283,12 @@ const AppNav: React.FC = ({ navOpen, setNavOpen }: IAppNav) => { Settings @@ -302,7 +303,6 @@ const AppNav: React.FC = ({ navOpen, setNavOpen }: IAppNav) => { Docs diff --git a/packages/storage-ui/src/Components/Modules/AddCIDModal.tsx b/packages/storage-ui/src/Components/Modules/AddCIDModal.tsx index 3d3f7cb70d..102b5d1231 100644 --- a/packages/storage-ui/src/Components/Modules/AddCIDModal.tsx +++ b/packages/storage-ui/src/Components/Modules/AddCIDModal.tsx @@ -1,30 +1,28 @@ -import React, { useCallback, useRef, useState } from "react" +import React, { useCallback, useEffect, useState } from "react" import { makeStyles, createStyles } from "@chainsafe/common-theme" -import { Button, FormikTextInput, Grid } from "@chainsafe/common-components" +import { Button, Grid, TextInput, Typography } from "@chainsafe/common-components" import CustomModal from "../Elements/CustomModal" import { CSSTheme } from "../../Themes/types" import CustomButton from "../Elements/CustomButton" import { t, Trans } from "@lingui/macro" -import { Formik, Form } from "formik" import { useStorage } from "../../Contexts/StorageContext" import { cidValidator } from "../../Utils/validationSchema" -const useStyles = makeStyles(({ constants, breakpoints, zIndex }: CSSTheme) => +const useStyles = makeStyles(({ constants, breakpoints, zIndex, palette, typography }: CSSTheme) => createStyles({ root: { padding: constants.generalUnit * 4, flexDirection: "column" }, modalRoot: { - zIndex: zIndex?.blocker, - [breakpoints.down("md")]: {} + zIndex: zIndex?.blocker }, modalInner: { backgroundColor: constants.createFolder.backgroundColor, color: constants.createFolder.color, + width: "100%", [breakpoints.down("md")]: { - bottom: - Number(constants?.mobileButtonHeight) + constants.generalUnit, + bottom: Number(constants?.mobileButtonHeight) + constants.generalUnit, borderTopLeftRadius: `${constants.generalUnit * 1.5}px`, borderTopRightRadius: `${constants.generalUnit * 1.5}px`, maxWidth: `${breakpoints.width("md")}px !important` @@ -45,13 +43,29 @@ const useStyles = makeStyles(({ constants, breakpoints, zIndex }: CSSTheme) => height: constants?.mobileButtonHeight } }, - label: { - fontSize: 14, - lineHeight: "22px" - }, heading: { color: constants.createFolder.color, + fontWeight: typography.fontWeight.semibold, + textAlign: "center", + marginBottom: constants.generalUnit * 2 + }, + inputLabel: { + fontSize: 14, + fontWeight: 600, marginBottom: constants.generalUnit + }, + cidInput: { + margin: 0, + width: "100%", + marginTop: constants.generalUnit * 2 + }, + errorText: { + marginTop: constants.generalUnit * 1, + color: palette.error.main + }, + warningText: { + marginTop: constants.generalUnit * 1, + color: palette.warning.main } }) ) @@ -63,97 +77,162 @@ interface IAddCIDModuleProps { const AddCIDModal = ({ modalOpen = false, close }: IAddCIDModuleProps) => { const classes = useStyles() - const { addPin, refreshPins } = useStorage() - const inputRef = useRef(null) + const { addPin, refreshPins, searchCid } = useStorage() const [accessingCID, setAccessingCID] = useState(false) + const [cidError, setCidError] = useState("") + const [cid, setCid] = useState("") + const [showWarning, setShowWarning] = useState(false) + const [name, setName] = useState("") + + const onClose = useCallback(() => { + setCid("") + setShowWarning(false) + setCidError("") + setName("") + close() + }, [close]) + + const onSubmit = useCallback(() => { + if (!cid) return - const onSubmit = useCallback((values, helpers) => { - helpers.setSubmitting(true) setAccessingCID(true) - addPin(values.cid.trim()) + + addPin(cid.trim(), name.trim()) .then(() => { - helpers.resetForm() - close() + onClose() }) .catch((e) => { - helpers.setFieldError("cid", e.message) console.error(e) }) .finally(() => { - refreshPins() + refreshPins(undefined) setAccessingCID(false) - helpers.setSubmitting(false) }) - }, [addPin, close, refreshPins]) + }, [addPin, cid, name, onClose, refreshPins]) + + useEffect(() => { + if (!cid) return + + cidValidator + .validate({ cid: cid.trim() }) + .then(() => { + searchCid(cid) + .then((res) => { + if(res && res.results?.length){ + setShowWarning(true) + } + }) + .catch(console.error) + }) + .catch((e: Error) => { + setCidError(e.message) + }) + }, [cid, cidError, searchCid]) + + const onCidChange = useCallback((cid?: string | number) => { + setCidError("") + setShowWarning(false) + + setCid(cid?.toString() || "") + }, []) + + const onNameChange = useCallback((name?: string | number) => { + setName(name?.toString() || "") + }, []) return ( - - -
+ + - Pin a CID + + + + {!!cidError && ( + - - - + )} + {showWarning && ( + - close()} - size="medium" - className={classes.cancelButton} - variant={"outline"} - type="button" - data-cy="button-cancel-add-pin" - > - Cancel - - - -
- -
+ Warning: CID already pinned + + )} + + + onClose()} + size="medium" + className={classes.cancelButton} + variant={"outline"} + type="button" + data-cy="button-cancel-add-pin" + > + Cancel + + + +
) } diff --git a/packages/storage-ui/src/Components/Modules/CreateFolderModal/CreateFolderModal.tsx b/packages/storage-ui/src/Components/Modules/CreateFolderModal/CreateFolderModal.tsx index 3cfadf65c8..542bc4a984 100644 --- a/packages/storage-ui/src/Components/Modules/CreateFolderModal/CreateFolderModal.tsx +++ b/packages/storage-ui/src/Components/Modules/CreateFolderModal/CreateFolderModal.tsx @@ -29,15 +29,13 @@ const useStyles = makeStyles( flexDirection: "column" }, modalRoot: { - zIndex: zIndex?.blocker, - [breakpoints.down("md")]: {} + zIndex: zIndex?.blocker }, modalInner: { backgroundColor: constants.createFolder.backgroundColor, color: constants.createFolder.color, [breakpoints.down("md")]: { - bottom: - Number(constants?.mobileButtonHeight) + constants.generalUnit, + bottom: Number(constants?.mobileButtonHeight) + constants.generalUnit, borderTopLeftRadius: `${constants.generalUnit * 1.5}px`, borderTopRightRadius: `${constants.generalUnit * 1.5}px`, maxWidth: `${breakpoints.width("md")}px !important` diff --git a/packages/storage-ui/src/Components/Modules/FileSystemItem/FileSystemGridItem.tsx b/packages/storage-ui/src/Components/Modules/FileSystemItem/FileSystemGridItem.tsx index 71beb08368..0a9284ed11 100644 --- a/packages/storage-ui/src/Components/Modules/FileSystemItem/FileSystemGridItem.tsx +++ b/packages/storage-ui/src/Components/Modules/FileSystemItem/FileSystemGridItem.tsx @@ -1,10 +1,11 @@ -import React, { useCallback, useEffect } from "react" +import React, { useCallback, useEffect, useState } from "react" import { makeStyles, createStyles, useThemeSwitcher } from "@chainsafe/common-theme" import { t } from "@lingui/macro" import clsx from "clsx" import { FormikTextInput, IMenuItem, + Loading, MenuDropdown, MoreIcon } from "@chainsafe/common-components" @@ -115,6 +116,10 @@ const useStyles = makeStyles(({ breakpoints, constants, palette }: CSSTheme) => [breakpoints.down("md")]: { padding: 0 } + }, + loadingIcon: { + marginLeft: constants.generalUnit, + verticalAlign: "middle" } }) }) @@ -125,12 +130,12 @@ interface IFileSystemTableItemProps { isOverUpload: boolean selected: ISelectedFile[] file: FileSystemItem - editing: ISelectedFile | undefined + editing?: string onFolderOrFileClicks: (e?: React.MouseEvent) => void icon: React.ReactNode preview: ConnectDragPreview setEditing: (editing: ISelectedFile | undefined) => void - handleRename?: (toRename: ISelectedFile, newPath: string) => Promise + handleRename?: (cid: string, newName: string) => Promise | undefined currentPath: string | undefined menuItems: IMenuItem[] resetSelectedFiles: () => void @@ -155,6 +160,7 @@ const FileSystemGridItem = React.forwardRef( const classes = useStyles() const { name, cid } = file const { desktop } = useThemeSwitcher() + const [isEditingLoading, setIsEditingLoading] = useState(false) const formik = useFormik({ initialValues: { @@ -164,10 +170,14 @@ const FileSystemGridItem = React.forwardRef( onSubmit: (values) => { const newName = values.fileName?.trim() - newName && handleRename && handleRename({ - cid: file.cid, - name: file.name - }, newName) + if (newName !== name && !!newName && handleRename) { + setIsEditingLoading(true) + + handleRename(file.cid, newName) + ?.then(() => setIsEditingLoading(false)) + } else { + stopEditing() + } }, enableReinitialize: true }) @@ -224,11 +234,11 @@ const FileSystemGridItem = React.forwardRef( > {icon}
- {(editing?.cid === cid && editing.name === name) && desktop ? ( + {editing === cid && desktop ? (
- ) : ( -
{name}
- )} -
-
- + ) :
+ {name}{isEditingLoading && } +
+ }
+ +
) } diff --git a/packages/storage-ui/src/Components/Modules/FileSystemItem/FileSystemItem.tsx b/packages/storage-ui/src/Components/Modules/FileSystemItem/FileSystemItem.tsx index eeb956b69e..f959d0feb4 100644 --- a/packages/storage-ui/src/Components/Modules/FileSystemItem/FileSystemItem.tsx +++ b/packages/storage-ui/src/Components/Modules/FileSystemItem/FileSystemItem.tsx @@ -107,9 +107,9 @@ interface IFileSystemItemProps { selected: ISelectedFile[] handleSelectCid(selectedFile: ISelectedFile): void handleAddToSelectedCids(selectedFile: ISelectedFile): void - editing: ISelectedFile | undefined + editing?: string setEditing(editing: ISelectedFile | undefined): void - handleRename?: (toRename: ISelectedFile, newName: string) => Promise + handleRename?: (cid: string, newName: string) => Promise | undefined handleMove?: (toMove: ISelectedFile, newPath: string) => Promise deleteFile?: () => void recoverFile?: (toRecover: ISelectedFile) => void @@ -158,9 +158,7 @@ const FileSystemItem = ({ const classes = useStyles() const formik = useFormik({ - initialValues: { - name - }, + initialValues: { name }, validationSchema: nameValidator, onSubmit: (values: {name: string}) => { const newName = values.name.trim() @@ -180,10 +178,7 @@ const FileSystemItem = ({ ), - onClick: () => setEditing({ - cid, - name - }) + onClick: () => setEditing({ cid, name }) }, delete: { contents: ( @@ -434,7 +429,7 @@ const FileSystemItem = ({ : } { - (editing?.cid === cid && editing.name === name) && !desktop && ( + editing === cid && !desktop && ( <> setEditing(undefined)} > @@ -462,7 +456,7 @@ const FileSystemItem = ({ className={classes.renameInput} name="name" placeholder={isFolder ? t`Please enter a folder name` : t`Please enter a file name`} - autoFocus={editing.cid === cid} + autoFocus />