diff --git a/.eslintrc.json b/.eslintrc.json index d046b33919..a4741cc5a0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -38,10 +38,8 @@ "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", "react/prop-types": 0, - "react/jsx-max-props-per-line": [2, { - "maximum": 1, - "when": "always" - }], + "react/jsx-max-props-per-line": ["error", { "maximum": 1, "when": "always" }], + "react/jsx-first-prop-new-line": ["error", "multiline-multiprop"], "react/jsx-fragments": "error", "arrow-spacing": "error", "space-infix-ops": "error", diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..5907b2d27d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +closes # + +--- + +Submission checklist: + + + +#### Layout +- [] Change looks good in the desktop web ui +- - [] Change looks good in the mobile web ui + +#### Theme +- [] Components / elements inspected in light mode +- [] Components / elements inspected in dark mode \ No newline at end of file diff --git a/.github/workflows/lingui-extract-gaming.yml b/.github/workflows/lingui-extract-gaming.yml deleted file mode 100644 index aca20cda66..0000000000 --- a/.github/workflows/lingui-extract-gaming.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Lingui extract - Gaming -on: - pull_request: - branches: - - "dev" - paths: - - "packages/gaming-ui/**/*" -jobs: - # extract any new translatable string - # and commit if there are new ones - # and if the user submitting the PR isn't weblate - extract: - name: lingui-extract - runs-on: ubuntu-latest - if: ${{ github.actor != 'weblate' }} - steps: - - uses: actions/checkout@v2 - with: - ref: ${{ github.event.pull_request.head.ref }} - ssh-key: ${{ secrets.LINGUI_GH_ACTION_COMMIT_KEY }} - - name: set user - run: | - git config --global user.name 'GitHub Actions' - git config --global user.email 'actions@github.com' - - # use node module caching - - uses: actions/cache@v2 - with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - - - name: install packages - run: yarn install --immutable - - - name: lingui-extract and commit - if: ${{ github.actor != 'weblate' }} - run: | - (cd packages/gaming-ui && yarn extract --clean) - git add packages/gaming-ui/src/locales/* - if git commit -m "lingui extract"; then git push; else echo 'exiting successfully without commit'; fi diff --git a/README.md b/README.md index b9736e21ad..18ec6322b1 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,6 @@ Theme context and other utilities for generating and using the theming capabilit The UI for Chainsafe Files -#### **`packages/gaming-ui`** - -The UI for Chainsafe Gaming - #### **`packages/storage-ui`** The UI for Chainsafe Storage diff --git a/package.json b/package.json index a62f07950c..703957bf2c 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "eslint": "^6.8.0", "eslint-plugin-cypress": "^2.11.3", "eslint-plugin-no-only-tests": "^2.6.0", - "eslint-plugin-react": "^7.22.0", + "eslint-plugin-react": "^7.27.1", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-ternary": "^1.0.4", "npm-run-all": "^4.1.5", @@ -38,9 +38,6 @@ "build:files-ui": "yarn wsrun -p files-ui -c build", "release:files-ui": "yarn wsrun -p files-ui -c release", "start:files-ui": "yarn wsrun -p files-ui -c start", - "build:gaming-ui": "yarn wsrun -p gaming-ui -c build", - "release:gaming-ui": "yarn wsrun -p gaming-ui -c release", - "start:gaming-ui": "yarn wsrun -p gaming-ui -c start", "build:storage-ui": "yarn wsrun -p storage-ui -c build", "release:storage-ui": "yarn wsrun -p storage-ui -c release", "start:storage-ui": "yarn wsrun -p storage-ui -c start", diff --git a/packages/common-components/package.json b/packages/common-components/package.json index a694ce3a86..db8116bd7a 100644 --- a/packages/common-components/package.json +++ b/packages/common-components/package.json @@ -10,7 +10,7 @@ }, "scripts": { "storybook": "start-storybook -p 6006", - "build:storybook": "build-storybook", + "build:storybook": "(export NODE_OPTIONS=--openssl-legacy-provider; build-storybook)", "build": "rollup -c", "start": "rollup -c -w", "lint": "eslint './{src, cypress}/**/*.{js,jsx,ts,tsx}'" diff --git a/packages/common-components/src/Button/Button.tsx b/packages/common-components/src/Button/Button.tsx index 0900773aaf..01b7a8c37a 100644 --- a/packages/common-components/src/Button/Button.tsx +++ b/packages/common-components/src/Button/Button.tsx @@ -93,7 +93,7 @@ const useStyles = makeStyles( fill: palette.common.white.main }, "&:hover": { - backgroundColor: palette.primary.main, + backgroundColor: palette.primary.hover, color: palette.common.white.main, ...overrides?.Button?.variants?.secondary?.hover }, @@ -109,6 +109,20 @@ const useStyles = makeStyles( }, ...overrides?.Button?.variants?.secondary?.root }, + text: { + backgroundColor: "transparent", + color: palette.additional["gray"][9], + "&:hover": { + ...overrides?.Button?.variants?.text?.hover + }, + "&:focus": { + ...overrides?.Button?.variants?.text?.focus + }, + "&:active": { + ...overrides?.Button?.variants?.text?.active + }, + ...overrides?.Button?.variants?.text?.root + }, tertiary: { backgroundColor: palette.additional["gray"][3], color: palette.common.black.main, @@ -293,7 +307,7 @@ interface IButtonProps extends Omit { className?: string children?: ReactNode | ReactNode[] fullsize?: boolean - variant?: "link" | "primary" | "secondary" |"tertiary" | "outline" | "dashed" | "danger" + variant?: "link" | "primary" | "secondary" |"tertiary" | "outline" | "dashed" | "danger" | "text" iconButton?: boolean size?: "large" | "medium" | "small" type?: "button" | "submit" | "reset" @@ -347,7 +361,7 @@ const Button: React.FC = ({ {loading && ( <> diff --git a/packages/common-components/src/CheckboxInput/CheckboxInput.tsx b/packages/common-components/src/CheckboxInput/CheckboxInput.tsx index 7d508154fa..8a1f4375a3 100644 --- a/packages/common-components/src/CheckboxInput/CheckboxInput.tsx +++ b/packages/common-components/src/CheckboxInput/CheckboxInput.tsx @@ -10,6 +10,7 @@ const useStyles = makeStyles( root: { cursor: "pointer", display: "flex", + alignItems: "center", ...overrides?.CheckboxInput?.root }, checkbox: { diff --git a/packages/common-components/src/Icons/icons/Sort.icon.tsx b/packages/common-components/src/Icons/icons/Sort.icon.tsx new file mode 100644 index 0000000000..680e24b8df --- /dev/null +++ b/packages/common-components/src/Icons/icons/Sort.icon.tsx @@ -0,0 +1,7 @@ +import * as React from "react" +import createSvgIcon from "../createSvgIcon" +import { ReactComponent as SortSvg } from "../svgs/sort.svg" + +export { SortSvg } + +export default createSvgIcon() diff --git a/packages/common-components/src/Icons/index.ts b/packages/common-components/src/Icons/index.ts index 6394b68825..e6286214f7 100644 --- a/packages/common-components/src/Icons/index.ts +++ b/packages/common-components/src/Icons/index.ts @@ -64,6 +64,7 @@ export { default as SearchIcon, SearchSvg } from "./icons/Search.icon" export { default as SettingIcon, SettingSvg } from "./icons/Setting.icon" export { default as ShareAltIcon, ShareAltSvg } from "./icons/ShareAlt.icon" export { default as StarIcon, StarSvg } from "./icons/Star.icon" +export { default as SortIcon, SortSvg } from "./icons/Sort.icon" export { default as SunIcon, SunSvg } from "./icons/Sun.icon" export { default as TableIcon, TableSvg } from "./icons/Table.icon" export { default as UpdateIcon, UpdateSvg } from "./icons/Update.icon" diff --git a/packages/common-components/src/Icons/svgs/document.svg b/packages/common-components/src/Icons/svgs/document.svg index da7d381789..b805583c15 100644 --- a/packages/common-components/src/Icons/svgs/document.svg +++ b/packages/common-components/src/Icons/svgs/document.svg @@ -1,3 +1 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/packages/common-components/src/Icons/svgs/ethereum-logo.svg b/packages/common-components/src/Icons/svgs/ethereum-logo.svg index 15eb15600a..7fe1413ff0 100644 --- a/packages/common-components/src/Icons/svgs/ethereum-logo.svg +++ b/packages/common-components/src/Icons/svgs/ethereum-logo.svg @@ -1,13 +1 @@ - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/packages/common-components/src/Icons/svgs/sort.svg b/packages/common-components/src/Icons/svgs/sort.svg new file mode 100644 index 0000000000..00c6c7e54e --- /dev/null +++ b/packages/common-components/src/Icons/svgs/sort.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/common-components/src/Modal/Modal.tsx b/packages/common-components/src/Modal/Modal.tsx index 63db06daa0..92d95e9bc4 100644 --- a/packages/common-components/src/Modal/Modal.tsx +++ b/packages/common-components/src/Modal/Modal.tsx @@ -94,6 +94,7 @@ const useStyles = makeStyles( margin: "auto", maxHeight: "100%", overflow: "auto", + alignItems: "center", "&.xs": { width: `calc(100% - ${constants.generalUnit * 2}px)`, maxWidth: breakpoints.width("xs"), diff --git a/packages/common-components/src/SelectInput/SelectInput.tsx b/packages/common-components/src/SelectInput/SelectInput.tsx index 802587398d..2af294f69b 100644 --- a/packages/common-components/src/SelectInput/SelectInput.tsx +++ b/packages/common-components/src/SelectInput/SelectInput.tsx @@ -8,7 +8,6 @@ const useStyles = makeStyles( ({ animation, constants, palette, overrides }: ITheme) => createStyles({ root: { - margin: 5, display: "block", ...overrides?.SelectInput?.root }, diff --git a/packages/common-components/src/Spinner/Loading.tsx b/packages/common-components/src/Spinner/Loading.tsx index 96e5c97287..e22b4e53f2 100644 --- a/packages/common-components/src/Spinner/Loading.tsx +++ b/packages/common-components/src/Spinner/Loading.tsx @@ -1,8 +1,8 @@ -import React from "react" +import React, { useMemo } from "react" import { ITheme, useTheme } from "@chainsafe/common-theme" export interface ILoadingProps { - type?: "inherit" | "primary" | "dark" | "light" + type?: "initial" | "primary" | "dark" | "light" size?: number className?: string } @@ -13,6 +13,42 @@ const Loading: React.FC = ({ className }) => { const theme: ITheme = useTheme() + + const uniqueKey = `${Math.random()}-${Math.random()}` + + const { + start, + end + } = useMemo(() => { + switch (type) { + case "primary": + return { + start: theme.palette.primary.main, + end: "transparent" + } + case "light": + return { + start: theme.palette.common.white.main, + end: "transparent" + } + case "dark": + return { + start: theme.palette.common.black.main, + end: "transparent" + } + case "initial": + return { + start: "#FFFFFF", + end: "transparent" + } + default: + return { + start: "#FFFFFF", + end: "transparent" + } + } + }, [type, theme.palette]) + return ( = ({ > = ({ > @@ -50,7 +78,7 @@ const Loading: React.FC = ({ cx="50" cy="50" r="47" - stroke="url(#gradient)" + stroke={`url(#gradient-${uniqueKey})`} strokeWidth="6" fill="none" transform="rotate(90 50 50)" diff --git a/packages/common-components/src/Toasts/ToastContent.tsx b/packages/common-components/src/Toasts/ToastContent.tsx index 5f74826548..c4e2df542f 100644 --- a/packages/common-components/src/Toasts/ToastContent.tsx +++ b/packages/common-components/src/Toasts/ToastContent.tsx @@ -105,7 +105,8 @@ const ToastContent = ({ toast, onClose }: ToastContentProps) => { /> {onProgressCancel && onProgressCancelLoading - ? : { activeKey={tab} onTabSelect={setTab} > - first diff --git a/packages/common-components/src/stories/Toasts.stories.tsx b/packages/common-components/src/stories/Toasts.stories.tsx index a18f1feffa..8e842496eb 100644 --- a/packages/common-components/src/stories/Toasts.stories.tsx +++ b/packages/common-components/src/stories/Toasts.stories.tsx @@ -107,10 +107,11 @@ const ToastNotificationDemo: React.FC<{toast: ToastParams}> = ({ toast }) => {
{toasts.map((toast) => ( toast.progress && toast.progress < 80 ? ( - ) : null diff --git a/packages/common-theme/src/Overrides/Button.ts b/packages/common-theme/src/Overrides/Button.ts index 106c92e11c..6f44e7e8a6 100644 --- a/packages/common-theme/src/Overrides/Button.ts +++ b/packages/common-theme/src/Overrides/Button.ts @@ -42,6 +42,18 @@ export interface IButtonOverride { focus?: Record active?: Record } + link?: { + root?: Record + hover?: Record + focus?: Record + active?: Record + } + text?: { + root?: Record + hover?: Record + focus?: Record + active?: Record + } } state?: { danger?: { diff --git a/packages/common-theme/src/Provider/ThemeSwitcherContext.tsx b/packages/common-theme/src/Provider/ThemeSwitcherContext.tsx index ffe8dc549a..aafb1d886c 100644 --- a/packages/common-theme/src/Provider/ThemeSwitcherContext.tsx +++ b/packages/common-theme/src/Provider/ThemeSwitcherContext.tsx @@ -10,6 +10,8 @@ import { useLocalStorage } from "@chainsafe/browser-storage-hooks" type ThemeSwitcherContext = { desktop: boolean + tablet: boolean + mobile: boolean themeKey: string availableThemes: string[] setTheme(themeName: string): void @@ -26,6 +28,8 @@ type ThemeSwitcherProps = { const ThemeSwitcher = ({ children, themes, storageKey = "cs.themeKey" }: ThemeSwitcherProps) => { const breakpoints = createBreakpoints({}) const desktop = useMediaQuery(breakpoints.up("md")) + const tablet = useMediaQuery(breakpoints.up("sm")) + const mobile = useMediaQuery(breakpoints.up("xs")) const { canUseLocalStorage, localStorageGet, localStorageSet } = useLocalStorage() // TODO: check min 1 theme @@ -60,6 +64,8 @@ const ThemeSwitcher = ({ children, themes, storageKey = "cs.themeKey" }: ThemeSw { ... }) import { authenticationPage } from "./page-objects/authenticationPage" -import { apiTestHelper } from "./utils/apiTestHelper" +import { apiTestHelper, ClearBucketType } from "./utils/apiTestHelper" import { ethers, Wallet } from "ethers" import { homePage } from "./page-objects/homePage" import { testPrivateKey, testAccountPassword, localHost } from "../fixtures/loginData" import { CustomizedBridge } from "./utils/CustomBridge" import "cypress-file-upload" import "cypress-pipe" -import { BucketType } from "@chainsafe/files-api-client" import { navigationMenu } from "./page-objects/navigationMenu" -Cypress.Commands.add("clearBucket", (bucketType: BucketType) => { +Cypress.Commands.add("clearBucket", (bucketType: ClearBucketType) => { apiTestHelper.clearBucket(bucketType) }) @@ -47,6 +46,7 @@ export interface Web3LoginOptions { clearCSFBucket?: boolean clearTrashBucket?: boolean deleteShareBucket?: boolean + withNewUser?: boolean } Cypress.Commands.add( @@ -56,7 +56,8 @@ Cypress.Commands.add( url = localHost, clearCSFBucket = false, clearTrashBucket = false, - deleteShareBucket = false + deleteShareBucket = false, + withNewUser = true }: Web3LoginOptions = {}) => { cy.on("window:before:load", (win) => { @@ -64,32 +65,49 @@ Cypress.Commands.add( "https://rinkeby.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847", 4 ) - const signer = new Wallet(testPrivateKey, provider) + const signer = withNewUser + ? Wallet.createRandom() + : new Wallet(testPrivateKey, provider) // inject ethereum object in the global window Object.defineProperty(win, "ethereum", { - get: () => new CustomizedBridge(signer as any, provider as any) + get: () => new CustomizedBridge(signer as any, signer.address, provider as any) }) }) - // with nothing in localstorage (and in session storage) - // the whole login flow should kick in - cy.session("web3login", () => { - cy.visit(url) - authenticationPage.web3Button().click() - authenticationPage.showMoreButton().click() - authenticationPage.detectedWallet().click() - authenticationPage.web3SignInButton().safeClick() - authenticationPage.loginPasswordButton().click() - authenticationPage.loginPasswordInput().type(`${testAccountPassword}{enter}`) - - if (saveBrowser) { - // this is taking forever for test accounts - authenticationPage.saveBrowserButton().click() - } else { - authenticationPage.doNotSaveBrowserButton().click() - } - homePage.appHeaderLabel().should("be.visible") - }) + if (withNewUser){ + cy.session("web3loginNewUser", () => { + cy.visit(url) + authenticationPage.web3Button().click() + authenticationPage.showMoreButton().click() + authenticationPage.detectedWallet().click() + authenticationPage.web3SignInButton().safeClick() + authenticationPage.signInExplainerContinueButton().safeClick() + // we are using the testAccount password here, but we could input anything + authenticationPage.signInSetupPasswordInput().type(`${testAccountPassword}`) + authenticationPage.signInSetupPasswordVerificationInput().type(`${testAccountPassword}{enter}`) + + homePage.appHeaderLabel().should("be.visible") + }) + } else { + cy.session("web3loginTestUser", () => { + cy.visit(url) + authenticationPage.web3Button().click() + authenticationPage.showMoreButton().click() + authenticationPage.detectedWallet().click() + authenticationPage.web3SignInButton().safeClick() + authenticationPage.loginPasswordButton().click() + authenticationPage.loginPasswordInput().type(`${testAccountPassword}{enter}`) + + if (saveBrowser) { + // this is taking forever for test accounts + authenticationPage.saveBrowserButton().click() + } else { + authenticationPage.doNotSaveBrowserButton().click() + } + homePage.appHeaderLabel().should("be.visible") + }) + } + cy.visit(url) homePage.appHeaderLabel().should("be.visible") @@ -115,7 +133,7 @@ Cypress.Commands.add( } ) -Cypress.Commands.add("safeClick", { prevSubject: "element" }, $element => { +Cypress.Commands.add("safeClick", { prevSubject: "element" }, ($element?: JQuery) => { const click = ($el: JQuery) => $el.trigger("click") return cy .wrap($element) @@ -137,9 +155,10 @@ declare global { * @param {Boolean} options.clearCSFBucket - (default: false) - whether any file in the csf bucket should be deleted. * @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. * @example cy.web3Login({saveBrowser: true, url: 'http://localhost:8080'}) */ - web3Login: (options?: Web3LoginOptions) => Chainable + web3Login: (options?: Web3LoginOptions) => void /** * Use this when encountering race condition issues resulting in @@ -153,8 +172,14 @@ declare global { * https://github.com/cypress-io/cypress/issues/7306 * */ - safeClick: () => Chainable + safeClick: ($element?: JQuery) => Chainable + /** + * Clear a bucket. + * @param {BucketType} - what bucket type to clear for this user. + * @example cy.clearBucket("csf") + */ + clearBucket: (bucketType: ClearBucketType) => void } } } diff --git a/packages/files-ui/cypress/support/index.ts b/packages/files-ui/cypress/support/index.ts index f0febdb6c2..80de34037f 100644 --- a/packages/files-ui/cypress/support/index.ts +++ b/packages/files-ui/cypress/support/index.ts @@ -19,13 +19,9 @@ 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) => { +Cypress.on("uncaught:exception", () => { /* 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 - } + return false }) // Hide fetch/XHR requests diff --git a/packages/files-ui/cypress/support/page-objects/authenticationPage.ts b/packages/files-ui/cypress/support/page-objects/authenticationPage.ts index 2c408c7cd3..5d8e68b258 100644 --- a/packages/files-ui/cypress/support/page-objects/authenticationPage.ts +++ b/packages/files-ui/cypress/support/page-objects/authenticationPage.ts @@ -12,6 +12,10 @@ export const authenticationPage = { // sign in section elements loginPasswordButton: () => cy.get("[data-cy=login-password-button]", { timeout: 20000 }), loginPasswordInput: () => cy.get("[data-cy=login-password-input]"), + signInExplainerContinueButton: () => cy.get("[data-cy=button-sign-in-explainer-continue]", { timeout: 20000 }), + signInSetupPasswordInput: () => cy.get("[data-cy=input-sign-in-password]"), + signInSetupPasswordVerificationInput: () => cy.get("[data-cy=input-sign-in-password-verification]"), + signInSetupPasswordSubmitButton: () => cy.get("[data-cy=button-sign-in-password]"), // save browser section elements saveBrowserButton: () => cy.get("[data-cy=save-browser-button]"), diff --git a/packages/files-ui/cypress/support/page-objects/modals/createSharedFolderModal.ts b/packages/files-ui/cypress/support/page-objects/modals/createSharedFolderModal.ts index 60659ab35d..f709cdfc09 100644 --- a/packages/files-ui/cypress/support/page-objects/modals/createSharedFolderModal.ts +++ b/packages/files-ui/cypress/support/page-objects/modals/createSharedFolderModal.ts @@ -7,5 +7,17 @@ export const createEditSharedFolderModal = { tagViewPermissionUser: () => cy.get("[data-cy=tag-view-permission-user]"), tagEditPermissionUser: () => cy.get("[data-cy=tag-edit-permission-user]"), updateButton: () => cy.get("[data-cy=button-update-shared-folder]", { timeout: 10000 }), - viewOnlyPermissionInput: () => cy.get("[data-cy=input-view-permission") -} \ No newline at end of file + viewOnlyPermissionInput: () => cy.get("[data-cy=input-view-permission"), + + // link sharing related elements + shareLink: () => cy.get("[data-cy=link-share]"), + activeShareLink: () => cy.get("[data-cy=link-active-share]"), + labelPermissionType: () => cy.get("[data-cy=label-permission-type]"), + copyLinkButton: () => cy.get("[data-cy=button-copy-link]"), + linkKebabMenu: () => cy.get("[data-testid=menu-title-link-kebab]"), + deleteLinkMenuOption: () => cy.get("[data-cy=menu-delete-active-link]"), + permissionTypeDropdown: () => cy.get("[data-testid=dropdown-title-permission]"), + viewOnlyOption: () => cy.get("[data-cy=menu-read]"), + canEditOption: () => cy.get("[data-cy=menu-write]"), + createLinkButton: () => cy.get("[data-cy=button-create-link]") +} diff --git a/packages/files-ui/cypress/support/page-objects/sharedPage.ts b/packages/files-ui/cypress/support/page-objects/sharedPage.ts index 072482022d..2c25afc7a4 100644 --- a/packages/files-ui/cypress/support/page-objects/sharedPage.ts +++ b/packages/files-ui/cypress/support/page-objects/sharedPage.ts @@ -1,5 +1,7 @@ +import { sharedFolderName } from "../../fixtures/filesTestData" import { basePage } from "./basePage" import { fileBrowser } from "./fileBrowser" +import { createEditSharedFolderModal } from "./modals/createSharedFolderModal" export const sharedPage = { ...basePage, @@ -15,5 +17,15 @@ export const sharedPage = { leaveMenuOption: () => cy.get("[data-cy=menu-leave]"), manageAccessMenuOption: () => cy.get("[data-cy=menu-manage-access]"), renameMenuOption: () => cy.get("[data-cy=menu-rename]"), - uploadButton: () => cy.get("[data-cy=button-upload-file]") + uploadButton: () => cy.get("[data-cy=button-upload-file]"), + + // helpers and convenience functions + createSharedFolder() { + sharedPage.createSharedFolderButton().click() + createEditSharedFolderModal.body().should("be.visible") + createEditSharedFolderModal.folderNameInput().type(sharedFolderName) + createEditSharedFolderModal.createButton().safeClick() + createEditSharedFolderModal.body().should("not.exist") + sharedPage.sharedFolderItemRow().should("have.length", 1) + } } diff --git a/packages/files-ui/cypress/support/utils/CustomBridge.ts b/packages/files-ui/cypress/support/utils/CustomBridge.ts index 4924ecbbff..ff1fd4c19c 100644 --- a/packages/files-ui/cypress/support/utils/CustomBridge.ts +++ b/packages/files-ui/cypress/support/utils/CustomBridge.ts @@ -1,8 +1,15 @@ import { Eip1193Bridge } from "@ethersproject/experimental/lib/eip1193-bridge" +import { ethers } from "ethers" import { toUtf8String } from "ethers/lib/utils" -import { testAddress } from "../../fixtures/loginData" export class CustomizedBridge extends Eip1193Bridge { + expectedAddress = "" + + constructor(signer: ethers.Signer, address: string, provider?: ethers.providers.Provider) { + super(signer as any, provider as any) + this.expectedAddress = address + } + async sendAsync(...args: Array) { return this.send(...args) } @@ -30,17 +37,17 @@ export class CustomizedBridge extends Eip1193Bridge { const message = params[0] if ( - (addr as string).toLowerCase() !== testAddress.toLowerCase() + (addr as string).toLowerCase() !== this.expectedAddress.toLowerCase() ) { return Promise.reject( - `Wrong address, expected ${testAddress}, but got ${addr}` + `Wrong address, expected ${this.expectedAddress}, but got ${addr}` ) } try { const sig = await this.signer.signMessage(toUtf8String(message)) return sig - } catch (e) { + } catch (e: any) { return Promise.reject( `Error in CustomizedBridge for personal_sign: ${e.message}` ) @@ -49,9 +56,9 @@ export class CustomizedBridge extends Eip1193Bridge { if (method === "eth_requestAccounts" || method === "eth_accounts") { if (isCallbackForm) { - callback({ result: [testAddress] }) + callback({ result: [this.expectedAddress] }) } else { - return Promise.resolve([testAddress]) + return Promise.resolve([this.expectedAddress]) } } diff --git a/packages/files-ui/cypress/support/utils/apiTestHelper.ts b/packages/files-ui/cypress/support/utils/apiTestHelper.ts index 50cd2e053a..4be4ea8ab8 100644 --- a/packages/files-ui/cypress/support/utils/apiTestHelper.ts +++ b/packages/files-ui/cypress/support/utils/apiTestHelper.ts @@ -7,6 +7,7 @@ import { homePage } from "../page-objects/homePage" const API_BASE_USE = "https://stage.imploy.site/api/v1" const REFRESH_TOKEN_KEY = "csf.refreshToken" +export type ClearBucketType = Exclude const getApiClient = () => { // Disable the internal Axios JSON deserialization as this is handled by the client const axiosInstance = axios.create({ transformResponse: [] }) @@ -34,7 +35,7 @@ export const apiTestHelper = { }) }) }, - clearBucket(bucketType: BucketType) { + clearBucket(bucketType: ClearBucketType) { const apiClient = getApiClient() return new Cypress.Promise(async (resolve) => { diff --git a/packages/files-ui/cypress/tests/file-preview-spec.ts b/packages/files-ui/cypress/tests/file-preview-spec.ts index 6f2dc17cee..655e1c68e5 100644 --- a/packages/files-ui/cypress/tests/file-preview-spec.ts +++ b/packages/files-ui/cypress/tests/file-preview-spec.ts @@ -51,6 +51,80 @@ describe("File Preview", () => { homePage.appHeaderLabel().should("exist") }) + it("can navigate through preview using keyboard hotkeys", () => { + cy.web3Login({ clearCSFBucket: true }) + + // add files + homePage.uploadFile("../fixtures/uploadedFiles/chainsafe.png") + homePage.uploadFile("../fixtures/uploadedFiles/logo.png") + homePage.uploadFile("../fixtures/uploadedFiles/text-file.txt") + homePage.fileItemRow().should("have.length", 3) + + // store the 3 file names as cypress aliases for later comparison + homePage.fileItemName().eq(0).invoke("text").as("fileNameA") + homePage.fileItemName().eq(1).invoke("text").as("fileNameB") + homePage.fileItemName().eq(2).invoke("text").as("fileNameC") + + // navigate to the preview modal for the first file + homePage.fileItemKebabButton().first().click() + homePage.previewMenuOption().eq(0).click() + previewModal.body().should("exist") + + // ensure the correct file is being previewed + cy.get("@fileNameA").then(($fileNameA) => { + previewModal.fileNameLabel().should("contain.text", $fileNameA) + previewModal.contentContainer() + .should("be.visible") + .should("not.have.text", "Loading preview") + previewModal.unsupportedFileLabel().should("not.exist") + }) + + // 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) + previewModal.contentContainer() + .should("be.visible") + .should("not.have.text", "Loading preview") + previewModal.unsupportedFileLabel().should("not.exist") + }) + + // 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) + previewModal.contentContainer() + .should("be.visible") + .should("not.have.text", "Loading preview") + previewModal.unsupportedFileLabel().should("not.exist") + }) + + // 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) + previewModal.contentContainer() + .should("be.visible") + .should("not.have.text", "Loading preview") + previewModal.unsupportedFileLabel().should("not.exist") + }) + + // 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) + previewModal.contentContainer() + .should("be.visible") + .should("not.have.text", "Loading preview") + previewModal.unsupportedFileLabel().should("not.exist") + }) + + // exit preview via the escape key + cy.get("body").type("{esc}") + previewModal.body().should("not.exist") + homePage.appHeaderLabel().should("exist") + }) + it("can see option to download file from the preview screen", () => { cy.web3Login({ clearCSFBucket: true }) homePage.uploadFile("../fixtures/uploadedFiles/logo.png") diff --git a/packages/files-ui/cypress/tests/link-sharing-spec.ts b/packages/files-ui/cypress/tests/link-sharing-spec.ts new file mode 100644 index 0000000000..ebf27609a6 --- /dev/null +++ b/packages/files-ui/cypress/tests/link-sharing-spec.ts @@ -0,0 +1,77 @@ +import { createEditSharedFolderModal } from "../support/page-objects/modals/createSharedFolderModal" +import { navigationMenu } from "../support/page-objects/navigationMenu" +import { sharingExplainerKey } from "../fixtures/filesTestData" +import { sharedPage } from "../support/page-objects/sharedPage" + +describe("Link Sharing", () => { + + context("desktop", () => { + + it("can create, copy and remove links to shared folders", () => { + // intercept and stub the response to ensure the explainer is not displayed + cy.intercept("GET", "**/user/store", { + body: { [sharingExplainerKey]: "true" } + }) + + cy.web3Login({ deleteShareBucket: true }) + + // create a shared folder + navigationMenu.sharedNavButton().click() + sharedPage.createSharedFolder() + sharedPage.fileItemKebabButton() + .should("be.visible") + .click() + sharedPage.manageAccessMenuOption().click() + createEditSharedFolderModal.body().should("be.visible") + + // ensure default state of displayed elements is correct + createEditSharedFolderModal.activeShareLink().should("not.exist") + createEditSharedFolderModal.labelPermissionType().should("not.exist") + createEditSharedFolderModal.copyLinkButton().should("not.exist") + createEditSharedFolderModal.linkKebabMenu().should("not.exist") + createEditSharedFolderModal.permissionTypeDropdown().should("be.visible") + createEditSharedFolderModal.createLinkButton().should("be.visible") + + // ensure "view-only" and "can-edit" options are present + createEditSharedFolderModal.permissionTypeDropdown().click() + createEditSharedFolderModal.viewOnlyOption() + .scrollIntoView() + .should("be.visible") + createEditSharedFolderModal.canEditOption() + .scrollIntoView() + .should("be.visible") + + // create a "view-only" link + createEditSharedFolderModal.viewOnlyOption().click() + createEditSharedFolderModal.createLinkButton().click() + createEditSharedFolderModal.activeShareLink().should("have.length", 1) + createEditSharedFolderModal.labelPermissionType().should("have.length", 1) + createEditSharedFolderModal.copyLinkButton().should("have.length", 1) + + // ensure only the can-edit option is present if a "view-only" link exists + createEditSharedFolderModal.permissionTypeDropdown().click() + createEditSharedFolderModal.viewOnlyOption().should("not.exist") + createEditSharedFolderModal.canEditOption() + .scrollIntoView() + .should("be.visible") + + // create a "can-edit" link + createEditSharedFolderModal.canEditOption().click() + createEditSharedFolderModal.createLinkButton().click() + createEditSharedFolderModal.activeShareLink().should("have.length", 2) + createEditSharedFolderModal.labelPermissionType().should("have.length", 2) + createEditSharedFolderModal.copyLinkButton().should("have.length", 2) + + // ensure create button and drop down are not shown if links exist + createEditSharedFolderModal.permissionTypeDropdown().should("not.exist") + createEditSharedFolderModal.createLinkButton().should("not.exist") + + // delete one of the links + createEditSharedFolderModal.linkKebabMenu().first().click() + createEditSharedFolderModal.deleteLinkMenuOption().first() + .scrollIntoView() + .click() + createEditSharedFolderModal.activeShareLink().should("have.length", 1) + }) + }) +}) diff --git a/packages/files-ui/cypress/tests/survey-banner-spec.ts b/packages/files-ui/cypress/tests/survey-banner-spec.ts index fb66f27708..eb9c08a074 100644 --- a/packages/files-ui/cypress/tests/survey-banner-spec.ts +++ b/packages/files-ui/cypress/tests/survey-banner-spec.ts @@ -20,7 +20,7 @@ describe("Survey Banner", () => { body: { [dismissedSurveyKey]: "false" } }) - cy.web3Login() + cy.web3Login({ withNewUser: false }) homePage.surveyBanner().should("be.visible") // set up a spy for the POST response @@ -42,7 +42,7 @@ describe("Survey Banner", () => { body: { [dismissedSurveyKey]: "true" } }) - cy.web3Login() + cy.web3Login({ withNewUser: false }) homePage.surveyBanner().should("not.exist") }) @@ -51,7 +51,7 @@ describe("Survey Banner", () => { body: {} }) - cy.web3Login() + cy.web3Login({ withNewUser: false }) homePage.surveyBanner().should("be.visible") }) @@ -67,7 +67,12 @@ describe("Survey Banner", () => { body: {} }) - cy.web3Login() + cy.web3Login({ withNewUser: false }) + homePage.surveyBanner().should("not.exist") + }) + + it("A new user should not see the banner", () => { + cy.web3Login({ withNewUser: true }) homePage.surveyBanner().should("not.exist") }) }) diff --git a/packages/files-ui/package.json b/packages/files-ui/package.json index aa63b8d35c..c906a834cb 100644 --- a/packages/files-ui/package.json +++ b/packages/files-ui/package.json @@ -74,7 +74,7 @@ "@types/yup": "^0.29.9", "@types/zxcvbn": "^4.4.0", "babel-plugin-macros": "^2.8.0", - "cypress": "^8.6", + "cypress": "^9.1.0", "cypress-file-upload": "^5.0.8", "cypress-pipe": "^2.0.0" }, diff --git a/packages/files-ui/src/Components/Elements/CustomModal.tsx b/packages/files-ui/src/Components/Elements/CustomModal.tsx index 62d63fa07c..dbe5d76a25 100644 --- a/packages/files-ui/src/Components/Elements/CustomModal.tsx +++ b/packages/files-ui/src/Components/Elements/CustomModal.tsx @@ -13,7 +13,8 @@ const useStyles = makeStyles(({ constants, breakpoints }: CSFTheme) => }, inner: { backgroundColor: constants.modalDefault.backgroundColor, - color: constants.modalDefault.color + color: constants.modalDefault.color, + width: "100%" }, mobileStickyBottom: { [breakpoints.down("md")]: { @@ -51,7 +52,8 @@ const CustomModal = ({ className, children, injectedClass, mobileStickyBottom = className={clsx(classes.root, className)} injectedClass={{ closeIcon: clsx(classes.closeIcon, injectedClass?.closeIcon), - inner: clsx(classes.inner, mobileStickyBottom ? classes.mobileStickyBottom : undefined, injectedClass?.inner) + inner: clsx(classes.inner, mobileStickyBottom ? classes.mobileStickyBottom : undefined, injectedClass?.inner), + subModalInner: injectedClass?.subModalInner }} {...rest} > diff --git a/packages/files-ui/src/Components/Elements/MnemonicForm.tsx b/packages/files-ui/src/Components/Elements/MnemonicForm.tsx index 31e95cf972..c1352d7159 100644 --- a/packages/files-ui/src/Components/Elements/MnemonicForm.tsx +++ b/packages/files-ui/src/Components/Elements/MnemonicForm.tsx @@ -136,8 +136,10 @@ const MnemonicForm = ({ buttonLabel, onComplete }: Props) => { component="p" className={classes.loader} > - + Generating… diff --git a/packages/files-ui/src/Components/Elements/PasswordForm.tsx b/packages/files-ui/src/Components/Elements/PasswordForm.tsx index e99eeea19a..8652444779 100644 --- a/packages/files-ui/src/Components/Elements/PasswordForm.tsx +++ b/packages/files-ui/src/Components/Elements/PasswordForm.tsx @@ -104,6 +104,7 @@ const PasswordForm = ({ buttonLabel, setPassword }: Props) => { label={t`Password:`} labelClassName={classes.inputLabel} captionMessage={} + data-cy="input-sign-in-password" /> { name="confirmPassword" label={t`Confirm Password:`} labelClassName={classes.inputLabel} + data-cy="input-sign-in-password-verification" /> diff --git a/packages/files-ui/src/Components/Elements/ShareTransferRequestModal.tsx b/packages/files-ui/src/Components/Elements/ShareTransferRequestModal.tsx index 61c2a6094b..321d3d6d81 100644 --- a/packages/files-ui/src/Components/Elements/ShareTransferRequestModal.tsx +++ b/packages/files-ui/src/Components/Elements/ShareTransferRequestModal.tsx @@ -19,6 +19,8 @@ const useStyles = makeStyles(({ constants }: CSFTheme) => modalInner: { padding: constants.generalUnit * 4, textAlign: "center", + display: "flex", + flexDirection: "column", "& img" : { width: "min-content", margin: "auto", @@ -121,7 +123,7 @@ const ShareTransferRequestModal = ({ requests }: Props) => { requests.length > 1 && (isClearing ? : { mobileStickyBottom={false} >
- Teams diff --git a/packages/files-ui/src/Components/Layouts/AppNav.tsx b/packages/files-ui/src/Components/Layouts/AppNav.tsx index f1099a9405..45bd47098b 100644 --- a/packages/files-ui/src/Components/Layouts/AppNav.tsx +++ b/packages/files-ui/src/Components/Layouts/AppNav.tsx @@ -80,12 +80,12 @@ const useStyles = makeStyles( top: `${constants.mobileHeaderHeight}px`, backgroundColor: constants.nav.mobileBackgroundColor, zIndex: zIndex?.layer1, - padding: `0 ${constants.generalUnit * 4}px`, maxWidth: "100vw", visibility: "hidden", "&.active": { visibility: "visible", - width: `${constants.mobileNavWidth}px` + width: `${constants.mobileNavWidth}px`, + padding: `0 ${constants.generalUnit * 4}px` } } }, @@ -178,7 +178,7 @@ const useStyles = makeStyles( } }, "& svg": { - "& path" : { + "& path": { fill: constants.nav.headingColor }, transitionDuration: `${animation.transform}ms`, @@ -264,10 +264,10 @@ const AppNav = ({ navOpen, setNavOpen }: IAppNav) => { })} > {isLoggedIn && - secured && - !!publicKey && - !isNewDevice && - !shouldInitializeAccount && ( + secured && + !!publicKey && + !isNewDevice && + !shouldInitializeAccount && ( <> {desktop && (
@@ -277,14 +277,14 @@ const AppNav = ({ navOpen, setNavOpen }: IAppNav) => { > - Files + Files -   +   - beta + beta
diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/BinFileBrowser.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/BinFileBrowser.tsx index ffc630d104..bf1172c6ae 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/BinFileBrowser.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/BinFileBrowser.tsx @@ -123,23 +123,24 @@ const BinFileBrowser: React.FC = ({ controls = false }: }), []) return ( - + diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/CSFFileBrowser.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/CSFFileBrowser.tsx index 37c0e02aa3..e458f9b587 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/CSFFileBrowser.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/CSFFileBrowser.tsx @@ -185,28 +185,29 @@ const CSFFileBrowser: React.FC = () => { }), []) return ( - + diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/CreateOrEditSharedFolderModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/CreateOrEditSharedFolderModal.tsx index 454992d687..0424df0615 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/CreateOrEditSharedFolderModal.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/CreateOrEditSharedFolderModal.tsx @@ -13,7 +13,6 @@ import { useLookupSharedFolderUser } from "./hooks/useLookupUser" import { nameValidator } from "../../../Utils/validationSchema" import { getUserDisplayName } from "../../../Utils/getUserDisplayName" import LinkList from "./LinkSharing/LinkList" -import clsx from "clsx" const useStyles = makeStyles( ({ breakpoints, constants, typography, zIndex, palette }: CSFTheme) => { @@ -109,7 +108,8 @@ const useStyles = makeStyles( color: palette.error.main }, sharingLink: { - padding: constants.generalUnit * 1.25 + padding: constants.generalUnit * 2, + margin: 0 } }) } @@ -124,7 +124,7 @@ interface ICreateOrEditSharedFolderModalProps { const CreateOrEditSharedFolderModal = ({ mode, isModalOpen, onClose, bucketToEdit }: ICreateOrEditSharedFolderModalProps) => { const classes = useStyles() - const { desktop } = useThemeSwitcher() + const { desktop, tablet, mobile } = useThemeSwitcher() const { handleCreateSharedFolder, handleEditSharedFolder, isEditingSharedFolder, isCreatingSharedFolder } = useCreateOrEditSharedFolder() const [sharedFolderName, setSharedFolderName] = useState("") const { sharedFolderReaders, sharedFolderWriters, onNewUsers, handleLookupUser, usersError, resetUsers } = useLookupSharedFolderUser() @@ -204,6 +204,12 @@ const CreateOrEditSharedFolderModal = ({ mode, isModalOpen, onClose, bucketToEdi closePosition="none" maxWidth="sm" testId="create-or-edit-shared-folder" + subModal={mode === "edit" && !!bucketToEdit && ( + + )} >
@@ -260,6 +266,12 @@ const CreateOrEditSharedFolderModal = ({ mode, isModalOpen, onClose, bucketToEdi ...provided, minHeight: 90, alignContent: "start" + }), + valueContainer: (provided) => mobile && !tablet ? ({ + ...provided, + paddingBottom: 24 + }) : ({ + ...provided }) }} loadingMessage={t`Loading`} @@ -286,6 +298,12 @@ const CreateOrEditSharedFolderModal = ({ mode, isModalOpen, onClose, bucketToEdi ...provided, minHeight: 90, alignContent: "start" + }), + valueContainer: (provided) => mobile && !tablet ? ({ + ...provided, + paddingBottom: 24 + }) : ({ + ...provided }) }} loadingMessage={t`Loading`} @@ -293,17 +311,6 @@ const CreateOrEditSharedFolderModal = ({ mode, isModalOpen, onClose, bucketToEdi data-cy="tag-edit-permission-user" />
- {mode === "edit" && !!bucketToEdit && ( -
- - Sharing link - - -
- )} { + return createStyles({ + root: { + padding: constants.generalUnit * 2, + flexDirection: "column", + display: "flex", + alignItems: "center", + [breakpoints.down("sm")]: { + padding: constants.generalUnit + } + }, + okButton: { + marginLeft: constants.generalUnit + }, + cancelButton: { + [breakpoints.down("md")]: { + position: "fixed", + bottom: 0, + left: 0, + width: "100%", + height: constants?.mobileButtonHeight + } + }, + label: { + fontSize: 14, + lineHeight: "22px" + }, + heading: { + color: constants.modalDefault.color, + fontWeight: typography.fontWeight.semibold, + marginBottom: 10 + }, + iconBacking: { + backgroundColor: constants.modalDefault.iconBackingColor, + width: 48, + height: 48, + borderRadius: 24, + marginBottom: constants.generalUnit * 2, + marginTop: constants.generalUnit, + "& > svg": { + width: 16, + height: 16, + fill: palette.primary.main, + position: "relative", + display: "block", + transform: "translate(-50%, -50%)", + top: "50%", + left: "50%" + } + }, + modalFlexItem: { + width: "100%", + margin: 5 + }, + inputLabel: { + fontSize: 16, + fontWeight: 600 + }, + shareFolderNameInput: { + margin: `0 ${constants.generalUnit * 1.5}px ${constants.generalUnit}px`, + display: "block" + }, + footer: { + width: "100%", + padding: `${constants.generalUnit * 2}px ${constants.generalUnit * 2}px ${constants.generalUnit}px 0` + }, + errorText: { + marginLeft: constants.generalUnit * 1.5, + color: palette.error.main + } + }) + } +) + +interface ICreateOrManageSharedFolderProps { + mode?: SharedFolderModalMode + onClose: () => void + bucketToEdit?: BucketKeyPermission +} + +const CreateOrManageSharedFolder = ({ mode, onClose, bucketToEdit }: ICreateOrManageSharedFolderProps) => { + const classes = useStyles() + const { desktop } = useThemeSwitcher() + const { handleCreateSharedFolder, handleEditSharedFolder, isEditingSharedFolder, isCreatingSharedFolder } = useCreateOrEditSharedFolder() + const [sharedFolderName, setSharedFolderName] = useState("") + const { sharedFolderReaders, sharedFolderWriters, onNewUsers, handleLookupUser, usersError, resetUsers } = useLookupSharedFolderUser() + const [hasPermissionsChanged, setHasPermissionsChanged] = useState(false) + const [nameError, setNameError] = useState("") + + const onReset = useCallback(() => { + setSharedFolderName("") + setHasPermissionsChanged(false) + resetUsers() + }, [resetUsers]) + + useEffect(() => { + onReset() + + if (!bucketToEdit) return + + const newWriters = bucketToEdit.writers.map((writer) => ({ + label: getUserDisplayName(writer), + value: writer.uuid || "", + data: writer + }) + ) || [] + + const newReaders = bucketToEdit.readers.map((reader) => ({ + label: getUserDisplayName(reader), + value: reader.uuid || "", + data: reader + }) + ) || [] + + onNewUsers(newWriters, "write") + onNewUsers(newReaders, "read") + }, [bucketToEdit, onNewUsers, onReset]) + + const onNameChange = useCallback((value?: string | number) => { + if (value === undefined) return + + const name = value.toString() + setSharedFolderName(name) + + nameValidator + .validate({ name }) + .then(() => { + setNameError("") + }) + .catch((e: Error) => { + setNameError(e.message) + }) + }, []) + + const handleClose = useCallback(() => { + onReset() + onClose() + }, [onClose, onReset]) + + const onCreateSharedFolder = useCallback(() => { + handleCreateSharedFolder(sharedFolderName, sharedFolderReaders, sharedFolderWriters) + .catch(console.error) + .finally(handleClose) + }, [handleCreateSharedFolder, sharedFolderName, sharedFolderWriters, sharedFolderReaders, handleClose]) + + const onEditSharedFolder = useCallback(() => { + if (!bucketToEdit) return + handleEditSharedFolder(bucketToEdit, sharedFolderReaders, sharedFolderWriters) + .catch(console.error) + .finally(handleClose) + }, [handleEditSharedFolder, sharedFolderWriters, sharedFolderReaders, handleClose, bucketToEdit]) + + return ( +
+
+ +
+
+ + {mode === "create" + ? Create Shared Folder + : Manage Shared Folder + } + + +
+ {mode === "create" && +
+ + {nameError && ( + + {nameError} + + )} +
+ } +
+ { + setHasPermissionsChanged(true) + onNewUsers(values, "read") + }} + label={t`Give view-only permission to:`} + labelClassName={classes.inputLabel} + value={sharedFolderReaders} + fetchTags={(inputVal) => handleLookupUser(inputVal, "read")} + placeholder={t`Add by sharing address, username or wallet address`} + styles={{ + control: (provided) => ({ + ...provided, + minHeight: 90, + alignContent: "start" + }) + }} + loadingMessage={t`Loading`} + noOptionsMessage={t`No user found for this query.`} + data-cy="tag-view-permission-user" + /> +
+
+ { + setHasPermissionsChanged(true) + onNewUsers(values, "write") + }} + label={t`Give edit permission to:`} + labelClassName={classes.inputLabel} + value={sharedFolderWriters} + fetchTags={(inputVal) => handleLookupUser(inputVal, "write")} + placeholder={t`Add by sharing address, username or wallet address`} + styles={{ + control: (provided) => ({ + ...provided, + minHeight: 90, + alignContent: "start" + }) + }} + loadingMessage={t`Loading`} + noOptionsMessage={t`No user found for this query.`} + data-cy="tag-edit-permission-user" + /> +
+ + {!!usersError && ( + + {usersError} + + )} + + + Cancel + + + + +
+ ) +} + +export default CreateOrManageSharedFolder diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/CreateOrManageSharedFolderModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/CreateOrManageSharedFolderModal.tsx new file mode 100644 index 0000000000..94e3da4c65 --- /dev/null +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/CreateOrManageSharedFolderModal.tsx @@ -0,0 +1,76 @@ +import { createStyles, makeStyles } from "@chainsafe/common-theme" +import React from "react" +import CustomModal from "../../Elements/CustomModal" +import { CSFTheme } from "../../../Themes/types" +import { BucketKeyPermission } from "../../../Contexts/FilesContext" +import { SharedFolderModalMode } from "./types" +import LinkList from "./LinkSharing/LinkList" +import CreateOrManageSharedFolder from "./CreateOrManageSharedFolder" + +const useStyles = makeStyles( + ({ breakpoints, constants, zIndex }: CSFTheme) => { + return createStyles({ + modalRoot: { + zIndex: zIndex?.blocker, + [breakpoints.down("md")]: { + paddingBottom: Number(constants?.mobileButtonHeight) + constants.generalUnit + } + }, + modalInner: { + backgroundColor: constants.modalDefault.backgroundColor, + color: constants.modalDefault.color, + [breakpoints.down("md")]: { + bottom: + Number(constants?.mobileButtonHeight) + constants.generalUnit, + borderTopLeftRadius: `${constants.generalUnit * 1.5}px`, + borderTopRightRadius: `${constants.generalUnit * 1.5}px`, + maxWidth: `${breakpoints.width("md")}px !important` + } + }, + subModal: { + width: "100%" + } + }) + } +) + +interface ICreateOrManageSharedFolderModalProps { + mode?: SharedFolderModalMode + isModalOpen: boolean + onClose: () => void + bucketToEdit?: BucketKeyPermission +} + +const CreateOrManageSharedFolderModal = ( + { mode, isModalOpen, onClose, bucketToEdit }: ICreateOrManageSharedFolderModalProps +) => { + const classes = useStyles() + + return ( + + )} + > + + + ) +} + +export default CreateOrManageSharedFolderModal diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/FileInfoModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/FileInfoModal.tsx index ae9553623c..3a7d8582f9 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/FileInfoModal.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/FileInfoModal.tsx @@ -9,7 +9,7 @@ import CustomButton from "../../Elements/CustomButton" import { Trans } from "@lingui/macro" import { FileFullInfo } from "../../../Contexts/FilesContext" import { - Button, + CopyIcon, formatBytes, Grid, Loading, @@ -44,7 +44,6 @@ const useStyles = makeStyles( }, closeButton: { flex: 1, - marginLeft: constants.generalUnit * 2, [breakpoints.down("md")]: { position: "fixed", bottom: 0, @@ -61,13 +60,6 @@ const useStyles = makeStyles( textAlign: "center" } }, - heading: { - fontWeight: typography.fontWeight.semibold, - textAlign: "left", - [breakpoints.down("md")]: { - textAlign: "center" - } - }, infoHeading: { fontWeight: typography.fontWeight.semibold, textAlign: "left" @@ -89,7 +81,8 @@ const useStyles = makeStyles( color: palette.additional["gray"][8], whiteSpace: "nowrap", overflow: "hidden", - textOverflow: "ellipsis" + textOverflow: "ellipsis", + marginRight: constants.generalUnit * 2 }, technicalContainer: { paddingTop: constants.generalUnit, @@ -114,7 +107,7 @@ const useStyles = makeStyles( alignItems: "center", justifyContent: "center", left: "50%", - bottom: "calc(100% + 5px)", + bottom: "calc(100% + 8px)", position: "absolute", transform: "translate(-50%, 0%)", zIndex: zIndex?.layer1, @@ -143,17 +136,20 @@ const useStyles = makeStyles( visibility: "visible" } }, - copyButton: { - width: "100%" - }, copyContainer: { - position: "relative", - flexBasis: "75%", - color: palette.additional["gray"][9], + position: "relative" + }, + copyIcon: { + fontSize: "16px", + fill: palette.additional["gray"][9], [breakpoints.down("md")]: { - flexBasis: "100%", - margin: `${constants.generalUnit * 2}px` + fontSize: "18px", + fill: palette.additional["gray"][9] } + }, + copyRow: { + display: "flex", + cursor: "pointer" } }) } @@ -186,15 +182,27 @@ const FileInfoModal = ({ filePath, close }: IFileInfoModuleProps) => { } , [bucket, filePath, filesApiClient]) - const [copied, setCopied] = useState(false) - const debouncedSwitchCopied = debounce(() => setCopied(false), 3000) + const [copiedCID, setCopiedCID] = useState(false) + const [copiedKey, setCopiedKey] = useState(false) + const debouncedSwitchCopiedCID = debounce(() => setCopiedCID(false), 3000) + const debouncedSwitchCopiedKey = debounce(() => setCopiedKey(false), 3000) const onCopyCID = () => { if (fullFileInfo?.content?.cid) { - navigator.clipboard.writeText(fullFileInfo?.content?.cid) + navigator.clipboard.writeText(fullFileInfo.content.cid) + .then(() => { + setCopiedCID(true) + debouncedSwitchCopiedCID() + }).catch(console.error) + } + } + + const onCopyKey = () => { + if (bucket?.encryptionKey) { + navigator.clipboard.writeText(bucket.encryptionKey) .then(() => { - setCopied(true) - debouncedSwitchCopied() + setCopiedKey(true) + debouncedSwitchCopiedKey() }).catch(console.error) } } @@ -232,7 +240,7 @@ const FileInfoModal = ({ filePath, close }: IFileInfoModuleProps) => {
@@ -360,13 +368,28 @@ const FileInfoModal = ({ filePath, close }: IFileInfoModuleProps) => { CID (Content Identifier) - - {fullFileInfo.content?.cid} - + + {fullFileInfo.content?.cid} + +
+ +
+ + + Copied! + + +
+
+
{ Decryption key - - - {bucket?.encryptionKey} - - + + + {bucket?.encryptionKey} + + +
+ +
+ + + Copied! + + +
+
+
@@ -398,24 +436,6 @@ const FileInfoModal = ({ filePath, close }: IFileInfoModuleProps) => { flexDirection="row" className={classes.buttonsContainer} > -
- -
- - - Copied! - - -
-
close()} size="large" diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/LinkSharing/LinkList.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/LinkSharing/LinkList.tsx index f557c760d7..b8c8d0ec36 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/LinkSharing/LinkList.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/LinkSharing/LinkList.tsx @@ -1,8 +1,8 @@ -import { Button, Loading, MenuDropdown } from "@chainsafe/common-components" +import { Button, Loading, MenuDropdown, Typography } from "@chainsafe/common-components" import { createStyles, makeStyles } from "@chainsafe/common-theme" import { NonceResponse, NonceResponsePermission } from "@chainsafe/files-api-client" import { t, Trans } from "@lingui/macro" -import React, { useCallback, useEffect, useState } from "react" +import React, { ReactNode, useCallback, useEffect, useMemo, useState } from "react" import { useFilesApi } from "../../../../Contexts/FilesApiContext" import { CSFTheme } from "../../../../Themes/types" import SharingLink from "./SharingLink" @@ -11,6 +11,7 @@ const useStyles = makeStyles( ({ constants, palette }: CSFTheme) => { return createStyles({ root: { + padding: 2 * constants.generalUnit }, options: { backgroundColor: constants.header.optionsBackground, @@ -47,49 +48,131 @@ const useStyles = makeStyles( }, permissionDropdown: { padding: `0px ${constants.generalUnit}px`, - backgroundColor: palette.additional["gray"][5], - marginLeft: constants.generalUnit + backgroundColor: palette.additional["gray"][1], + marginLeft: constants.generalUnit, + borderColor: palette.additional["gray"][5], + borderWidth: "1px", + borderStyle: "solid", + borderRadius: "4px" }, - createLink: { - display: "flex", - alignItems: "center", - margin: `${constants.generalUnit * 2.5}px 0` + rightsText: { + display: "inline-block" }, createLinkButton: { - marginRight: constants.generalUnit + width: "100%" }, dropdownTitle: { padding: `${constants.generalUnit * 0.75}px ${constants.generalUnit}px` + }, + heading : { + marginBottom: constants.generalUnit + }, + loadingContainer: { + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center" + }, + grayWrapper: { + backgroundColor: palette.additional["gray"][3], + borderRadius: "4px", + display: "flex", + padding: constants.generalUnit * 2, + flexDirection: "column" + }, + creationWrapper: { + display: "flex", + alignItems: "center", + justifyContent: "center", + flexDirection: "column", + margin: "auto" + }, + rightSelection: { + marginBottom: constants.generalUnit + }, + loader: { + textAlign: "center" + }, + activeLinks: { + marginBottom: constants.generalUnit } }) } ) +const MAX_LINKS = 2 + interface Props { bucketId: string bucketEncryptionKey: string } -const readRights = t`read rights` -const editRights = t`edit rights` +interface LinkMenuItems { + id: NonceResponsePermission + onClick: () => void + contents: ReactNode +} + +const readRights = t`view-only` +const editRights = t`can-edit` export const translatedPermission = (permission: NonceResponsePermission) => permission === "read" ? readRights : editRights const LinkList = ({ bucketId, bucketEncryptionKey }: Props) => { const classes = useStyles() const { filesApiClient } = useFilesApi() const [nonces, setNonces] = useState([]) - const [isLoading, setIsLoading] = useState(false) - const [newLinkPermission, setNewLinkPermission] = useState("read") + const [isLoadingNonces, setIsLoadingNonces] = useState(false) + const [isLoadingCreation, setIsLoadingCreation] = useState(false) + const hasAReadNonce = useMemo(() => !!nonces.find(n => n.permission === "read"), [nonces]) + const [newLinkPermission, setNewLinkPermission] = useState(undefined) + const menuItems: LinkMenuItems[] = useMemo(() => [ + { + id: "read", + onClick: () => setNewLinkPermission("read"), + contents: ( +
+ {readRights} +
+ ) + }, + { + id: "write", + onClick: () => setNewLinkPermission("write"), + contents: ( +
+ {editRights} +
+ ) + } + ], [classes.menuItem]) + + const displayedItems = useMemo(() => nonces.length === 0 + ? menuItems + : hasAReadNonce + ? menuItems.filter(i => i.id === "write") + : menuItems.filter(i => i.id === "read") + , [hasAReadNonce, menuItems, nonces.length] + ) + + useEffect(() => { + setNewLinkPermission(displayedItems[0].id) + }, [displayedItems]) const refreshNonces = useCallback(() => { - setIsLoading(true) + setIsLoadingNonces(true) filesApiClient.getAllNonces() .then((res) => { const noncesForCurrentBucket = res.filter(n => n.bucket_id === bucketId) setNonces(noncesForCurrentBucket) }) .catch(console.error) - .finally(() => setIsLoading(false)) + .finally(() => setIsLoadingNonces(false)) }, [bucketId, filesApiClient]) useEffect(() => { @@ -98,77 +181,103 @@ const LinkList = ({ bucketId, bucketEncryptionKey }: Props) => { const onCreateNonce = useCallback(() => { - setIsLoading(true) + if (!newLinkPermission) { + console.error("Permission not set") + return + } + + setIsLoadingCreation(true) return filesApiClient .createNonce({ bucket_id: bucketId, permission: newLinkPermission }) .catch(console.error) .finally(() => { - setIsLoading(false) + setIsLoadingCreation(false) refreshNonces() }) }, [bucketId, filesApiClient, newLinkPermission, refreshNonces]) return (
-
-
+ )} + {isLoadingNonces && ( + - Create new link - - with - setNewLinkPermission("read"), - contents: ( -
- {readRights} -
- ) - }, - { - onClick: () => setNewLinkPermission("write"), - contents: ( -
- {editRights} -
- ) - } - ]} - /> -
- { - isLoading && - } - { - !isLoading && nonces.length > 0 && nonces.map((nonce) => - - ) - } +
+ )} + {nonces.length < MAX_LINKS && ( + <> + + Create a sharing link + +
+
+
+ + Anyone with the link can: + + +
+ +
+
+ + )} ) } diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/LinkSharing/SharingLink.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/LinkSharing/SharingLink.tsx index 882f8c64fd..12fbdfa5fb 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/LinkSharing/SharingLink.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/LinkSharing/SharingLink.tsx @@ -1,5 +1,5 @@ -import { Button, DeleteSvg, Typography } from "@chainsafe/common-components" +import { CopyIcon, DeleteSvg, Loading, MoreIcon, Typography } from "@chainsafe/common-components" import { createStyles, debounce, makeStyles } from "@chainsafe/common-theme" import { NonceResponse } from "@chainsafe/files-api-client" import { Trans } from "@lingui/macro" @@ -7,19 +7,24 @@ import React, { useCallback, useEffect, useState } from "react" import { useFilesApi } from "../../../../Contexts/FilesApiContext" import { useThresholdKey } from "../../../../Contexts/ThresholdKeyContext" import { CSFTheme } from "../../../../Themes/types" +import Menu from "../../../../UI-components/Menu" import { ROUTE_LINKS } from "../../../FilesRoutes" import { translatedPermission } from "./LinkList" const useStyles = makeStyles( - ({ constants }: CSFTheme) => { + ({ constants, palette, zIndex, animation, typography }: CSFTheme) => { return createStyles({ root: { display: "flex", - marginBottom: constants.generalUnit * 0.5 + maxWidth: "100%", + position: "relative", + "&:not(:first-child)": { + marginTop: constants.generalUnit * 2 + } }, linkWrapper: { whiteSpace: "nowrap", - marginRight: constants.generalUnit * 2, + marginRight: constants.generalUnit * 3, display: "flex", alignItems: "center", overflow: "hidden" @@ -27,14 +32,18 @@ const useStyles = makeStyles( permissionWrapper: { display: "flex", alignItems: "center", - marginRight: constants.generalUnit, + marginRight: constants.generalUnit * 3, flex: 1, - whiteSpace: "nowrap" + whiteSpace: "nowrap", + textAlign: "right", + fontWeight: typography.fontWeight.regular }, copyButton: { - flex: 1, - whiteSpace: "nowrap", - marginRight: constants.generalUnit + display: "flex", + justifyContent: "center", + alignItems: "center", + marginRight: constants.generalUnit * 2, + cursor: "pointer" }, link: { textOverflow: "ellipsis", @@ -47,6 +56,69 @@ const useStyles = makeStyles( width: 20, marginRight: constants.generalUnit * 1.5, fill: constants.fileSystemItemRow.menuIcon + }, + copyIcon: { + fontSize: "18px", + fill: palette.additional["gray"][8] + }, + copiedFlag: { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + left: "50%", + top: -15, + position: "absolute", + transform: "translate(-50%, -50%)", + zIndex: zIndex?.layer1, + transitionDuration: `${animation.transform}ms`, + backgroundColor: constants.loginModule.flagBg, + color: constants.loginModule.flagText, + padding: `${constants.generalUnit / 2}px ${constants.generalUnit}px`, + borderRadius: 2, + "&:after": { + transitionDuration: `${animation.transform}ms`, + content: "''", + position: "absolute", + top: "100%", + left: "50%", + transform: "translate(-50%,0)", + width: 0, + height: 0, + borderLeft: "5px solid transparent", + borderRight: "5px solid transparent", + borderTop: `5px solid ${constants.loginModule.flagBg}` + } + }, + dropdownIcon: { + width: 14, + height: 14, + padding: 0, + display: "flex", + justifyContent: "center", + alignItems: "center", + fontSize: "unset", + "& svg": { + fill: constants.fileSystemItemRow.dropdownIcon, + width: 14, + height: 14 + } + }, + focusVisible: { + backgroundColor: "transparent !important" + }, + menuRoot: { + zIndex: "2500 !important" as any + }, + loader: { + display: "flex", + alignItems: "center", + margin: "auto" + }, + menuWrapper: { + display: "flex", + alignItems: "center", + margin: "auto" } }) } @@ -65,7 +137,7 @@ const SharingLink = ({ nonce, bucketEncryptionKey, refreshNonces }: Props) => { const [jwt, setJwt] = useState("") const { createJWT } = useThresholdKey() const [copied, setCopied] = useState(false) - const [isLoading, setIsLoading] = useState(true) + const [isDeleting, setIsDeleting] = useState(false) useEffect(() => { if(!nonce?.bucket_id || !nonce?.id) { @@ -74,7 +146,6 @@ const SharingLink = ({ nonce, bucketEncryptionKey, refreshNonces }: Props) => { const newJwt = createJWT(nonce.bucket_id, nonce.id, nonce.permission) newJwt && setJwt(newJwt) - setIsLoading(false) }, [createJWT, nonce]) useEffect(() => { @@ -94,50 +165,112 @@ const SharingLink = ({ nonce, bucketEncryptionKey, refreshNonces }: Props) => { debouncedSwitchCopied() }) .catch(console.error) + + // //Create a textbox field where we can insert text to. + // const copyFrom = document.createElement("textarea") + + // //Set the text content to be the text you wished to copy. + // copyFrom.textContent = link + + // //Append the textbox field into the body as a child. + // //"execCommand()" only works when there exists selected text, and the text is inside + // //document.body (meaning the text is part of a valid rendered HTML element). + // document.body.appendChild(copyFrom) + + // //Select all the text! + // copyFrom.select() + + // //Execute command + // document.execCommand("copy") + + // //(Optional) De-select the text using blur(). + // copyFrom.blur() + + // //Remove the textbox field from the document.body, so no other JavaScript nor + // //other elements can get access to this. + // document.body.removeChild(copyFrom) + + setCopied(true) + debouncedSwitchCopied() }, [debouncedSwitchCopied, link]) const onDeleteNonce = useCallback(() => { - setIsLoading(true) + setIsDeleting(true) filesApiClient.revokeNonce(nonce.id) .catch(console.error) .finally(() => { refreshNonces() - setIsLoading(false) + setIsDeleting(false) }) }, [filesApiClient, nonce, refreshNonces]) + if (isDeleting) { + return ( + <> + + + + + ) + } + return (
-
+ {copied && ( +
+ + Copied! + +
+ )} +
{link}
-
+
{translatedPermission(nonce.permission)}
- - + +
+
+ } + options={[{ + contents: ( + <> + + + Delete + + + ), + onClick: onDeleteNonce + }]} + style={{ focusVisible: classes.focusVisible, root: classes.menuRoot }} + /> +
) } diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/MoveFileModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/MoveFileModal.tsx index f8a9a6cf1d..4e4715db66 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/MoveFileModal.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/MoveFileModal.tsx @@ -189,7 +189,8 @@ const MoveFileModule = ({ filesToMove, onClose, onCancel, mode }: IMoveFileModul sm={12} className={classes.paddedContainer} > - diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/ReportFileModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/ReportFileModal.tsx index 71b0d70597..a0d8001258 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/ReportFileModal.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/ReportFileModal.tsx @@ -67,10 +67,12 @@ const useStyles = makeStyles( }px` }, infoBox: { - paddingLeft: constants.generalUnit + paddingLeft: constants.generalUnit, + maxWidth: "100%" }, subInfoBox: { - padding: `${constants.generalUnit * 1}px 0` + padding: `${constants.generalUnit * 1}px 0`, + maxWidth: "100%" }, subSubtitle: { color: palette.additional["gray"][8] @@ -137,14 +139,37 @@ const useStyles = makeStyles( }, decryptionKey: { width: "100%", - wordBreak: "break-all" + overflow: "hidden", + textOverflow: "ellipsis", + marginRight: constants.generalUnit * 2 }, infoText: { marginBottom: constants.generalUnit * 2 }, keysWrapper: { - maxHeight: constants.generalUnit * 10, - overflow: "scroll" + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + maxWidth: "100%", + display: "flex", + cursor: "pointer" + }, + copyIcon: { + fontSize: "16px", + fill: palette.additional["gray"][9], + marginRight: constants.generalUnit, + [breakpoints.down("md")]: { + fontSize: "18px", + fill: palette.additional["gray"][9] + } + }, + decryptionKeyTitle: { + display: "flex", + justifyContent: "space-between", + marginBottom: constants.generalUnit * 0.5 + }, + titleMargin: { + marginBottom: constants.generalUnit * 0.5 } }) } @@ -164,7 +189,7 @@ const ReportFileModal = ({ filePath, close }: IReportFileModalProps) => { const classes = useStyles() const { bucket } = useFileBrowser() const { encryptionKey, id } = bucket || {} - const [isLoadingAdminKey, setIsloadingAdminKey] = useState(true) + const [isLoadingAdminKey, setIsLoadingAdminKey] = useState(true) const [adminPubKeys, setAdminPubkeys] = useState([]) const [encryptedDecryptionKeyMap, setEncryptedDecryptionKeyMap] = useState([]) const { encryptForPublicKey } = useThresholdKey() @@ -199,7 +224,7 @@ const ReportFileModal = ({ filePath, close }: IReportFileModalProps) => { setEncryptedDecryptionKeyMap(map) }) .catch(console.error) - .finally(() => setIsloadingAdminKey(false)) + .finally(() => setIsLoadingAdminKey(false)) }, [adminPubKeys, encryptForPublicKey, encryptionKey]) @@ -251,7 +276,7 @@ const ReportFileModal = ({ filePath, close }: IReportFileModalProps) => {
@@ -287,12 +312,13 @@ const ReportFileModal = ({ filePath, close }: IReportFileModalProps) => { Bucket id {id} @@ -302,28 +328,33 @@ const ReportFileModal = ({ filePath, close }: IReportFileModalProps) => { File path {filePath}
- + + Decryption key + +
+
- Decryption key - -
{JSON.stringify(encryptedDecryptionKeyMap)} @@ -353,7 +384,7 @@ const ReportFileModal = ({ filePath, close }: IReportFileModalProps) => {
- Copied! + Copied!
diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/SearchFileBrowser.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/SearchFileBrowser.tsx index 055f11bc30..9cc76ca907 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/SearchFileBrowser.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/SearchFileBrowser.tsx @@ -102,20 +102,21 @@ const SearchFileBrowser: React.FC = ({ controls = false }), []) return ( - + diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/ShareModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/ShareModal.tsx index d88b27c568..070b0e9531 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/ShareModal.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/ShareModal.tsx @@ -2,11 +2,10 @@ import { createStyles, makeStyles } from "@chainsafe/common-theme" import React, { useState } from "react" import CustomModal from "../../Elements/CustomModal" import { t, Trans } from "@lingui/macro" -import { Button, CheckboxInput, SelectInput, ShareAltSvg, TagsInput, TextInput, Typography } from "@chainsafe/common-components" +import { Button, CheckboxInput, SelectInput, ShareAltSvg, TextInput, Typography } from "@chainsafe/common-components" import { CSFTheme } from "../../../Themes/types" import { useCallback } from "react" import { useCreateOrEditSharedFolder } from "./hooks/useCreateOrEditSharedFolder" -import { useLookupSharedFolderUser } from "./hooks/useLookupUser" import { useMemo } from "react" import { BucketKeyPermission, FileSystemItem, useFiles } from "../../../Contexts/FilesContext" import { useUser } from "../../../Contexts/UserContext" @@ -14,65 +13,37 @@ import { useFileBrowser } from "../../../Contexts/FileBrowserContext" import clsx from "clsx" import { useEffect } from "react" import { nameValidator } from "../../../Utils/validationSchema" +import CreateOrManageSharedFolder from "./CreateOrManageSharedFolder" +import LinkList from "./LinkSharing/LinkList" +import { usePosthogContext } from "../../../Contexts/PosthogContext" + +interface StyleProps { + width: number +} const useStyles = makeStyles( ({ breakpoints, constants, palette, typography, zIndex }: CSFTheme) => { return createStyles({ - modalRoot: { - zIndex: zIndex?.blocker, - [breakpoints.down("md")]: {} - }, - modalInner: { - backgroundColor: constants.fileInfoModal.background, - color: constants.fileInfoModal.color - }, root: { - padding: constants.generalUnit * 4, + padding: constants.generalUnit * 3, flexDirection: "column", - display: "flex", - alignItems: "center" + display: "flex" }, - closeButton: { - flex: 1, - marginLeft: constants.generalUnit * 2, - [breakpoints.down("md")]: { - position: "fixed", - bottom: 0, - left: 0, - width: "100%", - height: constants?.mobileButtonHeight, - margin: 0 - } + modalRoot: { + zIndex: zIndex?.blocker }, - title: { - fontWeight: typography.fontWeight.semibold, - textAlign: "left", - [breakpoints.down("md")]: { - textAlign: "center" + modalInner: ({ width }: StyleProps) => ({ + backgroundColor: constants.fileInfoModal.background, + color: constants.fileInfoModal.color, + width, + [breakpoints.down("sm")]: { + width: "100%" } - }, - infoHeading: { - fontWeight: typography.fontWeight.semibold, - textAlign: "left" - }, - infoContainer: { - borderTop: constants.fileInfoModal.infoContainerBorderTop, - padding: `${constants.generalUnit * 2}px ${constants.generalUnit * 3}px` - }, - infoBox: { - paddingLeft: constants.generalUnit - }, - subInfoBox: { - padding: `${constants.generalUnit * 1}px 0` - }, - subSubtitle: { - color: palette.additional["gray"][8] - }, - paddedContainer: { - padding: `${constants.generalUnit * 2}px ${ - constants.generalUnit * 4 - }px`, - borderBottom: `1px solid ${palette.additional["gray"][3]}` + }), + topIconContainer: { + display: "flex", + flexDirection: "column", + alignItems: "center" }, buttonsArea: { display: "flex", @@ -86,29 +57,28 @@ const useStyles = makeStyles( }, buttonsContainer: { display: "flex", - justifyContent: "center", + flexDirection: "column", + alignItems: "center", marginTop: constants.generalUnit * 2 }, mainButton: { - width: "100%" + width: 240, + marginBottom: constants.generalUnit * 0.5 }, - sideBySideButton: { - minWidth: constants.generalUnit * 12, - "&:first-child": { - marginRight: constants.generalUnit * 2 - } + cancelButton: { + maxWidth: 100 }, heading: { color: constants.modalDefault.color, fontWeight: typography.fontWeight.semibold, - marginBottom: 10 + marginBottom: constants.generalUnit * 3 }, iconBacking: { backgroundColor: constants.modalDefault.iconBackingColor, width: 48, height: 48, borderRadius: 24, - marginBottom: 16, + marginBottom: 8, "& > svg": { width: 16, height: 16, @@ -121,82 +91,70 @@ const useStyles = makeStyles( } }, inputLabel: { - fontSize: 16, - fontWeight: 600 + fontSize: 14, + fontWeight: 600, + marginBottom: constants.generalUnit }, modalFlexItem: { width: "100%", - margin: 5, marginBottom: constants.generalUnit * 2 }, - loadingContainer: { - width: "100%", - paddingBottom: constants.generalUnit * 6, - display: "flex", - flexDirection: "column", - alignItems: "center", - "& svg": { - marginBottom: constants.generalUnit * 2 - } - }, - shareFolderNameInput: { - display: "block" + newFolderInput: { + margin: 0, + width: "100%" }, buttonLink: { color: palette.additional["gray"][10], outline: "none", textDecoration: "underline", cursor: "pointer", - textAlign: "center", + textAlign: "left", marginBottom: constants.generalUnit * 2 }, error: { color: palette.error.main, textAlign: "center" }, - checkIcon: { - marginRight: constants.generalUnit * 2 - }, - successBox: { - textAlign: "center", - marginBottom: constants.generalUnit * 4 - }, - successText: { - display: "flex", - marginBottom: constants.generalUnit * 2 - }, inputWrapper: { marginBottom: 0 }, errorText: { - marginLeft: constants.generalUnit * 2, + marginTop: constants.generalUnit * 1, color: palette.error.main }, - titleWrapper: { - padding: "0 5px" - } + subModal: ({ width }: StyleProps) => ({ + width, + [breakpoints.down("sm")]: { + width: "100%" + } + }) }) } ) interface IShareFileProps { fileSystemItems: FileSystemItem[] - close: () => void + onClose: () => void } -const ShareModal = ({ close, fileSystemItems }: IShareFileProps) => { - const classes = useStyles() +const ShareModal = ({ onClose, fileSystemItems }: IShareFileProps) => { const { handleCreateSharedFolder } = useCreateOrEditSharedFolder() const [sharedFolderName, setSharedFolderName] = useState("") - const { sharedFolderReaders, sharedFolderWriters, handleLookupUser, onNewUsers, usersError, resetUsers } = useLookupSharedFolderUser() - const [isUsingCurrentBucket, setIsUsingCurrentBucket] = useState(true) + const [isUsingExistingBucket, setIsUsingExistingBucket] = useState(true) const [keepOriginalFile, setKeepOriginalFile] = useState(true) + const [bucketToUpload, setBucketToUpload] = useState() const [destinationBucket, setDestinationBucket] = useState() + const [isFolderCreationLoading, setIsFolderCreationLoading] = useState(false) const { buckets, transferFileBetweenBuckets } = useFiles() const { bucket, currentPath } = useFileBrowser() const { profile } = useUser() const [nameError, setNameError] = useState("") const inSharedBucket = useMemo(() => bucket?.type === "share", [bucket]) + + const classes = useStyles({ + width: bucketToUpload ? 600 : 500 + }) + const isReader = useMemo(() => { if (!bucket) return false @@ -210,6 +168,8 @@ const ShareModal = ({ close, fileSystemItems }: IShareFileProps) => { return buckets .filter(buck => buck.type === "share" || buck.type === "csf") + // keep only alive buckets + .filter(buck => buck.status !== "deleting") // filter out the current bucket .filter(buck => buck.id !== bucket?.id) // all buckets where the user is reader or writer @@ -224,15 +184,14 @@ const ShareModal = ({ close, fileSystemItems }: IShareFileProps) => { const hasNoSharedBucket = useMemo(() => bucketsOptions.length === 0, [bucketsOptions.length]) useEffect(() => { - resetUsers() setSharedFolderName("") setNameError("") - }, [resetUsers]) + }, []) // if the user has no shared bucket, we default to new folder creation useEffect(() => { if (hasNoSharedBucket) { - setIsUsingCurrentBucket(false) + setIsUsingExistingBucket(false) } }, [hasNoSharedBucket]) @@ -258,15 +217,16 @@ const ShareModal = ({ close, fileSystemItems }: IShareFileProps) => { return } - if(!destinationBucket && isUsingCurrentBucket){ + if(!destinationBucket && isUsingExistingBucket){ return } let bucketToUpload: BucketKeyPermission | undefined = destinationBucket - if (!isUsingCurrentBucket) { + if (!isUsingExistingBucket) { + setIsFolderCreationLoading(true) try { - const newBucket = await handleCreateSharedFolder(sharedFolderName, sharedFolderReaders, sharedFolderWriters) + const newBucket = await handleCreateSharedFolder(sharedFolderName, [], []) if(!newBucket){ return @@ -276,6 +236,7 @@ const ShareModal = ({ close, fileSystemItems }: IShareFileProps) => { console.error(e) return } + setIsFolderCreationLoading(false) } if(!bucketToUpload){ @@ -284,69 +245,90 @@ const ShareModal = ({ close, fileSystemItems }: IShareFileProps) => { } transferFileBetweenBuckets(bucket, fileSystemItems, currentPath, bucketToUpload, keepOriginalFile) - close() + setBucketToUpload(bucketToUpload) + if (isUsingExistingBucket) { + onClose() + } }, [ bucket, destinationBucket, handleCreateSharedFolder, - isUsingCurrentBucket, + isUsingExistingBucket, sharedFolderName, - sharedFolderReaders, - sharedFolderWriters, keepOriginalFile, - close, + onClose, transferFileBetweenBuckets, currentPath, fileSystemItems ]) + const { captureEvent } = usePosthogContext() + return ( + )} > -
-
- -
-
- - {inSharedBucket - ? t`Copy file` - : t`Share file` - } - -
-
- {isUsingCurrentBucket - ? ( -
- setDestinationBucket(buckets.find((bu) => bu.id === val))} - /> -
- ) - : ( - <> -
+ {bucketToUpload + ? + :
+
+
+ +
+
+ + {inSharedBucket + ? t`Copy file` + : t`Share file` + } + +
+
+
+ {isUsingExistingBucket + ? ( +
+ setDestinationBucket(buckets.find((bu) => bu.id === val))} + /> +
+ ) + : ( +
- {nameError && ( + {!!nameError && ( { )}
-
- onNewUsers(values, "read")} - label={t`Give view-only permission to:`} - labelClassName={classes.inputLabel} - value={sharedFolderReaders} - fetchTags={(inputVal) => handleLookupUser(inputVal, "read")} - placeholder={t`Add by sharing address, username or wallet address`} - styles={{ - control: (provided) => ({ - ...provided, - minHeight: 90, - alignContent: "start" - }) - }} - loadingMessage={t`Loading`} - noOptionsMessage={t`No user found for this query.`} - /> -
-
- onNewUsers(values, "write")} - label={t`Give edit permission to:`} - labelClassName={classes.inputLabel} - value={sharedFolderWriters} - fetchTags={(inputVal) => handleLookupUser(inputVal, "write")} - placeholder={t`Add by sharing address, username or wallet address`} - styles={{ - control: (provided) => ({ - ...provided, - minHeight: 90, - alignContent: "start" - }) - }} - loadingMessage={t`Loading...`} - noOptionsMessage={t`No user found for this query.`} - /> -
- {!!usersError && ( - - {usersError} - - )} - - )} -
-
+ )} +
{!hasNoSharedBucket && (
setIsUsingCurrentBucket(!isUsingCurrentBucket)} + onClick={() => setIsUsingExistingBucket(!isUsingExistingBucket)} > { - isUsingCurrentBucket - ? Create a new shared folder - : Use an existing shared folder + isUsingExistingBucket + ? Or Create a new shared folder + : Or Use an existing shared folder }
)} - {!isReader && ( -
- setKeepOriginalFile(!keepOriginalFile)} - label={t`Keep original files`} - /> +
+ {!isReader && ( +
+ { + captureEvent("copy or move files on share") + setKeepOriginalFile(!keepOriginalFile)} + } + label={t`Keep original files`} + /> +
+ )} +
+ +
- )} -
- -
-
+ } + ) } diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFileBrowser.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFileBrowser.tsx index cabf10838c..6f062582e5 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFileBrowser.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFileBrowser.tsx @@ -263,7 +263,7 @@ const SharedFileBrowser = () => { return (
@@ -287,28 +287,29 @@ const SharedFileBrowser = () => { return ( - + diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFoldersOverview.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFoldersOverview.tsx index acae947f4d..bd205bbf8a 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFoldersOverview.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFoldersOverview.tsx @@ -17,8 +17,7 @@ import { BucketKeyPermission, useFiles } from "../../../Contexts/FilesContext" import { t, Trans } from "@lingui/macro" import { createStyles, makeStyles, useThemeSwitcher } from "@chainsafe/common-theme" import { CSFTheme } from "../../../Themes/types" -import CreateOrEditSharedFolderModal from "./CreateOrEditSharedFolderModal" -import clsx from "clsx" +import CreateOrManageSharedFolderModal from "./CreateOrManageSharedFolderModal" import { useFilesApi } from "../../../Contexts/FilesApiContext" import { ROUTE_LINKS } from "../../FilesRoutes" import SharedFolderRow from "./views/FileSystemItem/SharedFolderRow" @@ -191,19 +190,21 @@ const SharedFolderOverview = () => { > - Create a Shared Folder + Create
{isLoadingBuckets && ( -
- - +
+ + Loading your shared folders…
@@ -280,7 +281,7 @@ const SharedFolderOverview = () => { showModal={!hasSeenSharingExplainerModal} onHide={hideModal} /> - { 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 c9d140d722..62b1e5aeb7 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,11 +1,12 @@ -import React, { useCallback, useEffect, useRef } from "react" +import React, { useCallback, useEffect, useMemo, useRef } from "react" import { makeStyles, createStyles, useThemeSwitcher, useOnClickOutside, LongPressEvents } from "@chainsafe/common-theme" import { t } from "@lingui/macro" import clsx from "clsx" import { FormikTextInput, IMenuItem, - MoreIcon + MoreIcon, + Typography } from "@chainsafe/common-components" import { CSFTheme } from "../../../../../Themes/types" import { FileSystemItem } from "../../../../../Contexts/FilesContext" @@ -64,9 +65,15 @@ const useStyles = makeStyles(({ breakpoints, constants, palette }: CSFTheme) => desktopRename: { display: "flex", flexDirection: "row", + alignItems: "center", "& svg": { width: 20, height: 20 + }, + "& > span": { + fontSize: 16, + lineHeight: "20px", + marginLeft: constants.generalUnit / 2 } }, dropdownIcon: { @@ -152,15 +159,45 @@ const FileSystemGridItem = React.forwardRef( const { desktop } = useThemeSwitcher() const formRef = useRef(null) + const { + fileName, + extension + } = useMemo(() => { + if (isFolder) { + return { + fileName : name, + extension: "" + } + } + const split = name.split(".") + const extension = `.${split[split.length - 1]}` + + if (split.length === 1) { + return { + fileName : name, + extension: "" + } + } + + return { + fileName: name.slice(0, name.length - extension.length), + extension: split[split.length - 1] + } + }, [name, isFolder]) + const formik = useFormik({ initialValues: { - name + name: fileName }, validationSchema: nameValidator, onSubmit: (values: { name: string }) => { - const newName = values.name.trim() + const newName = extension !== "" ? `${values.name.trim()}.${extension}` : values.name.trim() - newName && handleRename && handleRename(file.cid, newName) + if (newName !== name) { + newName && handleRename && handleRename(file.cid, newName) + } else { + stopEditing() + } }, enableReinitialize: true }) @@ -241,6 +278,13 @@ const FileSystemGridItem = React.forwardRef( } autoFocus={editing === cid} /> + { + !isFolder && extension !== "" && ( + + { `.${extension}` } + + ) + } ) 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 01f6d2756b..f86bfafdb4 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 @@ -43,9 +43,6 @@ const useStyles = makeStyles(({ breakpoints, constants }: CSFTheme) => { width: "100%", [breakpoints.up("md")]: { margin: 0 - }, - [breakpoints.down("md")]: { - margin: `${constants.generalUnit * 4.2}px 0` } }, modalRoot: { @@ -64,18 +61,34 @@ const useStyles = makeStyles(({ breakpoints, constants }: CSFTheme) => { maxWidth: `${breakpoints.width("md")}px !important` } }, + renameModal: { + padding: constants.generalUnit * 4 + }, renameHeader: { textAlign: "center" }, + renameInputWrapper: { + display: "flex", + flexDirection: "row", + alignItems: "flex-end", + [breakpoints.down("md")]: { + margin: `${constants.generalUnit * 4.2}px 0` + }, + "& > span": { + display: "block", + fontSize: 16, + lineHeight: "20px", + marginLeft: constants.generalUnit / 2, + marginBottom: (constants.generalUnit * 2.50), + transform: "translateY(50%)" + } + }, renameFooter: { display: "flex", flexDirection: "row", alignItems: "center", justifyContent: "flex-end" }, - renameModal: { - padding: constants.generalUnit * 4 - }, okButton: { marginLeft: constants.generalUnit }, @@ -154,19 +167,55 @@ const FileSystemItem = ({ const { downloadMultipleFiles } = useFiles() const { cid, name, isFolder, content_type } = file const inSharedFolder = useMemo(() => bucket?.type === "share", [bucket]) + + const { + fileName, + extension + } = useMemo(() => { + if (isFolder) { + return { + fileName : name, + extension: "" + } + } + const split = name.split(".") + const extension = `.${split[split.length - 1]}` + + if (split.length === 1) { + return { + fileName : name, + extension: "" + } + } + + return { + fileName: name.slice(0, name.length - extension.length), + extension: split[split.length - 1] + } + }, [name, isFolder]) + const formik = useFormik({ initialValues: { - name + name: fileName }, validationSchema: nameValidator, onSubmit: (values: { name: string }) => { - const newName = values.name.trim() + const newName = extension !== "" ? `${values.name.trim()}.${extension}` : values.name.trim() - newName && handleRename && handleRename(file.cid, newName) + if (newName !== name) { + newName && handleRename && handleRename(file.cid, newName) + } else { + stopEditing() + } }, enableReinitialize: true }) + const stopEditing = useCallback(() => { + setEditing(undefined) + formik.resetForm() + }, [formik, setEditing]) + let Icon if (isFolder) { Icon = FolderFilledSvg @@ -482,7 +531,7 @@ const FileSystemItem = ({ }} closePosition="none" active={editing === cid} - onClose={() => setEditing("")} + onClose={() => stopEditing()} >
@@ -496,13 +545,22 @@ const FileSystemItem = ({ : Rename file } - +
+ + { + !isFolder && extension !== "" && ( + + { `.${extension}` } + + ) + } +
+ +
+ +
+ + {name} + + ) } - - } - options={menuItems} - style={{ focusVisible: classes.focusVisible }} - /> - - + ) } 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 15edfaf251..2357eeb676 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FilesList.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FilesList.tsx @@ -21,7 +21,10 @@ import { GridIcon, TableIcon, UploadSvg, - PlusCircleSvg + PlusCircleSvg, + MoreIcon, + SortIcon, + CheckIcon } from "@chainsafe/common-components" import { useState } from "react" import { useMemo } from "react" @@ -52,6 +55,7 @@ import SharedUsers from "../../../Elements/SharedUsers" import Menu from "../../../../UI-components/Menu" import SharingExplainerModal from "../../../SharingExplainerModal" import { useSharingExplainerModalFlag } from "../hooks/useSharingExplainerModalFlag" +import { ListItemIcon, ListItemText } from "@material-ui/core" const baseOperations: FileOperation[] = ["download", "info", "preview", "share"] const readerOperations: FileOperation[] = [...baseOperations, "report"] @@ -415,6 +419,14 @@ const FilesList = ({ isShared = false }: Props) => { } } + const toggleSortDirection = () => { + if (direction === "ascend") { + setDirection("descend") + } else { + setDirection("ascend") + } + } + // Previews const setNextPreview = () => { if ( @@ -609,8 +621,8 @@ const FilesList = ({ isShared = false }: Props) => { }, [setIsSurveyBannerVisible]) const handleViewFolder = useCallback((cid: string) => { - viewFolder && viewFolder(cid) - }, [viewFolder]) + !loadingCurrentPath && viewFolder && viewFolder(cid) + }, [viewFolder, loadingCurrentPath]) const handleOpenMoveFileDialog = useCallback((e: React.MouseEvent) => { e.preventDefault() @@ -657,6 +669,91 @@ const FilesList = ({ isShared = false }: Props) => { ], [classes.menuIcon]) + const mobileBulkActions = useMemo(() => { + const menuOptions = [] + + validBulkOps.includes("download") && (selectedItems.length > 1 || selectionContainsAFolder) && + menuOptions.push({ + contents: ( + <> + + Download as zip + + + ), + onClick: () => { + bucket && downloadMultipleFiles(selectedItems, currentPath, bucket.id) + resetSelectedItems() + } + }) + + validBulkOps.includes("move") && + menuOptions.push({ + contents: ( + <> + + Move + + + ), + onClick: (e: React.MouseEvent) => { + handleOpenMoveFileDialog(e) + setMoveModalMode("move") + } + }) + + validBulkOps.includes("recover") && + menuOptions.push({ + contents: ( + <> + + Recover + + + ), + onClick: (e: React.MouseEvent) => { + handleOpenMoveFileDialog(e) + setMoveModalMode("recover") + } + }) + + validBulkOps.includes("delete") && + menuOptions.push({ + contents: ( + <> + + Delete + + + ), + onClick: handleOpenDeleteDialog + }) + validBulkOps.includes("share") && + menuOptions.push({ + contents: ( + <> + + Share + + + ), + onClick: handleOpenShareDialog + }) + + return menuOptions + }, [ + validBulkOps, + bucket, + currentPath, + downloadMultipleFiles, + handleOpenDeleteDialog, + handleOpenMoveFileDialog, + handleOpenShareDialog, + resetSelectedItems, + selectedItems, + selectionContainsAFolder + ]) + const onShare = useCallback((fileSystemItem: FileSystemItemType) => { setSelectedItems([fileSystemItem]) handleOpenShareDialog() @@ -779,9 +876,9 @@ const FilesList = ({ isShared = false }: Props) => { ? : } - -
- {selectedItems.length > 0 && ( + {desktop && ( +
+ {selectedItems.length > 0 && <> {validBulkOps.includes("download") && (selectedItems.length > 1 || selectionContainsAFolder) && ( )} - )} -
+ } +
+ )}
{ > { data-cy="data-state-no-files" > - + No files to show @@ -882,14 +982,17 @@ const FilesList = ({ isShared = false }: Props) => { className={clsx(loadingCurrentPath && classes.fadeOutLoading)} testId="home" > - {desktop && ( + {desktop ? ( - + toggleAll()} + indeterminate={selectedItems.length > 0} + onChange={toggleAll} testId="select-all" /> @@ -928,6 +1031,91 @@ const FilesList = ({ isShared = false }: Props) => { {/* Menu */} + ) : ( + + + + 0} + onChange={toggleAll} + testId="select-all" + /> + + {selectedItems.length === 0 + ? <> + + {t`Name`} + + + } + options={[{ + contents: ( + + Sort By: + + ) + }, { + contents: ( + <> + {column === "name" && } + + Name + + + ), + onClick: () => setColumn("name") + }, { + contents: ( + <> + {column === "date_uploaded" && } + + Date Uploaded + + + ), + onClick: () => setColumn("date_uploaded") + }, { + contents: ( + <> + {column === "size" && } + + Size + + + ), + onClick: () => setColumn("size") + }]} + style={{ focusVisible: classes.focusVisible }} + /> + + + : <> + + ({selectedItems.length}) items selected + + + } + options={mobileBulkActions} + style={{ focusVisible: classes.focusVisible }} + /> + + + } + + )} {items.map((file, index) => ( @@ -1115,7 +1303,7 @@ const FilesList = ({ isShared = false }: Props) => { } { !showExplainerBeforeShare && isShareModalOpen && selectedItems.length && { + onClose={() => { setIsShareModalOpen(false) setFilePath(undefined) }} diff --git a/packages/files-ui/src/Components/Modules/FilePreviewModal.tsx b/packages/files-ui/src/Components/Modules/FilePreviewModal.tsx index b20e8a399a..e7e0ef52c5 100644 --- a/packages/files-ui/src/Components/Modules/FilePreviewModal.tsx +++ b/packages/files-ui/src/Components/Modules/FilePreviewModal.tsx @@ -322,7 +322,8 @@ const FilePreviewModal = ({ file, nextFile, previousFile, closePreview, filePath className={classes.prevNext} > {previousFile && ( - - - - ), []) - - return ( - - window.location.reload()} - > - - - - - - - - - - - - - - - - - - - ) -} - -export default App diff --git a/packages/gaming-ui/src/Components/Elements/ApiKeyCard.tsx b/packages/gaming-ui/src/Components/Elements/ApiKeyCard.tsx deleted file mode 100644 index db489b7d8b..0000000000 --- a/packages/gaming-ui/src/Components/Elements/ApiKeyCard.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from "react" -import { AccessKey } from "@chainsafe/files-api-client" -import { makeStyles, createStyles } from "@chainsafe/common-theme" -import { CSGTheme } from "../../Themes/types" -import { Button, Typography, Paper } from "@chainsafe/common-components" -import { Trans } from "@lingui/macro" -import dayjs from "dayjs" - - -const useStyles = makeStyles(({ constants }: CSGTheme) => - createStyles({ - root: { - position: "relative", - margin: constants.generalUnit, - borderRadius: constants.generalUnit / 2, - maxWidth: 250, - padding: `${constants.generalUnit * 2}px ${constants.generalUnit}px` - }, - button: { - marginTop: constants.generalUnit * 2 - } - }) -) - -interface IApiKeyCard { - apiKey: AccessKey - deleteKey: () => void -} - -const ApiKeyCard = ({ apiKey, deleteKey }: IApiKeyCard) => { - const classes = useStyles() - - return ( - - - - Id: - - - - { apiKey.id } - - - - Status: - - - - { apiKey.status } - - - - Created on: - - - - { dayjs(apiKey.created_at).format("DD MMM YYYY h:mm a") } - - - - ) -} - -export default ApiKeyCard \ No newline at end of file diff --git a/packages/gaming-ui/src/Components/Elements/CustomButton.tsx b/packages/gaming-ui/src/Components/Elements/CustomButton.tsx deleted file mode 100644 index 3d9de50746..0000000000 --- a/packages/gaming-ui/src/Components/Elements/CustomButton.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Button, IButtonProps } from "@chainsafe/common-components" -import { createStyles, ITheme, makeStyles } from "@chainsafe/common-theme" -import React, { ReactNode } from "react" -import clsx from "clsx" - -const useStyles = makeStyles(({ palette }: ITheme) => - createStyles({ - root: { - "&.gray": { - backgroundColor: palette.additional["gray"][3], - color: palette.additional["gray"][9] - } - } - }) -) - -const CUSTOM_VARIANTS = ["gray"] - -const temp = [...CUSTOM_VARIANTS] -type customVariant = typeof temp[0] - -type buttonVariant = IButtonProps["variant"] | customVariant - -interface ICustomButton extends Omit { - children: ReactNode - variant?: buttonVariant - className?: string -} - -const CustomButton: React.FC = ({ - className, - children, - variant, - ...rest -}: ICustomButton) => { - const classes = useStyles() - - const setVariant = - variant && CUSTOM_VARIANTS.includes(variant) - ? "primary" - : variant || "primary" - - return ( - - ) -} - -export default CustomButton diff --git a/packages/gaming-ui/src/Components/Elements/CustomModal.tsx b/packages/gaming-ui/src/Components/Elements/CustomModal.tsx deleted file mode 100644 index 224b120ab3..0000000000 --- a/packages/gaming-ui/src/Components/Elements/CustomModal.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { IModalProps, Modal } from "@chainsafe/common-components" -import { createStyles, makeStyles } from "@chainsafe/common-theme" -import React, { ReactNode } from "react" -import clsx from "clsx" -import { CSGTheme } from "../../Themes/types" - -const useStyles = makeStyles(({ constants, breakpoints }: CSGTheme) => - createStyles({ - root: { - "&:before": { - backgroundColor: constants.modalDefault.fadeBackground - } - }, - inner: { - [breakpoints.down("md")]: { - backgroundColor: constants.modalDefault.background, - top: "unset", - bottom: 0, - left: 0, - width: "100% !important", - transform: "unset", - borderRadiusLeftTop: `${constants.generalUnit * 1.5}px`, - borderRadiusRightTop: `${constants.generalUnit * 1.5}px`, - borderRadiusLeftBottom: 0, - borderRadiusRightBottom: 0 - } - }, - closeIcon: { - [breakpoints.down("md")]: {} - } - }) -) - -interface ICustomModal extends IModalProps { - children: ReactNode - className?: string -} - -const CustomModal: React.FC = ({ - className, - children, - injectedClass, - ...rest -}: ICustomModal) => { - const classes = useStyles() - - return ( - - {children} - - ) -} - -export default CustomModal diff --git a/packages/gaming-ui/src/Components/Elements/SecretField.tsx b/packages/gaming-ui/src/Components/Elements/SecretField.tsx deleted file mode 100644 index ba059aa666..0000000000 --- a/packages/gaming-ui/src/Components/Elements/SecretField.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useCallback, useState } from "react" -import { EyeOpenIcon, EyeClosedIcon, Typography } from "@chainsafe/common-components" -import { makeStyles, createStyles } from "@chainsafe/common-theme" - -interface Props { - value: string -} - -const useStyles = makeStyles(() => - createStyles({ - root: { - display: "flex", - justifyContent: "space-between" - } - }) -) - -const SecretField = ({ value }: Props) => { - const [showValue, setShowValue] = useState(false) - const classes = useStyles() - - const toggleShowValue = useCallback(() => { - setShowValue(!showValue) - }, [showValue]) - - return ( -
- {showValue ? value : "⚫⚫⚫⚫⚫⚫⚫⚫⚫" } - {showValue ? : } -
- ) -} - -export default SecretField \ No newline at end of file diff --git a/packages/gaming-ui/src/Components/GamingRoutes.tsx b/packages/gaming-ui/src/Components/GamingRoutes.tsx deleted file mode 100644 index 3936871e6b..0000000000 --- a/packages/gaming-ui/src/Components/GamingRoutes.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from "react" -import { Switch, ConditionalRoute } from "@chainsafe/common-components" -import LoginPage from "./Pages/LoginPage" -import { useGamingApi } from "../Contexts/GamingApiContext" -import SettingsPage from "./Pages/SettingsPage" -import DashboardPage from "./Pages/DashboardPage" - -export const SETTINGS_PATHS = ["apiKeys"] as const -export type SettingsPath = typeof SETTINGS_PATHS[number] - -export const ROUTE_LINKS = { - Landing: "/", - Dashboard: "/dashboard", - SettingsRoot: "/settings", - Settings: (path: SettingsPath) => `/settings/${path}`, - PrivacyPolicy: "https://files.chainsafe.io/privacy-policy", - Terms: "https://files.chainsafe.io/terms-of-service", - ChainSafe: "https://chainsafe.io/" -} - -const GamingRoutes = () => { - const { isLoggedIn } = useGamingApi() - - return ( - - - - - - ) -} - -export default GamingRoutes diff --git a/packages/gaming-ui/src/Components/Layouts/AppHeader.tsx b/packages/gaming-ui/src/Components/Layouts/AppHeader.tsx deleted file mode 100644 index 45ed28b61a..0000000000 --- a/packages/gaming-ui/src/Components/Layouts/AppHeader.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import React, { useCallback } from "react" -import { createStyles, makeStyles, useThemeSwitcher } from "@chainsafe/common-theme" -import clsx from "clsx" -import { - Link, - Typography, - ChainsafeLogo, - HamburgerMenu, - MenuDropdown, - PowerDownSvg, - useHistory -} from "@chainsafe/common-components" -import { ROUTE_LINKS } from "../GamingRoutes" -import { Trans } from "@lingui/macro" -import { CSGTheme } from "../../Themes/types" -import { useGamingApi } from "../../Contexts/GamingApiContext" -import { useUser } from "../../Contexts/UserContext" - -const useStyles = makeStyles( - ({ palette, animation, breakpoints, constants, zIndex }: CSGTheme) => { - return createStyles({ - root: { - position: "fixed", - display: "flex", - flexDirection: "row", - top: 0, - transitionDuration: `${animation.translate}ms`, - visibility: "hidden", - [breakpoints.up("md")]: { - width: `calc(100% - ${constants.navWidth}px)`, - padding: `${0}px ${constants.contentPadding}px ${0}px ${ - constants.contentPadding - }px`, - left: Number(constants.navWidth), - opacity: 0, - - backgroundColor: constants.header.rootBackground, - - "& > *:first-child": { - flex: "1 1 0" - }, - "&.active": { - opacity: 1, - height: "auto", - visibility: "visible", - padding: `${constants.headerTopPadding}px ${ - constants.contentPadding - }px ${0}px ${constants.contentPadding}px`, - zIndex: zIndex?.layer1 - } - }, - [breakpoints.down("md")]: { - left: 0, - width: "100%", - justifyContent: "space-between", - alignItems: "center", - position: "fixed", - backgroundColor: palette.additional["gray"][3], - "&.active": { - opacity: 1, - visibility: "visible", - height: Number(constants.mobileHeaderHeight), - zIndex: Number(zIndex?.layer1) - } - } - }, - hamburgerMenu: { - position: "absolute", - "& span": { - backgroundColor: constants.header.hamburger - } - }, - logo: { - textDecoration: "none", - display: "flex", - flexDirection: "row", - alignItems: "center", - [breakpoints.up("md")]: { - "& img": { - height: constants.generalUnit * 5, - width: "auto" - } - }, - [breakpoints.down("md")]: { - position: "absolute", - left: "50%", - top: "50%", - transform: "translate(-50%,-50%)", - "& img": { - height: constants.generalUnit * 3.25, - width: "auto" - } - } - }, - accountControls: { - display: "flex", - justifyContent: "flex-end", - alignItems: "center", - flexDirection: "row", - [breakpoints.up("md")]: { - marginLeft: constants.accountControlsPadding - }, - "& > *:first-child": { - marginRight: constants.generalUnit * 2 - } - }, - searchModule: { - [breakpoints.down("md")]: { - height: constants.mobileHeaderHeight, - position: "absolute", - width: "100%", - zIndex: zIndex?.background, - "&.active": {} - } - }, - options: { - backgroundColor: constants.header.optionsBackground, - color: constants.header.optionsTextColor, - border: `1px solid ${constants.header.optionsBorder}`, - minWidth: 145 - }, - menuItem: { - width: "100%", - display: "flex", - flexDirection: "row", - alignItems: "center", - color: constants.header.menuItemTextColor, - "& svg": { - width: constants.generalUnit * 2, - height: constants.generalUnit * 2, - marginRight: constants.generalUnit, - fill: palette.additional["gray"][7], - stroke: palette.additional["gray"][7] - } - }, - icon: { - "& svg": { - fill: constants.header.iconColor - } - }, - title : { - marginLeft: constants.generalUnit - } - }) - } -) - -interface IAppHeader { - navOpen: boolean - setNavOpen: (state: boolean) => void -} - -const AppHeader = ({ navOpen, setNavOpen }: IAppHeader) => { - const { desktop } = useThemeSwitcher() - const classes = useStyles() - const { isLoggedIn, logout } = useGamingApi() - const { history } = useHistory() - const { getProfileTitle } = useUser() - - const signOut = useCallback(async () => { - logout() - history.replace("/", {}) - }, [logout, history]) - - return ( -
- {isLoggedIn && ( - <> - {desktop ? ( - <> -
- signOut(), - contents: ( -
- - - Sign Out - -
- ) - } - ]} - /> -
- - ) : ( - <> - setNavOpen(!navOpen)} - variant={navOpen ? "active" : "default"} - className={clsx(classes.hamburgerMenu, "hamburger-menu")} - testId="hamburger-menu" - /> - - - - Dashboard - - - - )} - - )} -
- ) -} - -export default AppHeader diff --git a/packages/gaming-ui/src/Components/Layouts/AppNav.tsx b/packages/gaming-ui/src/Components/Layouts/AppNav.tsx deleted file mode 100644 index 7746860bf1..0000000000 --- a/packages/gaming-ui/src/Components/Layouts/AppNav.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import { - createStyles, - makeStyles, - useThemeSwitcher -} from "@chainsafe/common-theme" -import React, { useCallback } from "react" -import clsx from "clsx" -import { - Link, - Typography, - PowerDownSvg, - ChainsafeLogo -} from "@chainsafe/common-components" -import { ROUTE_LINKS } from "../GamingRoutes" -import { Trans } from "@lingui/macro" -import { CSGTheme } from "../../Themes/types" -import { useGamingApi } from "../../Contexts/GamingApiContext" - -const useStyles = makeStyles( - ({ palette, animation, breakpoints, constants, zIndex }: CSGTheme) => { - return createStyles({ - root: { - width: 0, - overflow: "hidden", - transitionDuration: `${animation.translate}ms`, - display: "flex", - flexDirection: "column", - position: "fixed", - left: 0, - opacity: 0, - "&.active": { - opacity: 1 - }, - [breakpoints.up("md")]: { - padding: `${constants.topPadding}px ${ - constants.generalUnit * 4.5 - }px`, - top: 0, - height: "100%", - backgroundColor: constants.nav.backgroundColor, - "&.active": { - width: `${constants.navWidth}px` - } - }, - [breakpoints.down("md")]: { - height: `calc(100% - ${constants.mobileHeaderHeight}px)`, - top: `${constants.mobileHeaderHeight}px`, - backgroundColor: constants.nav.mobileBackgroundColor, - zIndex: zIndex?.layer1, - padding: `0 ${constants.generalUnit * 4}px`, - maxWidth: "100vw", - visibility: "hidden", - "&.active": { - visibility: "visible", - width: `${constants.mobileNavWidth}px` - } - } - }, - blocker: { - display: "block", - backgroundColor: constants.nav.blocker, - position: "fixed", - top: Number(constants.mobileHeaderHeight), - left: 0, - height: `calc(100% - ${constants.mobileHeaderHeight}px)`, - width: "100%", - transitionDuration: `${animation.translate}ms`, - zIndex: zIndex?.background, - opacity: 0, - visibility: "hidden", - "&.active": { - visibility: "visible", - [breakpoints.down("md")]: { - opacity: 0.5 - } - } - }, - logo: { - textDecoration: "none", - display: "flex", - flexDirection: "row", - alignItems: "center", - - [breakpoints.up("md")]: { - "& img": { - height: constants.generalUnit * 5, - width: "auto" - }, - "& > *:first-child": { - marginRight: constants.generalUnit - } - }, - [breakpoints.down("md")]: { - position: "absolute", - left: "50%", - top: "50%", - transform: "translate(-50%,-50%)", - "& img": { - height: constants.generalUnit * 3.25, - width: "auto" - } - } - }, - navMenu: { - display: "flex", - flexDirection: "column", - marginBottom: constants.generalUnit * 8.5, - transitionDuration: `${animation.translate}ms` - }, - linksArea: { - display: "flex", - flexDirection: "column", - flex: "1 1 0", - justifyContent: "center", - transitionDuration: `${animation.translate}ms`, - "& > span": { - marginBottom: constants.generalUnit * 2 - }, - [breakpoints.up("md")]: { - height: 0 - }, - [breakpoints.down("md")]: { - transitionDuration: `${animation.translate}ms`, - color: palette.additional["gray"][3], - "&.active": {} - } - }, - navItem: { - textDecoration: "none", - display: "flex", - flexDirection: "row", - alignItems: "center", - cursor: "pointer", - padding: `${constants.generalUnit * 1.5}px 0`, - transitionDuration: `${animation.transform}ms`, - "& span": { - transitionDuration: `${animation.transform}ms`, - [breakpoints.up("md")]: { - color: constants.nav.itemColor - }, - [breakpoints.down("md")]: { - color: constants.nav.itemColorHover - } - }, - "& svg": { - transitionDuration: `${animation.transform}ms`, - width: Number(constants.svgWidth), - marginRight: constants.generalUnit * 2, - [breakpoints.up("md")]: { - fill: constants.nav.itemIconColor - }, - [breakpoints.down("md")]: { - fill: constants.nav.itemIconColorHover - } - }, - "&:hover": { - "& span": { - color: constants.nav.itemColorHover - }, - "& svg": { - fill: constants.nav.itemIconColorHover - } - }, - [breakpoints.down("md")]: { - minWidth: Number(constants.mobileNavWidth) - } - }, - navItemText: { - [breakpoints.down("md")]: { - color: palette.additional["gray"][3] - } - }, - menuItem: { - width: 100, - display: "flex", - flexDirection: "row", - alignItems: "center", - "& svg": { - width: constants.generalUnit * 2, - height: constants.generalUnit * 2, - marginRight: constants.generalUnit - } - }, - spaceUsedMargin: { - marginBottom: constants.generalUnit - }, - betaCaption: { - marginBottom: constants.generalUnit * 0.5 - } - }) - } -) - -interface IAppNav { - navOpen: boolean - setNavOpen: (state: boolean) => void -} - -const AppNav: React.FC = ({ navOpen, setNavOpen }: IAppNav) => { - const { desktop } = useThemeSwitcher() - const classes = useStyles() - - const { isLoggedIn, logout } = useGamingApi() - - const signOut = useCallback(() => { - logout() - }, [logout]) - - const handleOnClick = useCallback(() => { - if (!desktop && navOpen) { - setNavOpen(false) - } - }, [desktop, navOpen, setNavOpen]) - - return ( -
- {isLoggedIn && ( - <> - {desktop && ( -
- - - - Gaming - - -
- )} -
- -
-
- {!desktop && ( -
{ - handleOnClick() - signOut() - }} - > - - - Sign Out - -
- )} -
- {!desktop && ( -
setNavOpen(false)} - className={clsx(classes.blocker, { - active: navOpen - })} - >
- )} - - )} -
- ) -} - -export default AppNav \ No newline at end of file diff --git a/packages/gaming-ui/src/Components/Layouts/AppWrapper.tsx b/packages/gaming-ui/src/Components/Layouts/AppWrapper.tsx deleted file mode 100644 index e09654b091..0000000000 --- a/packages/gaming-ui/src/Components/Layouts/AppWrapper.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { createStyles, ITheme, makeStyles } from "@chainsafe/common-theme" -import React, { useState } from "react" -import { ReactNode } from "react" -import clsx from "clsx" -import { useGamingApi } from "../../Contexts/GamingApiContext" -import { CssBaseline } from "@chainsafe/common-components" -import AppHeader from "./AppHeader" -import AppNav from "./AppNav" - -interface IAppWrapper { - children: ReactNode | ReactNode[] -} - -const useStyles = makeStyles( - ({ animation, breakpoints, constants }: ITheme) => { - return createStyles({ - root: { - minHeight: "100vh" - }, - bodyWrapper: { - transitionDuration: `${animation.translate}ms`, - [breakpoints.up("md")]: { - padding: "0", - "&.active": { - // This moves the content areas based on the size of the nav bar - - padding: `${0}px ${constants.contentPadding}px ${0}px ${ - Number(constants.navWidth) + - Number(constants.contentPadding) - }px` - } - }, - [breakpoints.down("md")]: {} - }, - content: { - minHeight: "100vh", - [breakpoints.up("md")]: { - height: "100%", - transitionDuration: `${animation.translate}ms`, - padding: 0, - "&.active": { - height: "initial", - padding: `${constants.contentTopPadding}px 0 0` - } - }, - [breakpoints.down("md")]: { - "&.active": { - height: "initial", - padding: `${constants.mobileHeaderHeight}px 0 0` - } - } - } - }) - } -) - -const AppWrapper: React.FC = ({ children }: IAppWrapper) => { - const classes = useStyles() - const [navOpen, setNavOpen] = useState(false) - const { isLoggedIn } = useGamingApi() - - return ( -
- - -
- -
- {children} -
-
-
- ) -} - -export default AppWrapper diff --git a/packages/gaming-ui/src/Components/Modules/DashboardModule.tsx b/packages/gaming-ui/src/Components/Modules/DashboardModule.tsx deleted file mode 100644 index 7d75feb1b8..0000000000 --- a/packages/gaming-ui/src/Components/Modules/DashboardModule.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import { Button, CopyIcon, Modal, PlusIcon, Typography } from "@chainsafe/common-components" -import { createStyles, debounce, makeStyles } from "@chainsafe/common-theme" -import { AccessKey } from "@chainsafe/files-api-client" -import { Trans } from "@lingui/macro" -import React, { useCallback, useEffect, useState } from "react" -import { useGamingApi } from "../../Contexts/GamingApiContext" -import { CSGTheme } from "../../Themes/types" -import ApiKeyCard from "../Elements/ApiKeyCard" - -const useStyles = makeStyles(({ breakpoints, constants, palette, zIndex }: CSGTheme) => - createStyles({ - root: { - position: "relative", - margin: constants.generalUnit - }, - header: { - display: "flex", - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - [breakpoints.down("md")]: { - marginTop: constants.generalUnit - } - }, - controls: { - display: "flex", - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - "& > button": { - marginLeft: constants.generalUnit - } - }, - dataArea: { - marginTop: constants.generalUnit * 2, - display: "flex", - flexDirection: "row", - justifyContent: "space-between", - flexWrap: "wrap", - "& > *": { - margin: constants.generalUnit, - width:"100%", - [breakpoints.up("xs")]: { - maxWidth: `calc(100% - ${constants.generalUnit * 2}px)` - }, - [breakpoints.up("sm")]: { - maxWidth: `calc(50% - ${constants.generalUnit * 2}px)` - }, - [breakpoints.up("md")]: { - maxWidth: `calc(33% - ${constants.generalUnit * 2}px)` - }, - [breakpoints.up("lg")]: { - maxWidth: `calc(25% - ${constants.generalUnit * 2}px)` - }, - [breakpoints.up("xl")]: { - maxWidth: `calc(20% - ${constants.generalUnit * 2}px)` - } - } - }, - modalRoot: { - zIndex: zIndex?.blocker, - [breakpoints.down("md")]: {} - }, - modalInner: { - [breakpoints.down("md")]: { - bottom: - Number(constants?.mobileButtonHeight) + constants.generalUnit, - borderTopLeftRadius: `${constants.generalUnit * 1.5}px`, - borderTopRightRadius: `${constants.generalUnit * 1.5}px`, - maxWidth: `${breakpoints.width("md")}px !important` - } - }, - modalHeading: { - textAlign: "center", - marginBottom: constants.generalUnit * 4 - }, - modalContent: { - display: "flex", - flexDirection: "column", - padding: constants.generalUnit * 4 - }, - secretContainer: { - display: "flex", - justifyContent: "space-between", - marginBottom: constants.generalUnit * 0.5 - }, - copyBox: { - display: "flex", - justifyContent: "space-between", - alignItems: "center", - cursor: "pointer", - color: palette.text.secondary - }, - copyIcon: { - fontSize: "14px", - fill: constants.profile.icon, - [breakpoints.down("md")]: { - fontSize: "18px", - fill: palette.additional["gray"][9] - } - }, - secret: { - maxWidth: "95%", - overflowWrap: "anywhere" - }, - field: { - marginBottom: constants.generalUnit * 4 - } - }) -) - -const DashboardModule = () => { - const classes = useStyles() - const { gamingApiClient } = useGamingApi() - const [keys, setKeys] = useState([]) - const [newKey, setNewKey] = useState() - const [isNewKeyModalOpen, setIsNewKeyModalOpen] = useState(false) - const [copiedSecret, setCopiedSecret] = useState(false) - const debouncedCopiedSecret = - debounce(() => setCopiedSecret(false), 3000) - - const copySecret = async () => { - if (newKey?.secret) { - try { - await navigator.clipboard.writeText(newKey.secret) - setCopiedSecret(true) - debouncedCopiedSecret() - } catch (err) { - console.error(err) - } - } - } - - const fetchAccessKeys = useCallback(() => { - gamingApiClient.listAccessKeys() - .then(keys => setKeys(keys.filter(key => key.type === "gaming"))) - .catch(console.error) - }, [gamingApiClient]) - - const createGamingAccessKey = useCallback(() => { - gamingApiClient.createAccessKey({ type: "gaming" }) - .then((key) => { - setNewKey(key) - fetchAccessKeys() - setIsNewKeyModalOpen(true) - }) - .catch(console.error) - }, [fetchAccessKeys, gamingApiClient]) - - const deleteAccessKey = useCallback((id: string) => { - gamingApiClient.deleteAccessKey(id) - .then(fetchAccessKeys) - .catch(console.error) - }, [gamingApiClient, fetchAccessKeys]) - - useEffect(() => { - fetchAccessKeys() - }, [fetchAccessKeys]) - - return ( - <> -
-
- - - Dashboard - - -
- -
-
-
- { - keys.map((key: AccessKey, index: number) => ( - deleteAccessKey(key.id)} - apiKey={key} />)) - } -
-
- -
- - New Key - - - Key ID - - {newKey?.id} - - Secret - -
-
- - Make sure to save the secret, as it can only be displayed once. - - {copiedSecret && ( - - Copied! - - )} -
-
- - {newKey?.secret} - - -
-
- -
-
- - ) -} - -export default DashboardModule \ No newline at end of file diff --git a/packages/gaming-ui/src/Components/Modules/LoginModule.tsx b/packages/gaming-ui/src/Components/Modules/LoginModule.tsx deleted file mode 100644 index c4268a24c0..0000000000 --- a/packages/gaming-ui/src/Components/Modules/LoginModule.tsx +++ /dev/null @@ -1,435 +0,0 @@ -import React, { useState } from "react" -import { Button, GithubLogoIcon, GoogleLogoIcon, Loading, MailIcon, Typography } from "@chainsafe/common-components" -import { createStyles, makeStyles, useThemeSwitcher } from "@chainsafe/common-theme" -import { CSGTheme } from "../../Themes/types" -import { t, Trans } from "@lingui/macro" -import { useGamingApi } from "../../Contexts/GamingApiContext" -import { useWeb3 } from "@chainsafe/web3-context" -import { ROUTE_LINKS } from "../GamingRoutes" -import clsx from "clsx" -import { IdentityProvider } from "@chainsafe/files-api-client" -import PasswordlessEmail from "./LoginModule/PasswordlessEmail" - -const useStyles = makeStyles( - ({ constants, palette, breakpoints, typography }: CSGTheme) => - createStyles({ - root: { - backgroundColor: constants.loginModule.background, - border: `1px solid ${constants.landing.border}`, - boxShadow: constants.landing.boxShadow, - alignItems: "center", - borderRadius: 6, - [breakpoints.up("md")]:{ - minHeight: "64vh", - justifyContent: "space-between", - width: 440 - }, - [breakpoints.down("md")]: { - padding: `${constants.generalUnit * 4}px ${constants.generalUnit * 2}px`, - justifyContent: "center", - width: `calc(100vw - ${constants.generalUnit * 2}px)` - } - }, - buttonSection: { - [breakpoints.up("md")]: { - position: "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)" - }, - [breakpoints.down("md")]: { - display: "flex", - flexDirection: "column", - justifyContent: "space-evenly" - } - }, - connectingWallet: { - textAlign: "center", - alignItems: "center", - display: "flex", - flexDirection: "column", - "& > *": { - fontWeight: 400 - }, - [breakpoints.up("md")]: { - padding: `${constants.generalUnit * 20}px ${constants.generalUnit * 8}px`, - "& > *": { - paddingBottom: `${constants.generalUnit * 5}px` - } - }, - [breakpoints.down("md")]: { - justifyContent: "space-evenly" - } - }, - button: { - width: 240, - fontWeight: typography.fontWeight.medium, - marginBottom: constants.generalUnit * 2, - "& .icon" : { - fontSize: 25 - }, - "&:last-child": { - marginBottom: 0 - } - }, - error: { - color: palette.error.main, - paddingBottom: constants.generalUnit * 2, - maxWidth: 240 - }, - headerText: { - [breakpoints.up("md")]: { - paddingTop: constants.generalUnit * 4, - paddingBottom: constants.generalUnit * 8 - }, - [breakpoints.down("md")]: { - paddingTop: constants.generalUnit * 3, - paddingBottom: constants.generalUnit * 3, - textAlign: "center" - } - }, - footer: { - backgroundColor: constants.landing.footerBg, - color: constants.landing.footerText, - padding: `${constants.generalUnit * 2.5}px ${constants.generalUnit * 1.5}px`, - width: "100%", - "& > *": { - marginRight: constants.generalUnit * 3.5 - }, - [breakpoints.down("md")]: { - display: "none" - } - }, - connectWalletFooter: { - backgroundColor: constants.landing.background, - color: constants.landing.footerText, - padding: `${constants.generalUnit * 4.375}px ${constants.generalUnit * 7}px`, - width: "100%", - textAlign: "center", - "& > *": { - fontWeight: 400 - }, - [breakpoints.down("md")]: { - display: "none" - } - }, - loader: { - marginTop: constants.generalUnit, - padding: 0 - }, - buttonLink: { - color: palette.additional["gray"][10], - outline: "none", - textDecoration: "underline", - cursor: "pointer", - textAlign: "center" - }, - web3Button: { - minHeight: 41 - } - }) -) - -interface IInitialScreen { - className?: string -} - -const LoginModule = ({ className }: IInitialScreen) => { - const { selectWallet, resetAndSelectWallet, login, resetStatus, status } = useGamingApi() - const { desktop } = useThemeSwitcher() - const { wallet } = useWeb3() - const classes = useStyles() - const [loginMode, setLoginMode] = useState() - const [error, setError] = useState() - const maintenanceMode = process.env.REACT_APP_MAINTENANCE_MODE === "true" - const [isConnecting, setIsConnecting] = useState(false) - - const handleSelectWalletAndConnect = async () => { - setError(undefined) - try { - await selectWallet() - } catch (error) { - setError(t`There was an error connecting your wallet`) - } - } - - const handleResetAndSelectWallet = async () => { - setError(undefined) - try { - await resetAndSelectWallet() - } catch (error) { - setError(t`There was an error connecting your wallet`) - } - } - - const resetLogin = async () => { - setError(undefined) - setLoginMode(undefined) - resetStatus() - } - - const handleLogin = async (loginType: IdentityProvider) => { - setError("") - setIsConnecting(true) - setLoginMode(loginType) - try { - await login(loginType) - } catch (error: any) { - let errorMessage = t`There was an error authenticating` - - // Invalid signature, or contract wallet not deployed - if (error?.error?.code === 403 && error?.error?.message?.includes("Invalid signature")) { - errorMessage = t`Failed to validate signature. - If you are using a contract wallet, please make - sure you have activated your wallet.` - } - - // User rejected the signature request (WalletConnect be sassy) - if (error?.message === "Just nope" || error?.code === 4001) { - errorMessage = t`Failed to get signature` - } - - // DirectAuth popup was closed - if (error?.message === "user closed popup") { - errorMessage = t`The authentication popup was closed` - } - setError(errorMessage) - } - setIsConnecting(false) - } - - const ConnectWallet = () => { - if (!wallet) { - console.error("No wallet found") - return null - } - - return ( -
-
- - -
- - Go back - -
-
-
-
- )} - - const WalletConnection = () => { - return ( -
- Connect Wallet to Gaming - {status === "awaiting confirmation" && - - You will need to sign a message in your wallet to complete sign in. - } - {status === "logging in" && <> - - Hold on, we are logging you in… - - - } -
- ) - } - - const WalletSelection = () => { - return ( - <> -
- - -
-