diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..07a0d212 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# FlexGet server to proxy API requests to (default: http://localhost:5050) +SERVER=http://localhost:5050 diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 3625ead2..00000000 --- a/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -dist/ -!.eslintrc.* -*.d.ts diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 0a03fd65..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,106 +0,0 @@ -module.exports = { - extends: [ - 'airbnb-typescript', - 'plugin:jest-dom/recommended', - 'plugin:testing-library/recommended', - "plugin:@typescript-eslint/eslint-recommended", - // "plugin:@typescript-eslint/recommended", - // "plugin:@typescript-eslint/recommended-requiring-type-checking", - 'plugin:prettier/recommended', - 'prettier', - 'prettier/@typescript-eslint', - 'prettier/react', - ], - parser: '@typescript-eslint/parser', - plugins: ['jest', 'emotion', 'react-hooks'], - env: { - browser: true, - node: true, - 'jest/globals': true, - }, - settings: { - 'import/resolver': { - webpack: { - config: 'webpack.config.js', - }, - }, - }, - globals: { - fetchMock: true, - __DEV__: true, - __VERSION__: true, - }, - parserOptions: { - warnOnUnsupportedTypeScriptVersion: false, - project: './tsconfig.eslint.json', - }, - rules: { - // typescript - '@typescript-eslint/ban-types': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }], - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/no-empty-function': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unused-expressions': 'error', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - vars: 'all', - args: 'after-used', - ignoreRestSiblings: true, - argsIgnorePattern: '^_', - }, - ], - '@typescript-eslint/prefer-nullish-coalescing': 'error', - '@typescript-eslint/prefer-optional-chain': 'error', - '@typescript-eslint/unified-signatures': 'warn', - - // emotion - 'emotion/import-from-emotion': 'error', - 'emotion/no-vanilla': 'error', - 'emotion/styled-import': 'error', - - // import - 'import/extensions': ['error', 'ignorePackages', { - ts: 'never', - tsx: 'never', - js: 'never', - jsx: 'never', - }], - 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], - 'import/no-named-as-default': 'off', - 'import/prefer-default-export': 'off', - 'import/no-cycle': 'off', - - // core - 'no-constant-condition': 'off', - 'no-underscore-dangle': 'off', - 'no-unused-expressions': 'off', - - // React - 'react/destructuring-assignment': ['error', 'always', { ignoreClassFields: true }], - 'react/forbid-prop-types': 'off', - 'react/jsx-filename-extension': ['error', { extensions: ['.tsx', '.jsx'] }], - 'react/jsx-no-bind': 'off', - 'react/jsx-props-no-spreading': 'off', - 'react/no-array-index-key': 'off', - 'react/prop-types': 'off', - 'react/state-in-constructor': ['error', 'never'], - 'react/static-property-placement': 'off', - - // hooks - 'react-hooks/exhaustive-deps': 'warn', - 'react-hooks/rules-of-hooks': 'error', - - // a11y - 'jsx-a11y/anchor-is-valid': [ - 'error', - { - components: ['Link'], - specialLink: ['to', 'hrefLeft', 'hrefRight'], - aspects: ['noHref', 'invalidHref', 'preferButton'], - }, - ], - }, -}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e40c89aa..111c3391 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,30 +14,21 @@ jobs: strategy: matrix: node: - - 12.x + - 20.x steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - name: Cache Dependencies - id: cache-dependencies - uses: actions/cache@v2 - with: - path: node_modules - key: ${{ matrix.node }}-deps-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ matrix.node }}-deps- + cache: 'yarn' - name: Install Dependencies - if: steps.cache-dependencies.outputs.cache-hit != 'true' run: yarn install --frozen-lockfile - name: Run Linter run: yarn lint - name: Run Tests run: yarn test --ci --coverage -i - name: Send to Code Cov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} - diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 72e6c107..b16a458e 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -8,13 +8,13 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/github-script@0.9.0 + - uses: actions/checkout@v4 + - uses: actions/github-script@v7 name: Create Deployment with: github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} script: | - await github.repos.createDeployment({ + await github.rest.repos.createDeployment({ ...context.repo, ref: context.ref.slice(11), }); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a5b01f8..ee896c50 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,13 +6,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [12.x] + node: [20.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} fetch-depth: 0 - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 name: Checkout Flexget with: ref: develop @@ -28,21 +28,14 @@ jobs: GIT_EMAIL: ${{ secrets.GIT_EMAIL }} GIT_USERNAME: ${{ secrets.GIT_USERNAME }} - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - name: Cache Dependencies - id: cache-dependencies - uses: actions/cache@v2 - with: - path: node_modules - key: ${{ matrix.node }}-deps-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ matrix.node }}-deps- + cache: 'yarn' - name: Install Dependencies run: yarn install --frozen-lockfile - name: Set Deployment Status Pending - uses: deliverybot/deployment-status@v1 + uses: chrnorm/deployment-status@v2 with: state: pending token: ${{ github.token }} @@ -55,13 +48,13 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Set Deployment Status Success - uses: deliverybot/deployment-status@v1 + uses: chrnorm/deployment-status@v2 with: state: success token: ${{ github.token }} - name: Set Deployment Status Failure if: failure() || cancelled() - uses: deliverybot/deployment-status@v1 + uses: chrnorm/deployment-status@v2 with: state: failure token: ${{ github.token }} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..209e3ef4 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 00000000..759f7882 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,68 @@ +version: '3' + +dotenv: ['.env'] + +tasks: + setup: + desc: Install project dependencies (run `nvm use` first to activate the correct Node) + preconditions: + - sh: which npm + msg: "npm not found in PATH. Activate the correct Node version first (e.g. 'nvm use')." + cmds: + - npm install -g yarn + - yarn install + + install: + desc: Install project dependencies + cmds: + - yarn install + sources: + - package.json + - yarn.lock + generates: + - node_modules/.yarn-integrity + + dev: + desc: Start development server + cmds: + - yarn start + + build: + desc: Build for production + cmds: + - yarn build + + test: + desc: Run test suite + cmds: + - yarn test + + test:watch: + desc: Run tests in watch mode + cmds: + - yarn test:watch + + test:coverage: + desc: Run tests with coverage report + cmds: + - yarn test --coverage + + lint: + desc: Run ESLint + cmds: + - yarn lint + + lint:fix: + desc: Fix auto-fixable ESLint errors + cmds: + - yarn lint --fix + + typecheck: + desc: Type-check without emitting + cmds: + - ./node_modules/.bin/tsc --noEmit + + clean: + desc: Remove build output + cmds: + - rm -rf dist/ diff --git a/babel.config.js b/babel.config.js index db6bd9b4..32942db4 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,4 +1,5 @@ module.exports = function config(api) { + api.cache(true); const presets = [ [ '@babel/preset-env', @@ -8,18 +9,16 @@ module.exports = function config(api) { modules: false, }, ], - '@babel/preset-react', - ...(api.env() !== 'test' - ? ['@emotion/babel-preset-css-prop', { sourceMap: api.env() !== 'test' }] - : []), + ['@babel/preset-react', { runtime: 'automatic', importSource: '@emotion/react' }], + '@babel/preset-typescript', ]; const plugins = [ + '@emotion/babel-plugin', '@babel/plugin-syntax-dynamic-import', '@babel/plugin-proposal-nullish-coalescing-operator', '@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-class-properties', 'babel-plugin-polished', - 'react-hot-loader/babel', ]; return { diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..27e99d16 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,134 @@ +const tseslint = require('typescript-eslint'); +const react = require('eslint-plugin-react'); +const reactHooks = require('eslint-plugin-react-hooks'); +const importPlugin = require('eslint-plugin-import'); +const jest = require('eslint-plugin-jest'); +const jestDom = require('eslint-plugin-jest-dom'); +const testingLibrary = require('eslint-plugin-testing-library'); +const a11y = require('eslint-plugin-jsx-a11y'); +const emotion = require('@emotion/eslint-plugin'); +const prettier = require('eslint-config-prettier'); +const globals = require('globals'); + +module.exports = tseslint.config( + { ignores: ['dist/', 'node_modules/', '**/*.d.ts'] }, + + // TypeScript base rules + ...tseslint.configs.recommended, + + // React (classic transform: react/jsx-uses-react marks React as used) + react.configs.flat.recommended, + + // Prettier disables formatting rules that conflict with prettier + prettier, + + { + plugins: { + import: importPlugin, + 'jsx-a11y': a11y, + '@emotion': emotion, + 'react-hooks': reactHooks, + }, + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + __DEV__: 'readonly', + __VERSION__: 'readonly', + }, + parser: tseslint.parser, + parserOptions: { + project: './tsconfig.eslint.json', + ecmaFeatures: { jsx: true }, + }, + }, + settings: { + react: { version: 'detect' }, + 'import/resolver': { typescript: { project: './tsconfig.json' } }, + }, + rules: { + // TypeScript + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }], + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-expressions': 'error', + '@typescript-eslint/no-unused-vars': [ + 'error', + { vars: 'all', args: 'after-used', ignoreRestSiblings: true, argsIgnorePattern: '^_' }, + ], + '@typescript-eslint/prefer-nullish-coalescing': 'error', + '@typescript-eslint/prefer-optional-chain': 'error', + '@typescript-eslint/unified-signatures': 'warn', + '@typescript-eslint/no-empty-object-type': 'warn', + '@typescript-eslint/no-require-imports': 'off', + '@typescript-eslint/no-wrapper-object-types': 'warn', + + // Emotion + '@emotion/import-from-emotion': 'error', + '@emotion/no-vanilla': 'error', + '@emotion/styled-import': 'error', + + // Import + 'import/extensions': ['error', 'ignorePackages', { ts: 'never', tsx: 'never', js: 'never', jsx: 'never' }], + 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], + 'import/no-named-as-default': 'off', + 'import/prefer-default-export': 'off', + 'import/no-cycle': 'off', + + // Core + 'no-constant-condition': 'off', + 'no-underscore-dangle': 'off', + 'no-unused-expressions': 'off', + + // React + 'react/destructuring-assignment': ['error', 'always', { ignoreClassFields: true }], + 'react/display-name': 'off', + 'react/forbid-prop-types': 'off', + 'react/no-unknown-property': ['error', { ignore: ['css'] }], + 'react/jsx-filename-extension': ['error', { extensions: ['.tsx', '.jsx'] }], + 'react/jsx-no-bind': 'off', + 'react/jsx-props-no-spreading': 'off', + 'react/no-array-index-key': 'off', + 'react/prop-types': 'off', + 'react/state-in-constructor': ['error', 'never'], + 'react/static-property-placement': 'off', + + // React Hooks + 'react-hooks/exhaustive-deps': 'warn', + 'react-hooks/rules-of-hooks': 'error', + + // A11y + 'jsx-a11y/anchor-is-valid': [ + 'error', + { + components: ['Link'], + specialLink: ['to', 'hrefLeft', 'hrefRight'], + aspects: ['noHref', 'invalidHref', 'preferButton'], + }, + ], + }, + }, + + // Test files + { + files: ['**/*.spec.{ts,tsx}', '**/__mocks__/**/*.{ts,tsx}'], + plugins: { jest, 'jest-dom': jestDom, 'testing-library': testingLibrary }, + languageOptions: { + globals: { ...globals.jest }, + }, + rules: { + ...jest.configs['flat/recommended'].rules, + ...jestDom.configs['flat/recommended'].rules, + ...testingLibrary.configs['flat/react'].rules, + 'jest/no-conditional-expect': 'warn', + 'jest/no-test-prefixes': 'warn', + 'jest/valid-title': 'warn', + 'testing-library/no-container': 'warn', + 'testing-library/no-manual-cleanup': 'warn', + 'testing-library/no-node-access': 'warn', + 'testing-library/prefer-presence-queries': 'warn', + 'testing-library/prefer-screen-queries': 'warn', + }, + }, +); diff --git a/jest.config.js b/jest.config.js index 8d66577f..9588c592 100644 --- a/jest.config.js +++ b/jest.config.js @@ -21,25 +21,22 @@ module.exports = { globals: { __DEV__: false, __VERSION__: '2.0.28', - 'ts-jest': { - tsConfig: 'tsconfig.jest.json', - babelConfig: true, - diagnostics: { - warnOnly: true, - }, - }, + }, + transform: { + '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.jest.json', diagnostics: { warnOnly: true } }], }, moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], - preset: 'ts-jest', moduleDirectories: ['node_modules', 'src'], moduleNameMapper: { '\\.css$': 'identity-obj-proxy', '\\.(gif|ttf|eot|svg|png)$': '/src/__mocks__/fileMock.ts', - 'monaco-editor': '/node_modules/react-monaco-editor', - '^worker-loader!': '/src/__mocks__/worker.ts', + '^@monaco-editor/react$': '/src/__mocks__/monacoEditor.tsx', + '^monaco-editor(/.*)?$': '/src/__mocks__/monaco-editor.ts', + '^monaco-yaml$': '/src/__mocks__/monacoYaml.ts', }, - snapshotSerializers: ['enzyme-to-json/serializer', 'jest-emotion'], - setupFiles: ['raf/polyfill', '/src/utils/tests/setupFiles.ts'], - setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect', 'jest-extended'], - testURL: 'http://localhost/', + snapshotSerializers: ['@emotion/jest/serializer'], + setupFiles: ['/src/utils/tests/setupFiles.ts'], + setupFilesAfterEnv: ['@testing-library/jest-dom', 'jest-extended/all'], + testEnvironment: 'jsdom', + testEnvironmentOptions: { url: 'http://localhost/' }, }; diff --git a/package.json b/package.json index 8acabc8c..2dbe1219 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,12 @@ "version": "2.0.30.dev", "description": "Flexget Web UI", "main": "index.js", + "packageManager": "yarn@1.22.22", "scripts": { - "start": "NODE_ENV=development BABEL_ENV=development babel-node ./server.js", - "build": "NODE_ENV=production BABEL_ENV=production webpack", + "start": "NODE_ENV=development webpack serve", + "build": "NODE_ENV=production webpack", "debug": "DEBUG=true yarn build", - "lint": "eslint --ext .js --ext .jsx --ext .ts --ext .tsx src", + "lint": "eslint src", "lint:css": "stylelint './src/**/*.js'", "test": "NODE_ENV=test jest", "test:watch": "jest --watch", @@ -33,21 +34,22 @@ "@actions/github": "^2.2.0", "@babel/cli": "^7.0.0", "@babel/core": "^7.0.0", - "@babel/node": "^7.0.0", "@babel/plugin-proposal-class-properties": "^7.0.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.7.4", "@babel/plugin-proposal-optional-chaining": "^7.7.4", "@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/preset-env": "^7.0.0", - "@babel/preset-react": "^7.0.0", + "@babel/preset-react": "^7.29.7", "@babel/preset-typescript": "^7.7.7", - "@emotion/babel-preset-css-prop": "^10.0.14", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/eslint-plugin": "^11.12.0", + "@emotion/jest": "^11.14.2", + "@faker-js/faker": "^9", "@octokit/rest": "^16.33.0", - "@testing-library/jest-dom": "^5.1.0", - "@testing-library/react": "^9.4.0", - "@types/enzyme": "^3.10.3", - "@types/enzyme-adapter-react-16": "^1.0.5", - "@types/enzyme-to-json": "^1.5.3", + "@pmmmwh/react-refresh-webpack-plugin": "^0.6.2", + "@testing-library/jest-dom": "^6", + "@testing-library/react": "^14", + "@testing-library/user-event": "^14", "@types/faker": "^4.1.5", "@types/fetch-mock": "^7.3.2", "@types/jest": "^25.1.1", @@ -55,95 +57,77 @@ "@types/node-fetch": "^2.5.4", "@types/oboe": "^2.0.28", "@types/qs": "^6.5.3", - "@types/react": "^16.9.16", - "@types/react-router-dom": "^5.1.0", - "@types/react-test-renderer": "^16.9.1", + "@types/react": "~18.2.0", + "@types/react-dom": "~18.2.0", + "@types/react-test-renderer": "^18", "@types/react-virtualized": "^9.21.7", "@types/semver": "^7.1.0", "@types/semver-compare": "^1.0.1", "@types/uuid": "^3.4.6", "@types/yaml": "^1.2.0", - "@typescript-eslint/eslint-plugin": "^4.11.0", - "@typescript-eslint/parser": "^4.11.0", - "babel-eslint": "^10.0.3", - "babel-jest": "^25.1.0", + "babel-jest": "^29", "babel-loader": "^8.0.0", "babel-plugin-dynamic-import-node": "^2.3.0", "babel-plugin-polished": "^1.1.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "codecov": "^3.7.1", - "css-loader": "^3.1.0", - "enzyme": "^3.10.0", - "enzyme-adapter-react-16": "^1.14.0", - "enzyme-to-json": "^3.3.5", - "eslint": "^7.16.0", - "eslint-config-airbnb-typescript": "^12.0.0", - "eslint-config-prettier": "^7.1.0", - "eslint-import-resolver-webpack": "^0.12.1", - "eslint-plugin-emotion": "^10.0.14", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jest": "^23.6.0", - "eslint-plugin-jest-dom": "^2.0.0", - "eslint-plugin-jsx-a11y": "^6.3.0", + "css-loader": "^7", + "eslint": "^9", + "eslint-config-prettier": "^9", + "eslint-import-resolver-typescript": "^4.4.5", + "eslint-plugin-import": "^2.30", + "eslint-plugin-jest": "^28", + "eslint-plugin-jest-dom": "^5", + "eslint-plugin-jsx-a11y": "^6.10", "eslint-plugin-prettier": "^3.3.0", - "eslint-plugin-react": "^7.20.0", - "eslint-plugin-react-hooks": "^4.2.0", - "eslint-plugin-testing-library": "^2.2.0", - "faker": "^4.1.0", + "eslint-plugin-react": "^7.37", + "eslint-plugin-react-hooks": "^5", + "eslint-plugin-testing-library": "^7", "fetch-mock": "^9.0.0-beta.2", - "file-loader": "^5.0.2", - "fork-ts-checker-webpack-plugin": "^4.0.3", - "html-webpack-plugin": "^3.2.0", + "fork-ts-checker-webpack-plugin": "^9", + "globals": "^17.7.0", + "html-webpack-plugin": "^5", "identity-obj-proxy": "^3.0.0", - "jest": "^25.1.0", - "jest-emotion": "^10.0.27", - "jest-extended": "^0.11.5", - "mini-css-extract-plugin": "^0.9.0", - "prettier": "^2.2.1", - "raf": "^3.4.1", - "react-test-renderer": "^16.12.0", - "style-loader": "^1.1.3", - "ts-jest": "^25.1.0", - "ts-loader": "^6.0.4", - "typescript": "^4.1.2", - "url-loader": "^3.0.0", - "webpack": "^4.36.1", - "webpack-bundle-analyzer": "^3.3.2", - "webpack-cli": "^3.3.10", - "webpack-dev-server": "^3.7.2", - "worker-loader": "^2.0.0" + "jest": "^29", + "jest-environment-jsdom": "^29", + "jest-extended": "^4", + "mini-css-extract-plugin": "^2", + "monaco-editor-webpack-plugin": "^7.1.1", + "prettier": "^3.0.0", + "react-refresh": "^0.18.0", + "react-test-renderer": "^18", + "style-loader": "^4", + "ts-jest": "^29", + "typescript": "^5.5.0", + "typescript-eslint": "^8", + "webpack": "^5", + "webpack-bundle-analyzer": "^4", + "webpack-cli": "^5", + "webpack-dev-server": "^5" }, "dependencies": { "@babel/runtime": "^7.5.5", "@babel/runtime-corejs3": "^7.5.5", - "@emotion/core": "^10.0.14", - "@emotion/styled": "^10.0.14", - "@hot-loader/react-dom": "^16.11.0", + "@emotion/react": "^11", + "@emotion/styled": "^11", "@loadable/component": "^5.11.0", - "@material-ui/core": "^4.9.0", - "@material-ui/icons": "^4.5.1", - "@material-ui/lab": "^4.0.0-alpha.40", - "@material-ui/styles": "^4.9.0", + "@monaco-editor/react": "^4.6", + "@mui/icons-material": "^5", + "@mui/material": "^5", "core-js": "^3.1.4", - "emotion-theming": "^10.0.19", - "formik": "^2.1.1", + "formik": "^2.4.0", "humps": "^2.0.1", - "jss": "^9.8.7", - "jss-preset-default": "^4.5.0", - "monaco-editor": "^0.19.2", + "monaco-editor": "^0.50", "monaco-themes": "^0.3.3", - "monaco-yaml": "^2.4.0", + "monaco-yaml": "^5", "normalize.css": "^8.0.1", "oboe": "^2.1.4", "polished": "^3.4.2", "qs": "^6.9.7", - "react": "^16.12.0", - "react-dom": "^16.12.0", - "react-hot-loader": "^4.12.10", + "react": "^18", + "react-dom": "^18", "react-infinite-scroller": "^1.2.4", - "react-monaco-editor": "^0.32.1", - "react-router": "^5.1.2", - "react-router-dom": "^5.1.2", + "react-router-dom": "^6", "react-virtualized": "^9.21.1", "regenerator-runtime": "^0.13.3", "semver": "^7.1.2", diff --git a/prettier.config.js b/prettier.config.js index 434571f3..187ae54e 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -2,6 +2,5 @@ module.exports = { trailingComma: 'all', singleQuote: true, printWidth: 100, - parser: 'typescript', arrowParens: 'avoid', }; diff --git a/server.js b/server.js deleted file mode 100755 index 8fce4594..00000000 --- a/server.js +++ /dev/null @@ -1,24 +0,0 @@ -const webpack = require('webpack'); -const WebpackDevServer = require('webpack-dev-server'); -const config = require('./webpack.config'); - -const server = new WebpackDevServer(webpack(config), { - contentBase: './app', - publicPath: config.output.publicPath, - hot: true, - historyApiFallback: true, - disableHostCheck: true, - proxy: { - '/api': process.env.SERVER || 'http://localhost:5050', - }, - stats: { - colors: true, - }, -}); - -server.listen(process.env.PORT || 8000, process.env.HOST || '0.0.0.0', (err) => { - if (err) { - console.log(err); - } - console.log(`Listening at ${process.env.HOST || 'localhost'}:${process.env.PORT || 8000}`); -}); diff --git a/src/Root.tsx b/src/Root.tsx index a72a5bb4..c613163b 100644 --- a/src/Root.tsx +++ b/src/Root.tsx @@ -1,12 +1,10 @@ -import React, { Suspense, FC, lazy } from 'react'; -import { hot } from 'react-hot-loader/root'; -import { StylesProvider } from '@material-ui/styles'; -import { CssBaseline } from '@material-ui/core'; -import { Route, Switch, BrowserRouter } from 'react-router-dom'; -import { Global, css } from '@emotion/core'; +import { Suspense, FC, lazy } from 'react'; +import { CssBaseline } from '@mui/material'; +import { Routes, Route, BrowserRouter } from 'react-router-dom'; +import { Global, css } from '@emotion/react'; import PrivateRoute from 'core/routes/PrivateRoute'; import Layout from 'core/layout/Layout'; -import Routes from 'core/routes/Routes'; +import AppRoutes from 'core/routes/Routes'; import registerHistory from 'plugins/history'; import registerLog from 'plugins/log'; import registerTasks from 'plugins/tasks'; @@ -61,36 +59,35 @@ const Root: FC = () => ( - - - - - - - - ( - - - - - - - - - - - - - )} - /> - - - - - - + + + + }> + + } /> + + + + }> + + } />} /> + } /> + + + + + + } + /> + + + + + ); -export default hot(Root); +export default Root; diff --git a/src/__mocks__/monaco-editor.ts b/src/__mocks__/monaco-editor.ts new file mode 100644 index 00000000..e3de91d8 --- /dev/null +++ b/src/__mocks__/monaco-editor.ts @@ -0,0 +1,4 @@ +export const editor = { + defineTheme: jest.fn(), +}; +export const languages = {}; diff --git a/src/__mocks__/monacoEditor.tsx b/src/__mocks__/monacoEditor.tsx new file mode 100644 index 00000000..f3380140 --- /dev/null +++ b/src/__mocks__/monacoEditor.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +const MonacoEditor: React.FC = () =>
; +export default MonacoEditor; +export const useMonaco = () => null; diff --git a/src/__mocks__/monacoYaml.ts b/src/__mocks__/monacoYaml.ts new file mode 100644 index 00000000..d333e1cc --- /dev/null +++ b/src/__mocks__/monacoYaml.ts @@ -0,0 +1 @@ +export const configureMonacoYaml = jest.fn(); diff --git a/src/app.tsx b/src/app.tsx index df33eda2..acdbd475 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,9 +1,6 @@ -import 'core-js/stable'; -import 'regenerator-runtime/runtime'; -import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import Root from 'Root'; import 'typeface-roboto'; // eslint-disable-line import/extensions import 'normalize.css'; -ReactDOM.render(, document.getElementById('react')); +createRoot(document.getElementById('react')!).render(); diff --git a/src/common/ExpandButton.tsx b/src/common/ExpandButton.tsx index 4e357692..45be0c96 100644 --- a/src/common/ExpandButton.tsx +++ b/src/common/ExpandButton.tsx @@ -1,7 +1,7 @@ -import React, { useCallback, forwardRef } from 'react'; -import { css } from '@emotion/core'; -import { ExpandMore } from '@material-ui/icons'; -import { IconButton, IconButtonProps, Theme } from '@material-ui/core'; +import { useCallback, forwardRef } from 'react'; +import { css } from '@emotion/react'; +import { ExpandMore } from '@mui/icons-material'; +import { IconButton, IconButtonProps, Theme } from '@mui/material'; export type Props = IconButtonProps & { open: boolean; diff --git a/src/common/LoadingSpinner.tsx b/src/common/LoadingSpinner.tsx index 4577cd30..8b77250f 100644 --- a/src/common/LoadingSpinner.tsx +++ b/src/common/LoadingSpinner.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; -import { CircularProgress } from '@material-ui/core'; -import { css } from '@emotion/core'; +import { CircularProgress } from '@mui/material'; +import { css } from '@emotion/react'; const spinner = css` margin: 0 auto; @@ -15,6 +15,7 @@ const wrapper = css` interface Props { loading?: boolean; + children?: React.ReactNode; } const LoadingSpinner: FC = ({ loading = true, children }) => { diff --git a/src/common/WaveSpinner.tsx b/src/common/WaveSpinner.tsx index 276fbf4a..0f690566 100644 --- a/src/common/WaveSpinner.tsx +++ b/src/common/WaveSpinner.tsx @@ -1,6 +1,5 @@ -import React from 'react'; -import { keyframes, css } from '@emotion/core'; -import { Theme } from '@material-ui/core'; +import { keyframes, css } from '@emotion/react'; +import { Theme } from '@mui/material'; const stretchDelay = keyframes` 0%, 40%, 100% { diff --git a/src/common/inputs/SelectField.tsx b/src/common/inputs/SelectField.tsx index 097789cf..a87295d5 100644 --- a/src/common/inputs/SelectField.tsx +++ b/src/common/inputs/SelectField.tsx @@ -1,5 +1,5 @@ -import React, { FC } from 'react'; -import { MenuItem, useTheme, useMediaQuery, TextField, TextFieldProps } from '@material-ui/core'; +import { FC } from 'react'; +import { MenuItem, useTheme, useMediaQuery, TextField, TextFieldProps } from '@mui/material'; export interface Option { value: string; diff --git a/src/common/inputs/formik/Autocomplete.tsx b/src/common/inputs/formik/Autocomplete.tsx index 91247c0b..95491223 100644 --- a/src/common/inputs/formik/Autocomplete.tsx +++ b/src/common/inputs/formik/Autocomplete.tsx @@ -1,24 +1,19 @@ -import React, { useCallback, ChangeEvent } from 'react'; +import React, { useCallback } from 'react'; import { useField } from 'formik'; -import { - AutocompleteProps, - Autocomplete as BaseAutocomplete, - UseAutocompleteSingleProps, - UseAutocompleteMultipleProps, -} from '@material-ui/lab'; -import { TextFieldProps, TextField } from '@material-ui/core'; +import { AutocompleteProps, Autocomplete as BaseAutocomplete, TextFieldProps, TextField } from '@mui/material'; -type Props = Omit, 'renderInput'> & - (Multiple extends true ? UseAutocompleteMultipleProps : UseAutocompleteSingleProps) & { - name: string; - InputProps: TextFieldProps; - renderInput?: AutocompleteProps['renderInput']; // eslint-disable-line react/require-default-props - }; +type Props = Omit< + AutocompleteProps, + 'renderInput' +> & { + name: string; + InputProps: TextFieldProps; +}; const Autocomplete = ({ name, InputProps, multiple, ...props }: Props) => { const [field, { touched, error }, { setValue }] = useField(name); - const handleChange = useCallback((_: ChangeEvent, value: unknown) => setValue(value), [setValue]); + const handleChange = useCallback((_: React.SyntheticEvent, value: unknown) => setValue(value), [setValue]); return ( = forwardRef(({ name, ...props }, ref) => { +const Checkbox = forwardRef(({ name, ...props }, ref) => { const [field, , { setValue }] = useField(name); const handleChange = useCallback( diff --git a/src/common/inputs/formik/DirectionButton.tsx b/src/common/inputs/formik/DirectionButton.tsx index 8f82e011..56d46699 100644 --- a/src/common/inputs/formik/DirectionButton.tsx +++ b/src/common/inputs/formik/DirectionButton.tsx @@ -1,6 +1,6 @@ import React, { useCallback, forwardRef, useMemo } from 'react'; import { useField } from 'formik'; -import { IconButtonProps } from '@material-ui/core'; +import { IconButtonProps } from '@mui/material'; import { Direction, toggleDirection } from 'utils/query'; import ExpandButton from 'common/ExpandButton'; diff --git a/src/common/inputs/formik/SelectField.tsx b/src/common/inputs/formik/SelectField.tsx index ce61f7f9..72d2d774 100644 --- a/src/common/inputs/formik/SelectField.tsx +++ b/src/common/inputs/formik/SelectField.tsx @@ -1,5 +1,5 @@ -import React, { FC } from 'react'; -import { MenuItem, useMediaQuery, useTheme } from '@material-ui/core'; +import { FC } from 'react'; +import { MenuItem, useMediaQuery, useTheme } from '@mui/material'; import TextField, { Props as TextFieldProps } from './TextField'; interface Option { diff --git a/src/common/inputs/formik/TextField.tsx b/src/common/inputs/formik/TextField.tsx index 04c36e93..3f0e9e73 100644 --- a/src/common/inputs/formik/TextField.tsx +++ b/src/common/inputs/formik/TextField.tsx @@ -1,5 +1,5 @@ -import React, { FC } from 'react'; -import BaseTextField, { TextFieldProps } from '@material-ui/core/TextField'; +import { FC } from 'react'; +import BaseTextField, { TextFieldProps } from '@mui/material/TextField'; import { useField } from 'formik'; export type Props = TextFieldProps & { diff --git a/src/common/inputs/formik/__snapshots__/TextField.spec.tsx.snap b/src/common/inputs/formik/__snapshots__/TextField.spec.tsx.snap index 6ce3a54e..70ffa63d 100644 --- a/src/common/inputs/formik/__snapshots__/TextField.spec.tsx.snap +++ b/src/common/inputs/formik/__snapshots__/TextField.spec.tsx.snap @@ -1,18 +1,222 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`common/TextField renders correctly 1`] = ` +.emotion-0 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + position: relative; + min-width: 0; + padding: 0; + margin: 0; + border: 0; + vertical-align: top; +} + +.emotion-1 { + font-family: "Roboto","Helvetica","Arial",sans-serif; + font-weight: 400; + font-size: 1.6rem; + line-height: 1.4375em; + letter-spacing: 0.00938em; + color: rgba(55,71,79,0.87); + box-sizing: border-box; + position: relative; + cursor: text; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + border-radius: 4px; +} + +.emotion-1.Mui-disabled { + color: rgba(55,71,79,0.38); + cursor: default; +} + +.emotion-1:hover .MuiOutlinedInput-notchedOutline { + border-color: rgba(55,71,79,0.87); +} + +@media (hover: none) { + .emotion-1:hover .MuiOutlinedInput-notchedOutline { + border-color: rgba(0, 0, 0, 0.23); + } +} + +.emotion-1.Mui-focused .MuiOutlinedInput-notchedOutline { + border-color: #ef6c00; + border-width: 2px; +} + +.emotion-1.Mui-error .MuiOutlinedInput-notchedOutline { + border-color: #d32f2f; +} + +.emotion-1.Mui-disabled .MuiOutlinedInput-notchedOutline { + border-color: rgba(55,71,79,0.26); +} + +.emotion-2 { + font: inherit; + letter-spacing: inherit; + color: currentColor; + padding: 4px 0 5px; + border: 0; + box-sizing: content-box; + background: none; + height: 1.4375em; + margin: 0; + -webkit-tap-highlight-color: transparent; + display: block; + min-width: 0; + width: 100%; + -webkit-animation-name: mui-auto-fill-cancel; + animation-name: mui-auto-fill-cancel; + -webkit-animation-duration: 10ms; + animation-duration: 10ms; + padding: 16.5px 14px; +} + +.emotion-2::-webkit-input-placeholder { + color: currentColor; + opacity: 0.42; + -webkit-transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; +} + +.emotion-2::-moz-placeholder { + color: currentColor; + opacity: 0.42; + -webkit-transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; +} + +.emotion-2:-ms-input-placeholder { + color: currentColor; + opacity: 0.42; + -webkit-transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; +} + +.emotion-2::-ms-input-placeholder { + color: currentColor; + opacity: 0.42; + -webkit-transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; +} + +.emotion-2:focus { + outline: 0; +} + +.emotion-2:invalid { + box-shadow: none; +} + +.emotion-2::-webkit-search-decoration { + -webkit-appearance: none; +} + +label[data-shrink=false]+.MuiInputBase-formControl .emotion-2::-webkit-input-placeholder { + opacity: 0!important; +} + +label[data-shrink=false]+.MuiInputBase-formControl .emotion-2::-moz-placeholder { + opacity: 0!important; +} + +label[data-shrink=false]+.MuiInputBase-formControl .emotion-2:-ms-input-placeholder { + opacity: 0!important; +} + +label[data-shrink=false]+.MuiInputBase-formControl .emotion-2::-ms-input-placeholder { + opacity: 0!important; +} + +label[data-shrink=false]+.MuiInputBase-formControl .emotion-2:focus::-webkit-input-placeholder { + opacity: 0.42; +} + +label[data-shrink=false]+.MuiInputBase-formControl .emotion-2:focus::-moz-placeholder { + opacity: 0.42; +} + +label[data-shrink=false]+.MuiInputBase-formControl .emotion-2:focus:-ms-input-placeholder { + opacity: 0.42; +} + +label[data-shrink=false]+.MuiInputBase-formControl .emotion-2:focus::-ms-input-placeholder { + opacity: 0.42; +} + +.emotion-2.Mui-disabled { + opacity: 1; + -webkit-text-fill-color: rgba(55,71,79,0.38); +} + +.emotion-2:-webkit-autofill { + -webkit-animation-duration: 5000s; + animation-duration: 5000s; + -webkit-animation-name: mui-auto-fill; + animation-name: mui-auto-fill; +} + +.emotion-2:-webkit-autofill { + border-radius: inherit; +} + +.emotion-3 { + text-align: left; + position: absolute; + bottom: 0; + right: 0; + top: -5px; + left: 0; + margin: 0; + padding: 0 8px; + pointer-events: none; + border-radius: inherit; + border-style: solid; + border-width: 1px; + overflow: hidden; + min-width: 0%; + border-color: rgba(0, 0, 0, 0.23); +} + +.emotion-4 { + float: unset; + width: auto; + overflow: hidden; + padding: 0; + line-height: 11px; + -webkit-transition: width 150ms cubic-bezier(0.0, 0, 0.2, 1) 0ms; + transition: width 150ms cubic-bezier(0.0, 0, 0.2, 1) 0ms; +} +
+
+ + + ​ + + +
`; diff --git a/src/common/styles.tsx b/src/common/styles.tsx index 5bf3b36c..4344730b 100644 --- a/src/common/styles.tsx +++ b/src/common/styles.tsx @@ -1,21 +1,16 @@ -import React, { forwardRef } from 'react'; +import { forwardRef } from 'react'; import { Link as RouterLink, LinkProps } from 'react-router-dom'; -import st, { CreateStyled } from '@emotion/styled'; -import { css } from '@emotion/core'; -import { Paper, Theme } from '@material-ui/core'; +import styled from '@emotion/styled'; +import { css } from '@emotion/react'; +import { Paper, Theme } from '@mui/material'; -const styled = st as CreateStyled; - -export const PaperWrapper = styled(Paper)` +export const PaperWrapper = styled(Paper)<{ elevation?: number }>` padding: 2.4rem; display: flex; flex: 1; height: 100%; box-sizing: border-box; flex-direction: column; - ${({ theme }) => theme.breakpoints.up('sm')} { - padding-top: 0; - } `; export const Spacer = styled.div` @@ -23,22 +18,28 @@ export const Spacer = styled.div` `; export const NoPaddingWrapper = styled.div` - height: calc(100% + ${({ theme }) => theme.typography.pxToRem(theme.spacing(2))}); - margin: -${({ theme }) => theme.typography.pxToRem(theme.spacing(1))}; + height: calc(100% + ${({ theme }) => theme.typography.pxToRem(parseFloat(theme.spacing(2)))}); + margin: -${({ theme }) => theme.typography.pxToRem(parseFloat(theme.spacing(1)))}; display: flex; flex-direction: column; ${({ theme }) => theme.breakpoints.up('sm')} { - height: calc(100% + ${({ theme }) => theme.typography.pxToRem(theme.spacing(4))}); - margin: -${({ theme }) => theme.typography.pxToRem(theme.spacing(2))}; + height: calc(100% + ${({ theme }) => theme.typography.pxToRem(parseFloat(theme.spacing(4)))}); + margin: -${({ theme }) => theme.typography.pxToRem(parseFloat(theme.spacing(2)))}; } `; +export const dialogFields = css` + display: flex; + flex-direction: column; + gap: 1rem; +`; + export const backgroundColor = (theme: Theme, color: string) => css` background-color: ${color}; color: ${theme.palette.getContrastText(color)}; `; export const Link = forwardRef((props, ref) => ( - + )); diff --git a/src/core/auth/Login.tsx b/src/core/auth/Login.tsx index 512896ed..a6ac4db0 100644 --- a/src/core/auth/Login.tsx +++ b/src/core/auth/Login.tsx @@ -1,17 +1,15 @@ -import React, { FC, useEffect } from 'react'; +import { FC, useEffect } from 'react'; import { useContainer } from 'unstated-next'; -import { Redirect, RouteComponentProps } from 'react-router-dom'; -import { StaticContext } from 'react-router'; +import { Navigate, useLocation } from 'react-router-dom'; import { AuthContainer } from 'core/auth/hooks'; import { useFlexgetAPI } from 'core/api'; import LoginCard from './LoginCard'; import SplashScreen from './Splash'; import Logo from './Logo'; -type Props = Partial>; - -const LoginPage: FC = ({ location }) => { - const { from } = location?.state ?? { from: { pathname: '/' } }; +const LoginPage: FC = () => { + const location = useLocation(); + const from = (location.state as { from?: { pathname: string } })?.from?.pathname ?? '/'; const [loggedIn, setLoggedIn] = useContainer(AuthContainer); const [loginState, checkLogin] = useFlexgetAPI('/user/token'); @@ -31,7 +29,7 @@ const LoginPage: FC = ({ location }) => { } if (loggedIn) { - return ; + return ; } return ( diff --git a/src/core/auth/LoginCard.spec.tsx b/src/core/auth/LoginCard.spec.tsx index a55cb2e4..78ca4299 100644 --- a/src/core/auth/LoginCard.spec.tsx +++ b/src/core/auth/LoginCard.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { cleanup, fireEvent } from '@testing-library/react'; +import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react'; import fetchMock from 'fetch-mock'; import { AuthContainer } from 'core/auth/hooks'; import { renderWithWrapper } from 'utils/tests'; @@ -27,11 +27,12 @@ describe('core/Login/LoginCard', () => { }); it('should call handleSubmit on submit', async () => { - const { findByLabelText, getByRole } = renderWithWrapper(); - expect(await findByLabelText('login-state')).toHaveTextContent('false'); + const { container } = renderWithWrapper(); + expect(screen.getByLabelText('login-state')).toHaveTextContent('false'); - const form = getByRole('form'); + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + const form = container.querySelector('form')!; fireEvent.submit(form); - expect(await findByLabelText('login-state')).toHaveTextContent('true'); + await waitFor(() => expect(screen.getByLabelText('login-state')).toHaveTextContent('true')); }); }); diff --git a/src/core/auth/LoginCard.tsx b/src/core/auth/LoginCard.tsx index cfd9ccb4..aaae6e70 100644 --- a/src/core/auth/LoginCard.tsx +++ b/src/core/auth/LoginCard.tsx @@ -1,8 +1,8 @@ -import React, { FC } from 'react'; +import { FC } from 'react'; import { Form, Formik } from 'formik'; import { useContainer } from 'unstated-next'; -import { css } from '@emotion/core'; -import { CardContent, Card, Button, CardActions, Theme } from '@material-ui/core'; +import { css } from '@emotion/react'; +import { CardContent, Card, Button, CardActions, Theme } from '@mui/material'; import { useFlexgetAPI } from 'core/api'; import { Method } from 'utils/fetch'; import TextField from 'common/inputs/formik/TextField'; diff --git a/src/core/auth/Logo.tsx b/src/core/auth/Logo.tsx index ee9a7b92..071a9d67 100644 --- a/src/core/auth/Logo.tsx +++ b/src/core/auth/Logo.tsx @@ -1,6 +1,6 @@ -import React, { FC } from 'react'; -import { css } from '@emotion/core'; -import { Theme } from '@material-ui/core'; +import { FC } from 'react'; +import { css } from '@emotion/react'; +import { Theme } from '@mui/material'; import headerImage from 'images/header.png'; export const logo = (theme: Theme) => css` diff --git a/src/core/auth/Splash.tsx b/src/core/auth/Splash.tsx index 697204ed..714696e8 100644 --- a/src/core/auth/Splash.tsx +++ b/src/core/auth/Splash.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import WaveSpinner from 'common/WaveSpinner'; import Logo from './Logo'; diff --git a/src/core/auth/__snapshots__/Login.spec.tsx.snap b/src/core/auth/__snapshots__/Login.spec.tsx.snap index 0ca76af5..509849f0 100644 --- a/src/core/auth/__snapshots__/Login.spec.tsx.snap +++ b/src/core/auth/__snapshots__/Login.spec.tsx.snap @@ -1,193 +1,191 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`core/auth/Login renders correctly when logged in 1`] = ` -
-
-
+
+
`; - -exports[`core/auth/Login renders correctly when logged out 1`] = ` -
-
-
-
-
-
-
- -
- -
-
-
- -
- -
-
-
-
- -
- -
-
-`; diff --git a/src/core/auth/__snapshots__/Splash.spec.tsx.snap b/src/core/auth/__snapshots__/Splash.spec.tsx.snap index cb642913..eb536a97 100644 --- a/src/core/auth/__snapshots__/Splash.spec.tsx.snap +++ b/src/core/auth/__snapshots__/Splash.spec.tsx.snap @@ -1,41 +1,188 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`pages/splash renders correctly 1`] = ` -
-
-
+
+
diff --git a/src/core/entry/cards/BaseCard.tsx b/src/core/entry/cards/BaseCard.tsx index 5f40ed6c..3c55feaa 100644 --- a/src/core/entry/cards/BaseCard.tsx +++ b/src/core/entry/cards/BaseCard.tsx @@ -1,8 +1,8 @@ import React, { FC, useCallback } from 'react'; -import { css } from '@emotion/core'; -import { CardMedia, Theme, CardContent } from '@material-ui/core'; +import { css } from '@emotion/react'; +import { CardMedia, Theme, CardContent } from '@mui/material'; import { getCachedUrl } from 'utils/image'; -import { Skeleton } from '@material-ui/lab'; +import { Skeleton } from '@mui/material'; interface Props { images?: string[] | string; @@ -10,6 +10,7 @@ interface Props { className?: string; label: string; loading?: boolean; + children?: React.ReactNode; } const image = css` @@ -19,8 +20,8 @@ const image = css` const poster = (theme: Theme) => css` width: 55%; float: left; - padding-right: ${theme.typography.pxToRem(theme.spacing(2))}; - padding-bottom: ${theme.typography.pxToRem(theme.spacing(2))}; + padding-right: ${theme.typography.pxToRem(parseFloat(theme.spacing(2)))}; + padding-bottom: ${theme.typography.pxToRem(parseFloat(theme.spacing(2)))}; ${theme.breakpoints.up('sm')} { width: 40%; } @@ -45,12 +46,12 @@ const text = css` const summary = (theme: Theme) => css` height: 1.4rem; width: 25%; - margin: ${theme.typography.pxToRem(theme.spacing(2))} 0; + margin: ${theme.typography.pxToRem(parseFloat(theme.spacing(2)))} 0; `; const header = (theme: Theme) => css` width: 70%; - margin-bottom: ${theme.typography.pxToRem(theme.spacing(1))}; + margin-bottom: ${theme.typography.pxToRem(parseFloat(theme.spacing(1)))}; `; const BaseCard: FC = ({ @@ -62,9 +63,10 @@ const BaseCard: FC = ({ loading = false, }) => { const imageCss = useCallback((theme: Theme) => [image, isPoster && poster(theme)], [isPoster]); - const textCss = useCallback((theme: Theme) => [text, isPoster && posterSkeletonWrapper(theme)], [ - isPoster, - ]); + const textCss = useCallback( + (theme: Theme) => [text, isPoster && posterSkeletonWrapper(theme)], + [isPoster], + ); const summaryCss = useCallback( (theme: Theme) => [summary(theme), isPoster && posterSkeletonWrapper(theme)], [isPoster], @@ -78,7 +80,7 @@ const BaseCard: FC = ({ return (
{loading ? ( - + ) : ( !!imageUrl && ( css` font-size: 1.5rem; font-weight: 500; - margin-top: ${theme.typography.pxToRem(theme.spacing(0.5))}; + margin-top: ${theme.typography.pxToRem(parseFloat(theme.spacing(0.5)))}; `; const EpisodeCard: FC = ({ series: rawSeries, entry: rawEpisode, className, children }) => { diff --git a/src/core/entry/cards/LinkDropdown.tsx b/src/core/entry/cards/LinkDropdown.tsx index 4f4165d6..9e4801d2 100644 --- a/src/core/entry/cards/LinkDropdown.tsx +++ b/src/core/entry/cards/LinkDropdown.tsx @@ -1,6 +1,6 @@ import React, { FC, useState, useCallback } from 'react'; -import { MoreVert } from '@material-ui/icons'; -import { IconButton, Menu, MenuItem } from '@material-ui/core'; +import { MoreVert } from '@mui/icons-material'; +import { IconButton, Menu, MenuItem } from '@mui/material'; interface Option { label: string; diff --git a/src/core/entry/cards/Movie.tsx b/src/core/entry/cards/Movie.tsx index 8cd86f9f..afbc7339 100644 --- a/src/core/entry/cards/Movie.tsx +++ b/src/core/entry/cards/Movie.tsx @@ -1,8 +1,8 @@ import React, { FC } from 'react'; -import { Typography, Theme } from '@material-ui/core'; +import { Typography, Theme } from '@mui/material'; import { normalizeMinutes } from 'utils/time'; -import { css } from '@emotion/core'; -import { StarRate } from '@material-ui/icons'; +import { css } from '@emotion/react'; +import { StarRate } from '@mui/icons-material'; import { MovieEntry, IMDBFields, TMDBFields, TraktFields } from '../fields/movies'; import { Bullet, titleArea, ratingLine, selectableType } from './styles'; import BaseCard from './BaseCard'; @@ -12,19 +12,20 @@ import { useMovieLookup } from '../lookup/movies'; interface Props { entry: MovieEntry; className?: string; + children?: React.ReactNode; } const summary = (theme: Theme) => css` font-size: 1.5rem; font-weight: 500; - margin-top: ${theme.typography.pxToRem(theme.spacing(0.5))}; + margin-top: ${theme.typography.pxToRem(parseFloat(theme.spacing(0.5)))}; `; const originalTitle = (theme: Theme) => css` font-size: 2.2rem; font-weight: 400; font-style: oblique; - margin-bottom: ${theme.typography.pxToRem(theme.spacing(0.5))}; + margin-bottom: ${theme.typography.pxToRem(parseFloat(theme.spacing(0.5)))}; `; const MovieCard: FC = ({ entry, className, children }) => { @@ -75,7 +76,8 @@ const MovieCard: FC = ({ entry, className, children }) => { )} - {rating} ({votes}) + {typeof rating === 'number' ? rating.toFixed(1) : rating} ( + {votes}) {quality} diff --git a/src/core/entry/cards/Series.tsx b/src/core/entry/cards/Series.tsx index 176f63d7..28030c7c 100644 --- a/src/core/entry/cards/Series.tsx +++ b/src/core/entry/cards/Series.tsx @@ -1,8 +1,8 @@ import React, { FC } from 'react'; -import { Typography, Theme } from '@material-ui/core'; -import { css } from '@emotion/core'; +import { Typography, Theme } from '@mui/material'; +import { css } from '@emotion/react'; import BaseCard from 'core/entry/cards/BaseCard'; -import { StarRate } from '@material-ui/icons'; +import { StarRate } from '@mui/icons-material'; import { SeriesEntry, TraktFields, TVDBFields, TVMazeFields } from '../fields/series'; import { Bullet, titleArea, ratingLine, selectableType } from './styles'; import LinkDropdown from './LinkDropdown'; @@ -11,12 +11,13 @@ import { useSeriesLookup } from '../lookup/series'; interface Props { entry: SeriesEntry; className?: string; + children?: React.ReactNode; } const summary = (theme: Theme) => css` font-size: 1.5rem; font-weight: 500; - margin-top: ${theme.typography.pxToRem(theme.spacing(0.5))}; + margin-top: ${theme.typography.pxToRem(parseFloat(theme.spacing(0.5)))}; `; const SeriesCard: FC = ({ entry, className, children }) => { @@ -60,7 +61,7 @@ const SeriesCard: FC = ({ entry, className, children }) => {
- {rating} + {typeof rating === 'number' ? rating.toFixed(1) : rating} {network} diff --git a/src/core/entry/cards/index.tsx b/src/core/entry/cards/index.tsx index e7f9e05c..36044d6a 100644 --- a/src/core/entry/cards/index.tsx +++ b/src/core/entry/cards/index.tsx @@ -16,6 +16,7 @@ import DefaultCard from './Default'; interface Props { entry: RawEntry; className?: string; + children?: React.ReactNode; } const EntryCard: FC = ({ entry, className, children }) => { diff --git a/src/core/entry/cards/styles.tsx b/src/core/entry/cards/styles.tsx index c254ac36..0e2726a9 100644 --- a/src/core/entry/cards/styles.tsx +++ b/src/core/entry/cards/styles.tsx @@ -1,12 +1,12 @@ -import React, { FC } from 'react'; -import { css } from '@emotion/core'; -import { Theme } from '@material-ui/core'; +import { FC } from 'react'; +import { css } from '@emotion/react'; +import { Theme } from '@mui/material'; export const Bullet: FC<{ className?: string }> = ({ className }) => ( css` display: inline-block; - margin: 0 ${theme.typography.pxToRem(theme.spacing(1))}; + margin: 0 ${theme.typography.pxToRem(parseFloat(theme.spacing(1)))}; `} className={className} > diff --git a/src/core/entry/fields/episodes.ts b/src/core/entry/fields/episodes.ts index 8cea8237..6678102c 100644 --- a/src/core/entry/fields/episodes.ts +++ b/src/core/entry/fields/episodes.ts @@ -8,7 +8,7 @@ export enum EpisodeFieldNames { ID = 'episodeId', } -export const enum TVDBFields { +export enum TVDBFields { Name = 'tvdbEpName', Image = 'tvdbEpImage', Description = 'tvdbEpOverview', @@ -16,13 +16,13 @@ export const enum TVDBFields { Url = 'tvdbUrl', } -export const enum TraktFields { +export enum TraktFields { Name = 'traktEpName', Description = 'traktEpOverview', ID = 'traktEpId', } -export const enum TVMazeFields { +export enum TVMazeFields { Name = 'tvmazeEpisodeName', Image = 'tvmazeEpisodeOriginalImage', Description = 'tvmazeEpisodeSummary', diff --git a/src/core/entry/fields/movies.ts b/src/core/entry/fields/movies.ts index df743c44..b23d47c3 100644 --- a/src/core/entry/fields/movies.ts +++ b/src/core/entry/fields/movies.ts @@ -13,7 +13,7 @@ export enum MovieFieldNames { OriginalName = 'originalName', } -export const enum IMDBFields { +export enum IMDBFields { Genres = 'imdbGenres', Posters = 'imdbPhoto', Rating = 'imdbScore', @@ -24,7 +24,7 @@ export const enum IMDBFields { ID = 'imdbId', } -export const enum TraktFields { +export enum TraktFields { Genres = 'traktGenres', Rating = 'traktRating', Votes = 'traktVotes', @@ -34,7 +34,7 @@ export const enum TraktFields { ID = 'traktMovieId', } -export const enum TMDBFields { +export enum TMDBFields { Genres = 'tmdbGenres', Posters = 'tmdbPosters', Backdrops = 'tmdbBackdrops', diff --git a/src/core/entry/fields/series.ts b/src/core/entry/fields/series.ts index dd0a6db4..487458b1 100644 --- a/src/core/entry/fields/series.ts +++ b/src/core/entry/fields/series.ts @@ -14,7 +14,7 @@ export enum SeriesFieldNames { ID = 'seriesId', } -export const enum TVDBFields { +export enum TVDBFields { Genres = 'tvdbGenres', Posters = 'tvdbPosters', Backdrops = 'tvdbBanner', @@ -27,7 +27,7 @@ export const enum TVDBFields { ID = 'tvdbId', } -export const enum TraktFields { +export enum TraktFields { Genres = 'traktGenres', Rating = 'traktSeriesRating', Description = 'traktSeriesOverview', @@ -39,7 +39,7 @@ export const enum TraktFields { ID = 'traktId', } -export const enum TVMazeFields { +export enum TVMazeFields { Genres = 'tvmazeGenres', Posters = 'tvmazeSeriesOriginalImage', Description = 'tvmazeSeriesSummary', diff --git a/src/core/entry/fixtures.ts b/src/core/entry/fixtures.ts index df6c20ca..251e7ae1 100644 --- a/src/core/entry/fixtures.ts +++ b/src/core/entry/fixtures.ts @@ -1,4 +1,4 @@ -import { random, internet, image, date, lorem } from 'faker'; +import { faker } from '@faker-js/faker'; import { RawEntry } from './types'; import { IMDBFields, TraktFields, TMDBFields, RawMovieEntry } from './fields/movies'; import { @@ -15,121 +15,121 @@ import { } from './fields/series'; export const makeRawEntry = (): RawEntry => ({ - originalUrl: internet.url(), - title: random.words(), - quality: random.words(), + originalUrl: faker.internet.url(), + title: faker.word.words(), + quality: faker.word.words(), }); export const withMovieRawEntry = (e: RawEntry) => ({ ...e, - movieName: random.words(), - movieYear: date.past().getFullYear(), + movieName: faker.word.words(), + movieYear: faker.date.past().getFullYear(), }); export const withIMDBFields = (e: RawMovieEntry) => ({ ...e, - [IMDBFields.Genres]: random.words().split(' '), - [IMDBFields.Posters]: image.imageUrl(), - [IMDBFields.Rating]: random.number(10), - [IMDBFields.Votes]: random.number(100), - [IMDBFields.Description]: lorem.paragraph(), - [IMDBFields.Url]: internet.url(), - [IMDBFields.Runtime]: random.number(150), - [IMDBFields.ID]: random.word(), + [IMDBFields.Genres]: faker.word.words().split(' '), + [IMDBFields.Posters]: faker.image.url(), + [IMDBFields.Rating]: faker.number.int({ max: 10 }), + [IMDBFields.Votes]: faker.number.int({ max: 100 }), + [IMDBFields.Description]: faker.lorem.paragraph(), + [IMDBFields.Url]: faker.internet.url(), + [IMDBFields.Runtime]: faker.number.int({ max: 150 }), + [IMDBFields.ID]: faker.word.sample(), }); export const withTraktFields = (e: RawMovieEntry) => ({ ...e, - [TraktFields.Genres]: random.words().split(' '), - [TraktFields.Rating]: random.number(10), - [TraktFields.Votes]: random.number(100), - [TraktFields.Description]: lorem.paragraph(), - [TraktFields.Url]: internet.url(), - [TraktFields.ID]: random.word(), - [TraktFields.Runtime]: random.number(150), + [TraktFields.Genres]: faker.word.words().split(' '), + [TraktFields.Rating]: faker.number.int({ max: 10 }), + [TraktFields.Votes]: faker.number.int({ max: 100 }), + [TraktFields.Description]: faker.lorem.paragraph(), + [TraktFields.Url]: faker.internet.url(), + [TraktFields.ID]: faker.word.sample(), + [TraktFields.Runtime]: faker.number.int({ max: 150 }), }); export const withTMDBFields = (e: RawMovieEntry) => ({ ...e, - [TMDBFields.Genres]: random.words().split(' '), - [TMDBFields.Posters]: image.imageUrl(), - [TMDBFields.Backdrops]: image.imageUrl(), - [TMDBFields.Runtime]: random.number(150), - [TMDBFields.Rating]: random.number(10), - [TMDBFields.Votes]: random.number(100), - [TMDBFields.Url]: internet.url(), - [TMDBFields.ID]: random.word(), + [TMDBFields.Genres]: faker.word.words().split(' '), + [TMDBFields.Posters]: faker.image.url(), + [TMDBFields.Backdrops]: faker.image.url(), + [TMDBFields.Runtime]: faker.number.int({ max: 150 }), + [TMDBFields.Rating]: faker.number.int({ max: 10 }), + [TMDBFields.Votes]: faker.number.int({ max: 100 }), + [TMDBFields.Url]: faker.internet.url(), + [TMDBFields.ID]: faker.word.sample(), }); export const withEpisodeRawEntry = (e: RawEntry): RawEpisodeEntry => ({ ...e, seriesEpisode: 1, seriesSeason: 2, - seriesName: random.word(), + seriesName: faker.word.sample(), seriesId: 'S02E01', }); export const withTraktEpisodeFields = (e: RawEpisodeEntry) => ({ ...e, - [TraktEpisodeFields.Name]: random.words(), - [TraktEpisodeFields.Description]: lorem.paragraph(), - [TraktEpisodeFields.ID]: random.word(), + [TraktEpisodeFields.Name]: faker.word.words(), + [TraktEpisodeFields.Description]: faker.lorem.paragraph(), + [TraktEpisodeFields.ID]: faker.word.sample(), }); export const withTVDBEpisodeFields = (e: RawEpisodeEntry) => ({ ...e, - [TVDBEpisodeFields.Image]: image.imageUrl(), - [TVDBEpisodeFields.Description]: lorem.paragraph(), - [TVDBEpisodeFields.ID]: random.word(), + [TVDBEpisodeFields.Image]: faker.image.url(), + [TVDBEpisodeFields.Description]: faker.lorem.paragraph(), + [TVDBEpisodeFields.ID]: faker.word.sample(), }); export const withTVMazeEpisodeFields = (e: RawEpisodeEntry) => ({ ...e, - [TVMazeEpisodeFields.Name]: random.words(), - [TVMazeEpisodeFields.Image]: image.imageUrl(), - [TVMazeEpisodeFields.Description]: lorem.paragraph(), - [TVMazeEpisodeFields.Url]: internet.url(), - [TVMazeEpisodeFields.ID]: random.word(), + [TVMazeEpisodeFields.Name]: faker.word.words(), + [TVMazeEpisodeFields.Image]: faker.image.url(), + [TVMazeEpisodeFields.Description]: faker.lorem.paragraph(), + [TVMazeEpisodeFields.Url]: faker.internet.url(), + [TVMazeEpisodeFields.ID]: faker.word.sample(), }); export const withSeriesRawEntry = (e: RawEntry) => ({ ...e, - seriesName: random.word(), - seriesYear: date.past().getFullYear(), + seriesName: faker.word.sample(), + seriesYear: faker.date.past().getFullYear(), }); export const withTraktSeriesFields = (e: RawSeriesEntry) => ({ ...e, - [TraktSeriesFields.Genres]: random.words().split(' '), - [TraktSeriesFields.Rating]: random.number(10), - [TraktSeriesFields.Votes]: random.number(100), - [TraktSeriesFields.Description]: lorem.paragraph(), - [TraktSeriesFields.ID]: random.word(), - [TraktSeriesFields.ContentRating]: random.words(), - [TraktSeriesFields.Url]: internet.url(), - [TraktSeriesFields.Network]: random.words(), + [TraktSeriesFields.Genres]: faker.word.words().split(' '), + [TraktSeriesFields.Rating]: faker.number.int({ max: 10 }), + [TraktSeriesFields.Votes]: faker.number.int({ max: 100 }), + [TraktSeriesFields.Description]: faker.lorem.paragraph(), + [TraktSeriesFields.ID]: faker.word.sample(), + [TraktSeriesFields.ContentRating]: faker.word.words(), + [TraktSeriesFields.Url]: faker.internet.url(), + [TraktSeriesFields.Network]: faker.word.words(), }); export const withTVDBSeriesFields = (e: RawSeriesEntry) => ({ ...e, - [TVDBSeriesFields.Genres]: random.words().split(' '), - [TVDBSeriesFields.Posters]: image.imageUrl(), - [TVDBSeriesFields.Rating]: random.number(10), - [TVDBSeriesFields.Description]: lorem.paragraph(), - [TVDBSeriesFields.Url]: internet.url(), - [TVDBSeriesFields.Backdrops]: image.imageUrl(), - [TVDBSeriesFields.ContentRating]: random.words(), - [TVDBSeriesFields.Network]: random.words(), - [TVDBSeriesFields.ID]: random.word(), + [TVDBSeriesFields.Genres]: faker.word.words().split(' '), + [TVDBSeriesFields.Posters]: faker.image.url(), + [TVDBSeriesFields.Rating]: faker.number.int({ max: 10 }), + [TVDBSeriesFields.Description]: faker.lorem.paragraph(), + [TVDBSeriesFields.Url]: faker.internet.url(), + [TVDBSeriesFields.Backdrops]: faker.image.url(), + [TVDBSeriesFields.ContentRating]: faker.word.words(), + [TVDBSeriesFields.Network]: faker.word.words(), + [TVDBSeriesFields.ID]: faker.word.sample(), }); export const withTVMazeSeriesFields = (e: RawSeriesEntry) => ({ ...e, - [TVMazeSeriesFields.Genres]: random.words().split(' '), - [TVMazeSeriesFields.Posters]: image.imageUrl(), - [TVMazeSeriesFields.Rating]: random.number(10), - [TVMazeSeriesFields.Description]: lorem.paragraph(), - [TVMazeSeriesFields.Url]: internet.url(), - [TVMazeSeriesFields.Network]: random.words(), - [TVMazeSeriesFields.ID]: random.word(), + [TVMazeSeriesFields.Genres]: faker.word.words().split(' '), + [TVMazeSeriesFields.Posters]: faker.image.url(), + [TVMazeSeriesFields.Rating]: faker.number.int({ max: 10 }), + [TVMazeSeriesFields.Description]: faker.lorem.paragraph(), + [TVMazeSeriesFields.Url]: faker.internet.url(), + [TVMazeSeriesFields.Network]: faker.word.words(), + [TVMazeSeriesFields.ID]: faker.word.sample(), }); diff --git a/src/core/entry/lookup/movies.ts b/src/core/entry/lookup/movies.ts index 4d2198f4..c09864cd 100644 --- a/src/core/entry/lookup/movies.ts +++ b/src/core/entry/lookup/movies.ts @@ -49,7 +49,7 @@ const tmdbToFields = (movie: TMDBMovie): RawMovieFields => ({ [TMDBFields.Backdrops]: movie.backdrops?.map(({ urls }) => urls.original), [TMDBFields.Rating]: movie.rating, [TMDBFields.Votes]: movie.votes, - [TMDBFields.Url]: movie.homepage, + [TMDBFields.Url]: `https://www.themoviedb.org/movie/${movie.id}`, [TMDBFields.Runtime]: movie.runtime, [TMDBFields.ID]: movie.id, [TMDBFields.Description]: movie.overview, @@ -125,7 +125,8 @@ interface TraktMovie { homepage: string; imdbId: string; tmdbId: string; - name: string; + slug: string; + title: string; year: number; runtime: number; overview: string; @@ -135,24 +136,24 @@ interface TraktMovie { } const traktToFields = (movie: TraktMovie): RawMovieFields => ({ - movieName: movie.name, + movieName: movie.title, movieYear: movie.year, [TraktFields.Genres]: movie.genres, [TraktFields.Rating]: movie.rating, [TraktFields.Votes]: movie.votes, - [TraktFields.Url]: movie.homepage, + [TraktFields.Url]: movie.slug ? `https://trakt.tv/movies/${movie.slug}` : undefined, [TraktFields.Runtime]: movie.runtime, [TraktFields.ID]: movie.id, [TraktFields.Description]: movie.overview, [IMDBFields.ID]: movie.imdbId, - [TraktFields.ID]: movie.tmdbId, + [TMDBFields.ID]: movie.tmdbId, }); export const useTraktLookup = (options: TraktOptions) => { const query = stringify(snakeCase(options)); const [entry, setEntry] = useState(); - const [state, request] = useFlexgetAPI(`/tmdb/movies?${query}`); + const [state, request] = useFlexgetAPI(`/trakt/movies?${query}`); useEffect(() => { const fn = async () => { diff --git a/src/core/entry/types.ts b/src/core/entry/types.ts index dd0bbce9..8ebfe4bb 100644 --- a/src/core/entry/types.ts +++ b/src/core/entry/types.ts @@ -21,7 +21,7 @@ type ArrayPairs>> = { export type Fields< U extends string, T extends ReadonlyArray>, - W extends Partial> + W extends Partial>, > = { readonly [P in Extract[1], string>]?: W[Extract, [any, P]>[0]]; }; diff --git a/src/core/home/PluginCard.tsx b/src/core/home/PluginCard.tsx index d8869714..ff81db43 100644 --- a/src/core/home/PluginCard.tsx +++ b/src/core/home/PluginCard.tsx @@ -1,6 +1,6 @@ -import React, { FC } from 'react'; -import { css } from '@emotion/core'; -import { Card, Avatar, Theme, Typography, CardActions, Button } from '@material-ui/core'; +import { FC } from 'react'; +import { css } from '@emotion/react'; +import { Card, Avatar, Theme, Typography, CardActions, Button } from '@mui/material'; import { Link } from 'common/styles'; import { CardInfo } from './types'; @@ -11,14 +11,14 @@ interface Props { const header = (theme: Theme) => css` display: flex; align-items: center; - padding: ${theme.typography.pxToRem(theme.spacing(2))}; + padding: ${theme.typography.pxToRem(parseFloat(theme.spacing(2)))}; padding-bottom: 0; `; const avatar = (theme: Theme) => css` color: ${theme.palette.primary.contrastText}; background-color: ${theme.palette.primary.main}; - margin-right: ${theme.typography.pxToRem(theme.spacing(1))}; + margin-right: ${theme.typography.pxToRem(parseFloat(theme.spacing(1)))}; `; const button = () => css` diff --git a/src/core/home/hooks.ts b/src/core/home/hooks.ts index ebe69c84..eb5018ff 100644 --- a/src/core/home/hooks.ts +++ b/src/core/home/hooks.ts @@ -7,17 +7,18 @@ export const useGetHomeCards = () => { const { pluginMap } = useContainer(PluginContainer); const cards: CardInfo[] = useMemo( () => - Object.entries(pluginMap).flatMap(([path, { icon, component, cardComponent, displayName }]) => - cardComponent - ? [ - { - path: component && path, - Component: cardComponent, - name: displayName, - Icon: icon, - }, - ] - : [], + Object.entries(pluginMap).flatMap( + ([path, { icon, component, cardComponent, displayName }]) => + cardComponent + ? [ + { + path: component && path, + Component: cardComponent, + name: displayName, + Icon: icon, + }, + ] + : [], ), [pluginMap], ); diff --git a/src/core/home/index.spec.tsx b/src/core/home/index.spec.tsx index 82f14dc9..c7c2852b 100644 --- a/src/core/home/index.spec.tsx +++ b/src/core/home/index.spec.tsx @@ -2,7 +2,7 @@ import React, { FC } from 'react'; import { cleanup } from '@testing-library/react'; import AppBar from 'core/layout/AppBar'; import { renderWithWrapper } from 'utils/tests'; -import { Home, Tv, Movie } from '@material-ui/icons'; +import { Home, Tv, Movie } from '@mui/icons-material'; import { registerPlugin } from 'core/plugins/registry'; import HomePage from './index'; diff --git a/src/core/home/index.tsx b/src/core/home/index.tsx index 4db8bc6c..59b694a5 100644 --- a/src/core/home/index.tsx +++ b/src/core/home/index.tsx @@ -1,6 +1,5 @@ -import React, { Suspense } from 'react'; -import { hot } from 'react-hot-loader/root'; -import { Grid } from '@material-ui/core'; +import { Suspense } from 'react'; +import { Grid } from '@mui/material'; import LoadingSpinner from 'common/LoadingSpinner'; import { useInjectPageTitle } from 'core/layout/AppBar/hooks'; import { useGetHomeCards } from './hooks'; @@ -22,4 +21,4 @@ const Home = () => { ); }; -export default hot(Home); +export default Home; diff --git a/src/core/layout/AppBar/AppBar.spec.tsx b/src/core/layout/AppBar/AppBar.spec.tsx index 110bb643..fa73bd38 100644 --- a/src/core/layout/AppBar/AppBar.spec.tsx +++ b/src/core/layout/AppBar/AppBar.spec.tsx @@ -1,40 +1,29 @@ import React, { FC, useEffect } from 'react'; -import { mount } from 'enzyme'; -import { IconButton } from '@material-ui/core'; -import { Check } from '@material-ui/icons'; -import { BaseProviders } from 'utils/tests'; -import AppBar from './index'; +import { Check } from '@mui/icons-material'; +import { fireEvent, screen } from '@testing-library/react'; +import { renderWithWrapper } from 'utils/tests'; import { useInjectPageTitle, useInjectContent, useContextualAppBar, ContextualProps, } from './hooks'; +import AppBar from './index'; describe('core/layout/AppBar', () => { it('calls toggle when the hamburger button is pressed', () => { const toggle = jest.fn(); - const wrapper = mount( - - - , - ); + renderWithWrapper(); - wrapper - .findWhere(el => el.props()['aria-label'] === 'toggle sidebar' && el.type() === IconButton) - .simulate('click'); + fireEvent.click(screen.getByRole('button', { name: 'toggle sidebar' })); expect(toggle).toHaveBeenCalled(); }); it('renders correct title', () => { - const wrapper = mount( - - - , - ); + renderWithWrapper(); - expect(wrapper.text()).toBe('Flexget Manager'); + expect(screen.getByText('Flexget Manager')).toBeInTheDocument(); }); describe('injectable app bar', () => { @@ -44,31 +33,31 @@ describe('core/layout/AppBar', () => { return null; }; - const wrapper = mount( - + renderWithWrapper( + <> - , + , ); - expect(wrapper.text()).toBe('New Title'); + expect(screen.getByText('New Title')).toBeInTheDocument(); }); - it('will render additonal content', () => { - const content =
Hello
; + it('will render additional content', () => { + const injectedContent =
Hello
; const OverrideContent: FC = () => { - useInjectContent(content); + useInjectContent(injectedContent); return null; }; - const wrapper = mount( - + renderWithWrapper( + <> - , + , ); - expect(wrapper.text()).toInclude('Hello'); + expect(screen.getByText('Hello')).toBeInTheDocument(); }); }); @@ -102,71 +91,55 @@ describe('core/layout/AppBar', () => { describe('enabled', () => { it('calls closeFn when the button is pressed', () => { - const wrapper = mount( - + renderWithWrapper( + <> - , + , ); - wrapper - .findWhere(el => el.props()['aria-label'] === 'close context' && el.type() === IconButton) - .simulate('click'); + fireEvent.click(screen.getByRole('button', { name: 'close context' })); expect(onClose).toHaveBeenCalled(); }); it('renders normal content when no contextualProps are specified', () => { - const wrapper = mount( - + renderWithWrapper( + <> - , + , ); - expect(wrapper.text()).toInclude('Flexget Manager'); - expect( - wrapper - .findWhere(el => el.props()['aria-label'] === 'My Icon' && el.type() === IconButton) - .exists(), - ).toBeFalse(); + expect(screen.getByText('Flexget Manager')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'My Icon' })).not.toBeInTheDocument(); }); }); + describe('not enabled', () => { it('renders normal content when not enabled', () => { - const wrapper = mount( - + renderWithWrapper( + <> - , + , ); - expect(wrapper.text()).toInclude('Flexget Manager'); - expect( - wrapper - .findWhere(el => el.props()['aria-label'] === 'My Icon' && el.type() === IconButton) - .exists(), - ).toBeFalse(); + + expect(screen.getByText('Flexget Manager')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'My Icon' })).not.toBeInTheDocument(); }); }); - it('render contextual content when enabled', () => { - const wrapper = mount( - + it('renders contextual content when enabled', () => { + renderWithWrapper( + <> - , + , ); - expect( - wrapper - .findWhere(el => el.props()['aria-label'] === 'My Icon' && el.type() === IconButton) - .exists(), - ).toBeTrue(); - expect( - wrapper - .findWhere(el => el.props()['aria-label'] === 'config editor' && el.type() === IconButton) - .exists(), - ).toBeFalse(); + expect(screen.getByRole('button', { name: 'My Icon' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'config editor' })).not.toBeInTheDocument(); }); }); }); diff --git a/src/core/layout/AppBar/InfoCard.tsx b/src/core/layout/AppBar/InfoCard.tsx index cf1962ab..947ae1a1 100644 --- a/src/core/layout/AppBar/InfoCard.tsx +++ b/src/core/layout/AppBar/InfoCard.tsx @@ -1,5 +1,5 @@ -import React, { FC } from 'react'; -import { css } from '@emotion/core'; +import { FC } from 'react'; +import { css } from '@emotion/react'; import { IconButton, CardActions, @@ -10,8 +10,8 @@ import { Dialog, DialogTitle, DialogContent, -} from '@material-ui/core'; -import { GitHub, ForumOutlined, Home, Chat } from '@material-ui/icons'; +} from '@mui/material'; +import { GitHub, ForumOutlined, Home, Chat } from '@mui/icons-material'; import YAML from 'yaml'; const bold = css` diff --git a/src/core/layout/AppBar/OverflowMenu.tsx b/src/core/layout/AppBar/OverflowMenu.tsx index 888d3bb1..7e1cc40a 100644 --- a/src/core/layout/AppBar/OverflowMenu.tsx +++ b/src/core/layout/AppBar/OverflowMenu.tsx @@ -10,8 +10,8 @@ import { Tooltip, IconButtonProps, MenuItemProps, -} from '@material-ui/core'; -import { MoreVert } from '@material-ui/icons'; +} from '@mui/material'; +import { MoreVert } from '@mui/icons-material'; export type OverflowMenuProps = Omit< IconButtonProps & diff --git a/src/core/layout/AppBar/index.tsx b/src/core/layout/AppBar/index.tsx index fd599f97..2bf9c6d2 100644 --- a/src/core/layout/AppBar/index.tsx +++ b/src/core/layout/AppBar/index.tsx @@ -1,6 +1,6 @@ -import React, { FC, useCallback, useMemo } from 'react'; +import { FC, useCallback, useMemo } from 'react'; import { useContainer } from 'unstated-next'; -import { css } from '@emotion/core'; +import { css } from '@emotion/react'; import { AppBar as MUIAppBar, Toolbar, @@ -8,17 +8,17 @@ import { Typography, Theme, Tooltip, -} from '@material-ui/core'; +} from '@mui/material'; import { EmojiObjects, EmojiObjectsOutlined, Menu as MenuIcon, Clear, Help, -} from '@material-ui/icons'; +} from '@mui/icons-material'; import { Spacer } from 'common/styles'; import LoadingBar from 'core/status/LoadingBar'; -import { SpeedDialIcon } from '@material-ui/lab'; +import { SpeedDialIcon } from '@mui/material'; import { ThemeContainer } from 'core/theme'; import { useOverlayState } from 'utils/hooks'; import { AppBarContainer } from './hooks'; @@ -70,11 +70,10 @@ const AppBar: FC = ({ toggleSidebar, className }) => { setContextual(false); }, [contextualProps, setContextual]); - const menuClick = useMemo(() => (contextualMode ? handleContextualClose : icon.onClick), [ - contextualMode, - handleContextualClose, - icon.onClick, - ]); + const menuClick = useMemo( + () => (contextualMode ? handleContextualClose : icon.onClick), + [contextualMode, handleContextualClose, icon.onClick], + ); const menuLabel = contextualMode ? 'close context' : icon.label; const [mode, toggleMode] = useContainer(ThemeContainer); diff --git a/src/core/layout/Layout.tsx b/src/core/layout/Layout.tsx index b1ad99da..2582d7e6 100644 --- a/src/core/layout/Layout.tsx +++ b/src/core/layout/Layout.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import ErrorStatus from 'core/status/ErrorStatus'; import InfoStatus from 'core/status/InfoStatus'; -import { useTheme } from '@material-ui/core'; +import { useTheme } from '@mui/material'; import { useOverlayState } from 'utils/hooks'; import AppBar from './AppBar'; @@ -17,7 +17,7 @@ import { } from './styles'; import { AppBarContainer } from './AppBar/hooks'; -const Layout: React.FC = ({ children }) => { +const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => { const theme = useTheme(); const matches = window.matchMedia && !window.matchMedia('(max-width: 600px)').matches; const [sidebarOpen, { close, toggle }] = useOverlayState(matches); @@ -25,10 +25,10 @@ const Layout: React.FC = ({ children }) => { const sideTransition = sidebarOpen ? enterTransition : leavingTransition; const mainTransition = sidebarOpen ? leavingTransition : enterTransition; - const contentCss = useMemo(() => [content(theme), mainTransition(theme)], [ - mainTransition, - theme, - ]); + const contentCss = useMemo( + () => [content(theme), mainTransition(theme)], + [mainTransition, theme], + ); return ( diff --git a/src/core/layout/SideNav/Entry.spec.tsx b/src/core/layout/SideNav/Entry.spec.tsx index 564cb4a6..065a229e 100644 --- a/src/core/layout/SideNav/Entry.spec.tsx +++ b/src/core/layout/SideNav/Entry.spec.tsx @@ -1,7 +1,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { themed, router } from 'utils/tests'; -import SVGIcon from '@material-ui/core/SvgIcon'; +import SVGIcon from '@mui/material/SvgIcon'; import Entry from './Entry'; describe('core/layout/SideNavEntry', () => { diff --git a/src/core/layout/SideNav/Entry.tsx b/src/core/layout/SideNav/Entry.tsx index 492eac63..62f921eb 100644 --- a/src/core/layout/SideNav/Entry.tsx +++ b/src/core/layout/SideNav/Entry.tsx @@ -1,7 +1,7 @@ -import React, { ComponentType, FC } from 'react'; -import { css } from '@emotion/core'; +import { ComponentType, FC } from 'react'; +import { css } from '@emotion/react'; import { Link } from 'react-router-dom'; -import { ListItem, ListItemText, ListItemIcon, Theme } from '@material-ui/core'; +import { ListItem, ListItemText, ListItemIcon, Theme } from '@mui/material'; interface Props { onClick?: () => void; diff --git a/src/core/layout/SideNav/Logo.tsx b/src/core/layout/SideNav/Logo.tsx index f7815c15..59671fc0 100644 --- a/src/core/layout/SideNav/Logo.tsx +++ b/src/core/layout/SideNav/Logo.tsx @@ -1,6 +1,6 @@ -import React, { FC } from 'react'; -import { css } from '@emotion/core'; -import { Theme } from '@material-ui/core'; +import { FC } from 'react'; +import { css } from '@emotion/react'; +import { Theme } from '@mui/material'; import headerImage from 'images/header.png'; import { Link } from 'common/styles'; diff --git a/src/core/layout/SideNav/Menu.tsx b/src/core/layout/SideNav/Menu.tsx index 9776bed4..6dd7799d 100644 --- a/src/core/layout/SideNav/Menu.tsx +++ b/src/core/layout/SideNav/Menu.tsx @@ -1,7 +1,7 @@ -import React, { FC, useCallback } from 'react'; +import { FC, useCallback } from 'react'; import { useContainer } from 'unstated-next'; -import { Menu, MenuItem, ListItemIcon } from '@material-ui/core'; -import { Sync, Storage, ExitToApp, PowerSettingsNew } from '@material-ui/icons'; +import { Menu, MenuItem, ListItemIcon } from '@mui/material'; +import { Sync, Storage, ExitToApp, PowerSettingsNew } from '@mui/icons-material'; import { AuthContainer } from 'core/auth/hooks'; import { useFlexgetAPI } from 'core/api'; diff --git a/src/core/layout/SideNav/Version.tsx b/src/core/layout/SideNav/Version.tsx index e69e0734..66083c90 100644 --- a/src/core/layout/SideNav/Version.tsx +++ b/src/core/layout/SideNav/Version.tsx @@ -1,9 +1,9 @@ -import React, { FC } from 'react'; +import { FC } from 'react'; import { gt } from 'semver'; -import { css } from '@emotion/core'; -import { IconButton, Theme } from '@material-ui/core'; -import HelpOutlineIcon from '@material-ui/icons/HelpOutline'; -import Typography from '@material-ui/core/Typography'; +import { css } from '@emotion/react'; +import { IconButton, Theme } from '@mui/material'; +import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; +import Typography from '@mui/material/Typography'; import { cleanFlexgetVersion } from 'utils/version'; import { useVersion } from './hooks'; @@ -14,7 +14,7 @@ interface Props { const wrapper = (theme: Theme) => css` color: ${theme.palette.primary.main}; opacity: 1; - padding: ${theme.typography.pxToRem(theme.spacing(2))}; + padding: ${theme.typography.pxToRem(parseFloat(theme.spacing(2)))}; `; const Version: FC = ({ className }) => { diff --git a/src/core/layout/SideNav/__snapshots__/Entry.spec.tsx.snap b/src/core/layout/SideNav/__snapshots__/Entry.spec.tsx.snap index 992b1193..fdc32257 100644 --- a/src/core/layout/SideNav/__snapshots__/Entry.spec.tsx.snap +++ b/src/core/layout/SideNav/__snapshots__/Entry.spec.tsx.snap @@ -1,30 +1,114 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`core/layout/SideNavEntry renders correctly with link 1`] = ` +.emotion-0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -ms-flex-pack: start; + -webkit-justify-content: flex-start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + -webkit-text-decoration: none; + text-decoration: none; + width: 100%; + box-sizing: border-box; + text-align: left; + padding-top: 8px; + padding-bottom: 8px; + padding-left: 16px; + padding-right: 16px; + color: #b0bec5; + border-left: 3px solid transparent; + cursor: pointer; +} + +.emotion-0.Mui-focusVisible { + background-color: rgba(0, 0, 0, 0.12); +} + +.emotion-0.Mui-selected { + background-color: rgba(239, 108, 0, 0.08); +} + +.emotion-0.Mui-selected.Mui-focusVisible { + background-color: rgba(239, 108, 0, 0.2); +} + +.emotion-0.Mui-disabled { + opacity: 0.38; +} + +.emotion-0:hover { + border-left: 3px solid #ef6c00; +} + +.emotion-1 { + min-width: 56px; + color: rgba(55,71,79,0.54); + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + color: #b0bec5; +} + +.emotion-2 { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: 1em; + height: 1em; + display: inline-block; + fill: currentColor; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + -webkit-transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + font-size: 2.4rem; +} + +.emotion-3 { + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + min-width: 0; + margin-top: 4px; + margin-bottom: 4px; + color: #b0bec5; +} +
  • Test
    @@ -33,26 +117,110 @@ exports[`core/layout/SideNavEntry renders correctly with link 1`] = ` `; exports[`core/layout/SideNavEntry renders correctly without link 1`] = ` +.emotion-0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -ms-flex-pack: start; + -webkit-justify-content: flex-start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + -webkit-text-decoration: none; + text-decoration: none; + width: 100%; + box-sizing: border-box; + text-align: left; + padding-top: 8px; + padding-bottom: 8px; + padding-left: 16px; + padding-right: 16px; + color: #b0bec5; + border-left: 3px solid transparent; + cursor: pointer; +} + +.emotion-0.Mui-focusVisible { + background-color: rgba(0, 0, 0, 0.12); +} + +.emotion-0.Mui-selected { + background-color: rgba(239, 108, 0, 0.08); +} + +.emotion-0.Mui-selected.Mui-focusVisible { + background-color: rgba(239, 108, 0, 0.2); +} + +.emotion-0.Mui-disabled { + opacity: 0.38; +} + +.emotion-0:hover { + border-left: 3px solid #ef6c00; +} + +.emotion-1 { + min-width: 56px; + color: rgba(55,71,79,0.54); + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + color: #b0bec5; +} + +.emotion-2 { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: 1em; + height: 1em; + display: inline-block; + fill: currentColor; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + -webkit-transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + font-size: 2.4rem; +} + +.emotion-3 { + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + min-width: 0; + margin-top: 4px; + margin-bottom: 4px; + color: #b0bec5; +} +
  • Test
    diff --git a/src/core/layout/SideNav/index.tsx b/src/core/layout/SideNav/index.tsx index 45e7037f..f7e7c9f5 100644 --- a/src/core/layout/SideNav/index.tsx +++ b/src/core/layout/SideNav/index.tsx @@ -1,5 +1,5 @@ import React, { FC, useCallback, useMemo, useState } from 'react'; -import { css } from '@emotion/core'; +import { css } from '@emotion/react'; import { Drawer, List, @@ -8,10 +8,10 @@ import { useMediaQuery, useTheme, Divider, -} from '@material-ui/core'; -import { Settings } from '@material-ui/icons'; +} from '@mui/material'; +import { Settings } from '@mui/icons-material'; -import { useHistory } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import { Route } from 'core/routes/types'; import { useGetRoutes } from 'core/routes/hooks'; @@ -37,25 +37,26 @@ interface Props { const SideNav: FC = ({ sidebarOpen = false, onClose, className }) => { const { routes } = useGetRoutes(); - const history = useHistory(); + const navigate = useNavigate(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('xs')); - const drawerCss = useMemo(() => (sidebarOpen ? drawerOpen(theme) : drawerClose(theme)), [ - sidebarOpen, - theme, - ]); + const drawerCss = useMemo( + () => (sidebarOpen ? drawerOpen(theme) : drawerClose(theme)), + [sidebarOpen, theme], + ); const drawerRootCss = useMemo(() => [drawer(theme), drawerCss], [drawerCss, theme]); const handleClick = useCallback( - ({ path }: Route) => () => { - if (isMobile) { - onClose(); - } - history.push(path); - }, - [history, isMobile, onClose], + ({ path }: Route) => + () => { + if (isMobile) { + onClose(); + } + navigate(path); + }, + [navigate, isMobile, onClose], ); const [anchorEl, setAnchorEl] = useState(); diff --git a/src/core/layout/SideNav/styles.ts b/src/core/layout/SideNav/styles.ts index bc024d46..93070fb6 100644 --- a/src/core/layout/SideNav/styles.ts +++ b/src/core/layout/SideNav/styles.ts @@ -1,5 +1,5 @@ -import { Theme } from '@material-ui/core'; -import { css } from '@emotion/core'; +import { Theme } from '@mui/material'; +import { css } from '@emotion/react'; import { rgba } from 'polished'; export const nested = (theme: Theme) => css` diff --git a/src/core/layout/__snapshots__/Layout.spec.tsx.snap b/src/core/layout/__snapshots__/Layout.spec.tsx.snap index b217a194..4ba39ac7 100644 --- a/src/core/layout/__snapshots__/Layout.spec.tsx.snap +++ b/src/core/layout/__snapshots__/Layout.spec.tsx.snap @@ -2,108 +2,412 @@ exports[`common/layout renders correctly 1`] = ` .emotion-0 { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; -} - -
    -