1
Fork 0

Merge pull request #715 from photoview/ui-gql-fragments

Refactor UI: Add graphql fragments + eslint
This commit is contained in:
Viktor Strate Kløvedal 2022-07-13 18:08:31 +02:00 committed by GitHub
commit a17a680457
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 1104 additions and 469 deletions

3
ui/.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
coverage

View File

@ -1,3 +1,5 @@
/* global __dirname */
module.exports = { module.exports = {
root: true, root: true,
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
@ -11,6 +13,7 @@ module.exports = {
'plugin:react/recommended', 'plugin:react/recommended',
'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'prettier', 'prettier',
], ],
globals: { globals: {
@ -21,6 +24,8 @@ module.exports = {
require: 'readonly', require: 'readonly',
}, },
parserOptions: { parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
ecmaFeatures: { ecmaFeatures: {
jsx: true, jsx: true,
}, },
@ -41,30 +46,13 @@ module.exports = {
version: 'detect', version: 'detect',
}, },
}, },
// parser: 'babel-eslint',
overrides: [ overrides: [
// Object.assign(require('eslint-plugin-jest').configs.recommended, {
// files: ['**/*.test.js', '**/*.test.ts', '**/*.test.tsx'],
// env: { jest: true },
// plugins: ['jest', 'jest-dom'],
// rules: Object.assign(
// require('eslint-plugin-jest').configs.recommended.rules,
// {
// 'no-import-assign': 'off',
// 'react/prop-types': 'off',
// 'jest/valid-title': 'off',
// }
// ),
// settings: {
// jest: {
// version: 26,
// },
// },
// }),
{ {
files: ['**/*.js'], files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
rules: { rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-misused-promises': 'off',
}, },
}, },
], ],

507
ui/package-lock.json generated
View File

@ -53,6 +53,8 @@
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0", "@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.1", "@testing-library/user-event": "^14.2.1",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"@vitest/ui": "^0.17.1", "@vitest/ui": "^0.17.1",
"apollo": "2.34.0", "apollo": "2.34.0",
"apollo-language-server": "1.26.9", "apollo-language-server": "1.26.9",
@ -5326,13 +5328,13 @@
"integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA=="
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.30.5", "version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz",
"integrity": "sha512-lftkqRoBvc28VFXEoRgyZuztyVUQ04JvUnATSPtIRFAccbXTWL6DEtXGYMcbg998kXw1NLUJm7rTQ9eUt+q6Ig==", "integrity": "sha512-J4zYMIhgrx4MgnZrSDD7sEnQp7FmhKNOaqaOpaoQ/SfdMfRB/0yvK74hTnvH+VQxndZynqs5/Hn4t+2/j9bADg==",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "5.30.5", "@typescript-eslint/scope-manager": "5.30.6",
"@typescript-eslint/type-utils": "5.30.5", "@typescript-eslint/type-utils": "5.30.6",
"@typescript-eslint/utils": "5.30.5", "@typescript-eslint/utils": "5.30.6",
"debug": "^4.3.4", "debug": "^4.3.4",
"functional-red-black-tree": "^1.0.1", "functional-red-black-tree": "^1.0.1",
"ignore": "^5.2.0", "ignore": "^5.2.0",
@ -5357,6 +5359,99 @@
} }
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz",
"integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==",
"dependencies": {
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/visitor-keys": "5.30.6"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz",
"integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg==",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz",
"integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==",
"dependencies": {
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/visitor-keys": "5.30.6",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"semver": "^7.3.7",
"tsutils": "^3.21.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.6.tgz",
"integrity": "sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA==",
"dependencies": {
"@types/json-schema": "^7.0.9",
"@typescript-eslint/scope-manager": "5.30.6",
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/typescript-estree": "5.30.6",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz",
"integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==",
"dependencies": {
"@typescript-eslint/types": "5.30.6",
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
"version": "7.3.7", "version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
@ -5390,13 +5485,13 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "5.30.5", "version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.6.tgz",
"integrity": "sha512-zj251pcPXI8GO9NDKWWmygP6+UjwWmrdf9qMW/L/uQJBM/0XbU2inxe5io/234y/RCvwpKEYjZ6c1YrXERkK4Q==", "integrity": "sha512-gfF9lZjT0p2ZSdxO70Xbw8w9sPPJGfAdjK7WikEjB3fcUI/yr9maUVEdqigBjKincUYNKOmf7QBMiTf719kbrA==",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "5.30.5", "@typescript-eslint/scope-manager": "5.30.6",
"@typescript-eslint/types": "5.30.5", "@typescript-eslint/types": "5.30.6",
"@typescript-eslint/typescript-estree": "5.30.5", "@typescript-eslint/typescript-estree": "5.30.6",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@ -5415,6 +5510,90 @@
} }
} }
}, },
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz",
"integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==",
"dependencies": {
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/visitor-keys": "5.30.6"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz",
"integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg==",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz",
"integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==",
"dependencies": {
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/visitor-keys": "5.30.6",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"semver": "^7.3.7",
"tsutils": "^3.21.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz",
"integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==",
"dependencies": {
"@typescript-eslint/types": "5.30.6",
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "5.30.5", "version": "5.30.5",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.5.tgz",
@ -5432,11 +5611,11 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "5.30.5", "version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.30.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.30.6.tgz",
"integrity": "sha512-k9+ejlv1GgwN1nN7XjVtyCgE0BTzhzT1YsQF0rv4Vfj2U9xnslBgMYYvcEYAFVdvhuEscELJsB7lDkN7WusErw==", "integrity": "sha512-GFVVzs2j0QPpM+NTDMXtNmJKlF842lkZKDSanIxf+ArJsGeZUIaeT4jGg+gAgHt7AcQSFwW7htzF/rbAh2jaVA==",
"dependencies": { "dependencies": {
"@typescript-eslint/utils": "5.30.5", "@typescript-eslint/utils": "5.30.6",
"debug": "^4.3.4", "debug": "^4.3.4",
"tsutils": "^3.21.0" "tsutils": "^3.21.0"
}, },
@ -5456,6 +5635,113 @@
} }
} }
}, },
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz",
"integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==",
"dependencies": {
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/visitor-keys": "5.30.6"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz",
"integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg==",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz",
"integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==",
"dependencies": {
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/visitor-keys": "5.30.6",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"semver": "^7.3.7",
"tsutils": "^3.21.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.6.tgz",
"integrity": "sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA==",
"dependencies": {
"@types/json-schema": "^7.0.9",
"@typescript-eslint/scope-manager": "5.30.6",
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/typescript-estree": "5.30.6",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz",
"integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==",
"dependencies": {
"@typescript-eslint/types": "5.30.6",
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "5.30.5", "version": "5.30.5",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.5.tgz",
@ -28832,13 +29118,13 @@
"integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA=="
}, },
"@typescript-eslint/eslint-plugin": { "@typescript-eslint/eslint-plugin": {
"version": "5.30.5", "version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz",
"integrity": "sha512-lftkqRoBvc28VFXEoRgyZuztyVUQ04JvUnATSPtIRFAccbXTWL6DEtXGYMcbg998kXw1NLUJm7rTQ9eUt+q6Ig==", "integrity": "sha512-J4zYMIhgrx4MgnZrSDD7sEnQp7FmhKNOaqaOpaoQ/SfdMfRB/0yvK74hTnvH+VQxndZynqs5/Hn4t+2/j9bADg==",
"requires": { "requires": {
"@typescript-eslint/scope-manager": "5.30.5", "@typescript-eslint/scope-manager": "5.30.6",
"@typescript-eslint/type-utils": "5.30.5", "@typescript-eslint/type-utils": "5.30.6",
"@typescript-eslint/utils": "5.30.5", "@typescript-eslint/utils": "5.30.6",
"debug": "^4.3.4", "debug": "^4.3.4",
"functional-red-black-tree": "^1.0.1", "functional-red-black-tree": "^1.0.1",
"ignore": "^5.2.0", "ignore": "^5.2.0",
@ -28847,6 +29133,56 @@
"tsutils": "^3.21.0" "tsutils": "^3.21.0"
}, },
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz",
"integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==",
"requires": {
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/visitor-keys": "5.30.6"
}
},
"@typescript-eslint/types": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz",
"integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg=="
},
"@typescript-eslint/typescript-estree": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz",
"integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==",
"requires": {
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/visitor-keys": "5.30.6",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"semver": "^7.3.7",
"tsutils": "^3.21.0"
}
},
"@typescript-eslint/utils": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.6.tgz",
"integrity": "sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA==",
"requires": {
"@types/json-schema": "^7.0.9",
"@typescript-eslint/scope-manager": "5.30.6",
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/typescript-estree": "5.30.6",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0"
}
},
"@typescript-eslint/visitor-keys": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz",
"integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==",
"requires": {
"@typescript-eslint/types": "5.30.6",
"eslint-visitor-keys": "^3.3.0"
}
},
"semver": { "semver": {
"version": "7.3.7", "version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
@ -28866,14 +29202,61 @@
} }
}, },
"@typescript-eslint/parser": { "@typescript-eslint/parser": {
"version": "5.30.5", "version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.6.tgz",
"integrity": "sha512-zj251pcPXI8GO9NDKWWmygP6+UjwWmrdf9qMW/L/uQJBM/0XbU2inxe5io/234y/RCvwpKEYjZ6c1YrXERkK4Q==", "integrity": "sha512-gfF9lZjT0p2ZSdxO70Xbw8w9sPPJGfAdjK7WikEjB3fcUI/yr9maUVEdqigBjKincUYNKOmf7QBMiTf719kbrA==",
"requires": { "requires": {
"@typescript-eslint/scope-manager": "5.30.5", "@typescript-eslint/scope-manager": "5.30.6",
"@typescript-eslint/types": "5.30.5", "@typescript-eslint/types": "5.30.6",
"@typescript-eslint/typescript-estree": "5.30.5", "@typescript-eslint/typescript-estree": "5.30.6",
"debug": "^4.3.4" "debug": "^4.3.4"
},
"dependencies": {
"@typescript-eslint/scope-manager": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz",
"integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==",
"requires": {
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/visitor-keys": "5.30.6"
}
},
"@typescript-eslint/types": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz",
"integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg=="
},
"@typescript-eslint/typescript-estree": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz",
"integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==",
"requires": {
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/visitor-keys": "5.30.6",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"semver": "^7.3.7",
"tsutils": "^3.21.0"
}
},
"@typescript-eslint/visitor-keys": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz",
"integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==",
"requires": {
"@typescript-eslint/types": "5.30.6",
"eslint-visitor-keys": "^3.3.0"
}
},
"semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"requires": {
"lru-cache": "^6.0.0"
}
}
} }
}, },
"@typescript-eslint/scope-manager": { "@typescript-eslint/scope-manager": {
@ -28886,13 +29269,73 @@
} }
}, },
"@typescript-eslint/type-utils": { "@typescript-eslint/type-utils": {
"version": "5.30.5", "version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.30.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.30.6.tgz",
"integrity": "sha512-k9+ejlv1GgwN1nN7XjVtyCgE0BTzhzT1YsQF0rv4Vfj2U9xnslBgMYYvcEYAFVdvhuEscELJsB7lDkN7WusErw==", "integrity": "sha512-GFVVzs2j0QPpM+NTDMXtNmJKlF842lkZKDSanIxf+ArJsGeZUIaeT4jGg+gAgHt7AcQSFwW7htzF/rbAh2jaVA==",
"requires": { "requires": {
"@typescript-eslint/utils": "5.30.5", "@typescript-eslint/utils": "5.30.6",
"debug": "^4.3.4", "debug": "^4.3.4",
"tsutils": "^3.21.0" "tsutils": "^3.21.0"
},
"dependencies": {
"@typescript-eslint/scope-manager": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz",
"integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==",
"requires": {
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/visitor-keys": "5.30.6"
}
},
"@typescript-eslint/types": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz",
"integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg=="
},
"@typescript-eslint/typescript-estree": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz",
"integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==",
"requires": {
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/visitor-keys": "5.30.6",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"semver": "^7.3.7",
"tsutils": "^3.21.0"
}
},
"@typescript-eslint/utils": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.6.tgz",
"integrity": "sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA==",
"requires": {
"@types/json-schema": "^7.0.9",
"@typescript-eslint/scope-manager": "5.30.6",
"@typescript-eslint/types": "5.30.6",
"@typescript-eslint/typescript-estree": "5.30.6",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0"
}
},
"@typescript-eslint/visitor-keys": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz",
"integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==",
"requires": {
"@typescript-eslint/types": "5.30.6",
"eslint-visitor-keys": "^3.3.0"
}
},
"semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"requires": {
"lru-cache": "^6.0.0"
}
}
} }
}, },
"@typescript-eslint/types": { "@typescript-eslint/types": {

View File

@ -11,10 +11,10 @@
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"build": "vite build", "build": "vite build",
"lint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js", "lint": "eslint ./src --max-warnings 0 --config .eslintrc.js",
"test": "vitest", "test": "vitest",
"test:ci": "CI=true vitest --reporter verbose --run --coverage", "test:ci": "CI=true vitest --reporter verbose --run --coverage",
"genSchemaTypes": "apollo client:codegen --target=typescript --globalTypesFile=src/__generated__/globalTypes.ts && prettier --write */**/__generated__/*.ts", "genSchemaTypes": "apollo client:codegen --target=typescript --globalTypesFile=src/__generated__/globalTypes.ts --passthroughCustomScalars && prettier --write */**/__generated__/*.ts",
"extractTranslations": "i18next -c i18next-parser.config.js", "extractTranslations": "i18next -c i18next-parser.config.js",
"prepare": "(cd .. && ./ui/node_modules/.bin/husky install)" "prepare": "(cd .. && ./ui/node_modules/.bin/husky install)"
}, },
@ -63,6 +63,8 @@
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0", "@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.1", "@testing-library/user-event": "^14.2.1",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"@vitest/ui": "^0.17.1", "@vitest/ui": "^0.17.1",
"apollo": "2.34.0", "apollo": "2.34.0",
"apollo-language-server": "1.26.9", "apollo-language-server": "1.26.9",

View File

@ -5,3 +5,16 @@ declare module '*.svg' {
export { ReactComponent } export { ReactComponent }
// export default content // export default content
} }
interface ImportMetaEnv {
readonly REACT_APP_BUILD_VERSION: string | undefined
readonly REACT_APP_BUILD_DATE: string | undefined
readonly REACT_APP_BUILD_COMMIT_SHA: string | undefined
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
type Time = string
type Any = object

View File

@ -1,6 +1,8 @@
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { useQuery, gql } from '@apollo/client' import { useQuery, gql } from '@apollo/client'
import AlbumGallery from '../../components/albumGallery/AlbumGallery' import AlbumGallery, {
ALBUM_GALLERY_FRAGMENT,
} from '../../components/albumGallery/AlbumGallery'
import Layout from '../../components/layout/Layout' import Layout from '../../components/layout/Layout'
import useURLParameters from '../../hooks/useURLParameters' import useURLParameters from '../../hooks/useURLParameters'
import useScrollPagination from '../../hooks/useScrollPagination' import useScrollPagination from '../../hooks/useScrollPagination'
@ -12,6 +14,8 @@ import { useParams } from 'react-router-dom'
import { isNil } from '../../helpers/utils' import { isNil } from '../../helpers/utils'
const ALBUM_QUERY = gql` const ALBUM_QUERY = gql`
${ALBUM_GALLERY_FRAGMENT}
query albumQuery( query albumQuery(
$id: ID! $id: ID!
$onlyFavorites: Boolean $onlyFavorites: Boolean
@ -21,41 +25,7 @@ const ALBUM_QUERY = gql`
$offset: Int $offset: Int
) { ) {
album(id: $id) { album(id: $id) {
id ...AlbumGalleryFields
title
subAlbums(
order: { order_by: "title", order_direction: $orderDirection }
) {
id
title
thumbnail {
id
thumbnail {
url
}
}
}
media(
paginate: { limit: $limit, offset: $offset }
order: { order_by: $mediaOrderBy, order_direction: $orderDirection }
onlyFavorites: $onlyFavorites
) {
id
type
blurhash
thumbnail {
url
width
height
}
highRes {
url
}
videoWeb {
url
}
favorite
}
} }
} }
` `

View File

@ -8,35 +8,35 @@
// ==================================================== // ====================================================
export interface getMyAlbums_myAlbums_thumbnail_thumbnail { export interface getMyAlbums_myAlbums_thumbnail_thumbnail {
__typename: "MediaURL"; __typename: 'MediaURL'
/** /**
* URL for previewing the image * URL for previewing the image
*/ */
url: string; url: string
} }
export interface getMyAlbums_myAlbums_thumbnail { export interface getMyAlbums_myAlbums_thumbnail {
__typename: "Media"; __typename: 'Media'
id: string; id: string
/** /**
* URL to display the media in a smaller resolution * URL to display the media in a smaller resolution
*/ */
thumbnail: getMyAlbums_myAlbums_thumbnail_thumbnail | null; thumbnail: getMyAlbums_myAlbums_thumbnail_thumbnail | null
} }
export interface getMyAlbums_myAlbums { export interface getMyAlbums_myAlbums {
__typename: "Album"; __typename: 'Album'
id: string; id: string
title: string; title: string
/** /**
* An image in this album used for previewing this album * An image in this album used for previewing this album
*/ */
thumbnail: getMyAlbums_myAlbums_thumbnail | null; thumbnail: getMyAlbums_myAlbums_thumbnail | null
} }
export interface getMyAlbums { export interface getMyAlbums {
/** /**
* List of albums owned by the logged in user. * List of albums owned by the logged in user.
*/ */
myAlbums: getMyAlbums_myAlbums[]; myAlbums: getMyAlbums_myAlbums[]
} }

View File

@ -9,10 +9,10 @@ import { mockInitialSetupGraphql } from './loginTestHelpers'
vi.mock('../../helpers/authentication.ts') vi.mock('../../helpers/authentication.ts')
const authToken = authentication.authToken // as vi.Mock<ReturnType<typeof authentication.authToken>> const authToken = vi.mocked(authentication.authToken)
describe('Initial setup page', () => { describe('Initial setup page', () => {
test('Render initial setup form', async () => { test('Render initial setup form', () => {
authToken.mockImplementation(() => null) authToken.mockImplementation(() => null)
const history = createMemoryHistory({ const history = createMemoryHistory({

View File

@ -10,6 +10,10 @@ import { CheckInitialSetup } from './__generated__/CheckInitialSetup'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { Submit, TextField } from '../../primitives/form/Input' import { Submit, TextField } from '../../primitives/form/Input'
import MessageBox from '../../primitives/form/MessageBox' import MessageBox from '../../primitives/form/MessageBox'
import {
InitialSetup,
InitialSetupVariables,
} from './__generated__/InitialSetup'
const initialSetupMutation = gql` const initialSetupMutation = gql`
mutation InitialSetup( mutation InitialSetup(
@ -59,13 +63,12 @@ const InitialSetupPage = () => {
}, [notInitialSetup]) }, [notInitialSetup])
const [authorize, { loading: authorizeLoading, data: authorizationData }] = const [authorize, { loading: authorizeLoading, data: authorizationData }] =
useMutation(initialSetupMutation, { useMutation<InitialSetup, InitialSetupVariables>(initialSetupMutation, {
onCompleted: data => { onCompleted: data => {
const { success, token } = data.initialSetupWizard if (!data.initialSetupWizard) return
if (success) { const { success, token } = data.initialSetupWizard
login(token) if (success && token) login(token)
}
}, },
}) })
@ -84,8 +87,8 @@ const InitialSetupPage = () => {
} }
let errorMessage = null let errorMessage = null
if (authorizationData && !authorizationData.initialSetupWizard.success) { if (authorizationData && !authorizationData?.initialSetupWizard?.success) {
errorMessage = authorizationData.initialSetupWizard.status errorMessage = authorizationData?.initialSetupWizard?.status
} }
return ( return (
@ -138,7 +141,7 @@ const InitialSetupPage = () => {
<MessageBox <MessageBox
type="negative" type="negative"
message={errorMessage} message={errorMessage}
show={errorMessage} show={!!errorMessage}
/> />
<Submit className="mt-2" disabled={authorizeLoading}> <Submit className="mt-2" disabled={authorizeLoading}>
{t('login_page.initial_setup.field.submit', 'Setup Photoview')} {t('login_page.initial_setup.field.submit', 'Setup Photoview')}

View File

@ -9,7 +9,7 @@ import { mockInitialSetupGraphql } from './loginTestHelpers'
vi.mock('../../helpers/authentication.ts') vi.mock('../../helpers/authentication.ts')
const authToken = authentication.authToken // as vi.Mock<ReturnType<typeof authentication.authToken>> const authToken = vi.mocked(authentication.authToken)
describe('Login page redirects', () => { describe('Login page redirects', () => {
test('Auth token redirect', async () => { test('Auth token redirect', async () => {
@ -54,7 +54,7 @@ describe('Login page redirects', () => {
}) })
describe('Login page', () => { describe('Login page', () => {
test('Render login form', async () => { test('Render login form', () => {
authToken.mockImplementation(() => null) authToken.mockImplementation(() => null)
const history = createMemoryHistory({ const history = createMemoryHistory({

View File

@ -106,7 +106,7 @@ const LoginForm = () => {
<input <input
type="submit" type="submit"
disabled={loading} disabled={loading}
value={t('login_page.field.submit', 'Sign in') as string} value={t('login_page.field.submit', 'Sign in')}
className="rounded-md px-8 py-2 mt-2 focus:outline-none cursor-pointer bg-gradient-to-bl from-[#FF8246] to-[#D6264D] text-white font-semibold focus:ring-2 focus:ring-red-200 disabled:cursor-default disabled:opacity-80" className="rounded-md px-8 py-2 mt-2 focus:outline-none cursor-pointer bg-gradient-to-bl from-[#FF8246] to-[#D6264D] text-white font-semibold focus:ring-2 focus:ring-red-200 disabled:cursor-default disabled:opacity-80"
/> />
<MessageBox <MessageBox

View File

@ -8,13 +8,13 @@
// ==================================================== // ====================================================
export interface CheckInitialSetup_siteInfo { export interface CheckInitialSetup_siteInfo {
__typename: "SiteInfo"; __typename: 'SiteInfo'
/** /**
* Whether or not the initial setup wizard should be shown * Whether or not the initial setup wizard should be shown
*/ */
initialSetup: boolean; initialSetup: boolean
} }
export interface CheckInitialSetup { export interface CheckInitialSetup {
siteInfo: CheckInitialSetup_siteInfo; siteInfo: CheckInitialSetup_siteInfo
} }

View File

@ -1,15 +1,26 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { ProtectedImage } from '../../components/photoGallery/ProtectedMedia' import {
ProtectedImage,
ProtectedImageProps,
} from '../../components/photoGallery/ProtectedMedia'
import { import {
myFaces_myFaceGroups_imageFaces_media, myFaces_myFaceGroups_imageFaces_media,
myFaces_myFaceGroups_imageFaces_rectangle, myFaces_myFaceGroups_imageFaces_rectangle,
} from './__generated__/myFaces' } from './__generated__/myFaces'
// eslint-disable-next-line @typescript-eslint/no-unused-vars type FaceImageProps = ProtectedImageProps & {
const FaceImage = styled(({ origin, selectable, scale, ...rest }) => ( origin: { x: number; y: number }
<ProtectedImage {...rest} /> selectable: boolean
))<{ origin: { x: number; y: number }; selectable: boolean; scale: number }>` scale: number
}
const FaceImage = styled(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
({ origin, selectable, scale, ...rest }: FaceImageProps) => (
<ProtectedImage {...rest} />
)
)`
position: absolute; position: absolute;
transform-origin: ${({ origin }) => `${origin.x * 100}% ${origin.y * 100}%`}; transform-origin: ${({ origin }) => `${origin.x * 100}% ${origin.y * 100}%`};
object-fit: cover; object-fit: cover;

View File

@ -218,7 +218,7 @@ describe('FaceDetails component', () => {
}) })
}) })
test('cancel add label to face group', async () => { test('cancel add label to face group', () => {
render( render(
<MockedProvider mocks={[]} addTypename={false}> <MockedProvider mocks={[]} addTypename={false}>
<MemoryRouter> <MemoryRouter>

View File

@ -2,8 +2,8 @@ import { gql, useQuery } from '@apollo/client'
import React, { useEffect, useReducer } from 'react' import React, { useEffect, useReducer } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import PaginateLoader from '../../../components/PaginateLoader' import PaginateLoader from '../../../components/PaginateLoader'
import PhotoGallery from '../../../components/photoGallery/PhotoGallery' import MediaGallery from '../../../components/photoGallery/MediaGallery'
import { photoGalleryReducer } from '../../../components/photoGallery/photoGalleryReducer' import { mediaGalleryReducer } from '../../../components/photoGallery/mediaGalleryReducer'
import useScrollPagination from '../../../hooks/useScrollPagination' import useScrollPagination from '../../../hooks/useScrollPagination'
import FaceGroupTitle from './FaceGroupTitle' import FaceGroupTitle from './FaceGroupTitle'
import { import {
@ -62,7 +62,7 @@ const SingleFaceGroup = ({ faceGroupID }: SingleFaceGroupProps) => {
}, },
}) })
const [mediaState, dispatchMedia] = useReducer(photoGalleryReducer, { const [mediaState, dispatchMedia] = useReducer(mediaGalleryReducer, {
presenting: false, presenting: false,
activeIndex: -1, activeIndex: -1,
media: [], media: [],
@ -91,7 +91,7 @@ const SingleFaceGroup = ({ faceGroupID }: SingleFaceGroupProps) => {
<div ref={containerElem}> <div ref={containerElem}>
<FaceGroupTitle faceGroup={faceGroup} /> <FaceGroupTitle faceGroup={faceGroup} />
<div> <div>
<PhotoGallery <MediaGallery
loading={loading} loading={loading}
dispatchMedia={dispatchMedia} dispatchMedia={dispatchMedia}
mediaState={mediaState} mediaState={mediaState}

View File

@ -8,18 +8,18 @@
// ==================================================== // ====================================================
export interface combineFaces_combineFaceGroups { export interface combineFaces_combineFaceGroups {
__typename: "FaceGroup"; __typename: 'FaceGroup'
id: string; id: string
} }
export interface combineFaces { export interface combineFaces {
/** /**
* Merge two face groups into a single one, all ImageFaces from source will be moved to destination * Merge two face groups into a single one, all ImageFaces from source will be moved to destination
*/ */
combineFaceGroups: combineFaces_combineFaceGroups; combineFaceGroups: combineFaces_combineFaceGroups
} }
export interface combineFacesVariables { export interface combineFacesVariables {
destID: string; destID: string
srcID: string; srcID: string
} }

View File

@ -8,24 +8,24 @@
// ==================================================== // ====================================================
export interface moveImageFaces_moveImageFaces_imageFaces { export interface moveImageFaces_moveImageFaces_imageFaces {
__typename: "ImageFace"; __typename: 'ImageFace'
id: string; id: string
} }
export interface moveImageFaces_moveImageFaces { export interface moveImageFaces_moveImageFaces {
__typename: "FaceGroup"; __typename: 'FaceGroup'
id: string; id: string
imageFaces: moveImageFaces_moveImageFaces_imageFaces[]; imageFaces: moveImageFaces_moveImageFaces_imageFaces[]
} }
export interface moveImageFaces { export interface moveImageFaces {
/** /**
* Move a list of ImageFaces to another face group * Move a list of ImageFaces to another face group
*/ */
moveImageFaces: moveImageFaces_moveImageFaces; moveImageFaces: moveImageFaces_moveImageFaces
} }
export interface moveImageFacesVariables { export interface moveImageFacesVariables {
faceIDs: string[]; faceIDs: string[]
destFaceGroupID: string; destFaceGroupID: string
} }

View File

@ -8,13 +8,13 @@
// ==================================================== // ====================================================
export interface recognizeUnlabeledFaces_recognizeUnlabeledFaces { export interface recognizeUnlabeledFaces_recognizeUnlabeledFaces {
__typename: "ImageFace"; __typename: 'ImageFace'
id: string; id: string
} }
export interface recognizeUnlabeledFaces { export interface recognizeUnlabeledFaces {
/** /**
* Check all unlabeled faces to see if they match a labeled FaceGroup, and move them if they match * Check all unlabeled faces to see if they match a labeled FaceGroup, and move them if they match
*/ */
recognizeUnlabeledFaces: recognizeUnlabeledFaces_recognizeUnlabeledFaces[]; recognizeUnlabeledFaces: recognizeUnlabeledFaces_recognizeUnlabeledFaces[]
} }

View File

@ -49,7 +49,7 @@ const MapClusterMarker = ({
marker, marker,
dispatchMarkerMedia, dispatchMarkerMedia,
}: MapClusterMarkerProps) => { }: MapClusterMarkerProps) => {
const thumbnail = JSON.parse(marker.thumbnail) const thumbnail = JSON.parse(marker.thumbnail) as { url: string }
const presentMedia = () => { const presentMedia = () => {
dispatchMarkerMedia({ dispatchMarkerMedia({

View File

@ -7,7 +7,7 @@ import styled from 'styled-components'
import Layout from '../../components/layout/Layout' import Layout from '../../components/layout/Layout'
import { registerMediaMarkers } from '../../components/mapbox/mapboxHelperFunctions' import { registerMediaMarkers } from '../../components/mapbox/mapboxHelperFunctions'
import useMapboxMap from '../../components/mapbox/MapboxMap' import useMapboxMap from '../../components/mapbox/MapboxMap'
import { urlPresentModeSetupHook } from '../../components/photoGallery/photoGalleryReducer' import { urlPresentModeSetupHook } from '../../components/photoGallery/mediaGalleryReducer'
import MapPresentMarker from './MapPresentMarker' import MapPresentMarker from './MapPresentMarker'
import { PlacesAction, placesReducer } from './placesReducer' import { PlacesAction, placesReducer } from './placesReducer'
import { mediaGeoJson } from './__generated__/mediaGeoJson' import { mediaGeoJson } from './__generated__/mediaGeoJson'
@ -108,7 +108,7 @@ const configureMapbox =
map.addSource('media', { map.addSource('media', {
type: 'geojson', type: 'geojson',
data: mapboxData?.myMediaGeoJson, data: mapboxData?.myMediaGeoJson as never,
cluster: true, cluster: true,
clusterRadius: 50, clusterRadius: 50,
clusterProperties: { clusterProperties: {

View File

@ -11,5 +11,5 @@ export interface mediaGeoJson {
/** /**
* Get media owned by the logged in user, returned in GeoJson format * Get media owned by the logged in user, returned in GeoJson format
*/ */
myMediaGeoJson: any myMediaGeoJson: Any
} }

View File

@ -1,11 +1,11 @@
import { PresentMarker } from './PlacesPage' import { PresentMarker } from './PlacesPage'
import { import {
PhotoGalleryState, MediaGalleryState,
PhotoGalleryAction, PhotoGalleryAction,
photoGalleryReducer, mediaGalleryReducer,
} from './../../components/photoGallery/photoGalleryReducer' } from '../../components/photoGallery/mediaGalleryReducer'
export interface PlacesState extends PhotoGalleryState { export interface PlacesState extends MediaGalleryState {
presentMarker?: PresentMarker presentMarker?: PresentMarker
} }
@ -36,6 +36,6 @@ export function placesReducer(
} }
} }
default: default:
return photoGalleryReducer(state, action) return mediaGalleryReducer(state, action)
} }
} }

View File

@ -9,7 +9,7 @@ import {
ScannerConcurrentWorkers, ScannerConcurrentWorkers,
} from './ScannerConcurrentWorkers' } from './ScannerConcurrentWorkers'
test('load ScannerConcurrentWorkers', async () => { test('load ScannerConcurrentWorkers', () => {
const graphqlMocks = [ const graphqlMocks = [
{ {
request: { request: {

View File

@ -4,6 +4,11 @@ import { useTranslation } from 'react-i18next'
import Checkbox from '../../../primitives/form/Checkbox' import Checkbox from '../../../primitives/form/Checkbox'
import { TextField, Button, ButtonGroup } from '../../../primitives/form/Input' import { TextField, Button, ButtonGroup } from '../../../primitives/form/Input'
import { TableRow, TableCell } from '../../../primitives/Table' import { TableRow, TableCell } from '../../../primitives/Table'
import { createUser, createUserVariables } from './__generated__/createUser'
import {
userAddRootPath,
userAddRootPathVariables,
} from './__generated__/userAddRootPath'
export const CREATE_USER_MUTATION = gql` export const CREATE_USER_MUTATION = gql`
mutation createUser($username: String!, $admin: Boolean!) { mutation createUser($username: String!, $admin: Boolean!) {
@ -46,35 +51,35 @@ const AddUserRow = ({ setShow, show, onUserAdded }: AddUserRowProps) => {
onUserAdded() onUserAdded()
} }
const [addRootPath, { loading: addRootPathLoading }] = useMutation( const [addRootPath, { loading: addRootPathLoading }] = useMutation<
USER_ADD_ROOT_PATH_MUTATION, userAddRootPath,
{ userAddRootPathVariables
onCompleted: () => { >(USER_ADD_ROOT_PATH_MUTATION, {
finished() onCompleted: () => {
}, finished()
onError: () => { },
finished() onError: () => {
}, finished()
} },
) })
const [createUser, { loading: createUserLoading }] = useMutation( const [createUser, { loading: createUserLoading }] = useMutation<
CREATE_USER_MUTATION, createUser,
{ createUserVariables
onCompleted: ({ createUser: { id } }) => { >(CREATE_USER_MUTATION, {
if (state.rootPath) { onCompleted: ({ createUser: { id } }) => {
addRootPath({ if (state.rootPath) {
variables: { addRootPath({
id: id, variables: {
rootPath: state.rootPath, id: id,
}, rootPath: state.rootPath,
}) },
} else { })
finished() } else {
} finished()
}, }
} },
) })
const loading = addRootPathLoading || createUserLoading const loading = addRootPathLoading || createUserLoading

View File

@ -67,7 +67,7 @@ export type UserRowChildProps = {
export type UserRowProps = { export type UserRowProps = {
user: settingsUsersQuery_user user: settingsUsersQuery_user
refetchUsers(): void refetchUsers: () => void
} }
const UserRow = ({ user, refetchUsers }: UserRowProps) => { const UserRow = ({ user, refetchUsers }: UserRowProps) => {

View File

@ -8,17 +8,17 @@
// ==================================================== // ====================================================
export interface scanUser_scanUser { export interface scanUser_scanUser {
__typename: "ScannerResult"; __typename: 'ScannerResult'
success: boolean; success: boolean
} }
export interface scanUser { export interface scanUser {
/** /**
* Scan a single user for new media * Scan a single user for new media
*/ */
scanUser: scanUser_scanUser; scanUser: scanUser_scanUser
} }
export interface scanUserVariables { export interface scanUserVariables {
userId: string; userId: string
} }

View File

@ -10,9 +10,7 @@ import {
const VERSION = import.meta.env.REACT_APP_BUILD_VERSION ?? 'undefined' const VERSION = import.meta.env.REACT_APP_BUILD_VERSION ?? 'undefined'
const BUILD_DATE = import.meta.env.REACT_APP_BUILD_DATE ?? 'undefined' const BUILD_DATE = import.meta.env.REACT_APP_BUILD_DATE ?? 'undefined'
const COMMIT_SHA = import.meta.env.REACT_APP_BUILD_COMMIT_SHA as const COMMIT_SHA = import.meta.env.REACT_APP_BUILD_COMMIT_SHA
| string
| undefined
let commitLink: ReactElement let commitLink: ReactElement
if (COMMIT_SHA) { if (COMMIT_SHA) {

View File

@ -12,9 +12,9 @@ export interface changeScanIntervalMutation {
* Set how often, in seconds, the server should automatically scan for new media, * Set how often, in seconds, the server should automatically scan for new media,
* a value of 0 will disable periodic scans * a value of 0 will disable periodic scans
*/ */
setPeriodicScanInterval: number; setPeriodicScanInterval: number
} }
export interface changeScanIntervalMutationVariables { export interface changeScanIntervalMutationVariables {
interval: number; interval: number
} }

View File

@ -8,13 +8,13 @@
// ==================================================== // ====================================================
export interface concurrentWorkersQuery_siteInfo { export interface concurrentWorkersQuery_siteInfo {
__typename: "SiteInfo"; __typename: 'SiteInfo'
/** /**
* How many max concurrent scanner jobs that should run at once * How many max concurrent scanner jobs that should run at once
*/ */
concurrentWorkers: number; concurrentWorkers: number
} }
export interface concurrentWorkersQuery { export interface concurrentWorkersQuery {
siteInfo: concurrentWorkersQuery_siteInfo; siteInfo: concurrentWorkersQuery_siteInfo
} }

View File

@ -8,14 +8,14 @@
// ==================================================== // ====================================================
export interface scanAllMutation_scanAll { export interface scanAllMutation_scanAll {
__typename: "ScannerResult"; __typename: 'ScannerResult'
success: boolean; success: boolean
message: string | null; message: string | null
} }
export interface scanAllMutation { export interface scanAllMutation {
/** /**
* Scan all users for new media * Scan all users for new media
*/ */
scanAll: scanAllMutation_scanAll; scanAll: scanAllMutation_scanAll
} }

View File

@ -8,13 +8,13 @@
// ==================================================== // ====================================================
export interface scanIntervalQuery_siteInfo { export interface scanIntervalQuery_siteInfo {
__typename: "SiteInfo"; __typename: 'SiteInfo'
/** /**
* How often automatic scans should be initiated in seconds * How often automatic scans should be initiated in seconds
*/ */
periodicScanInterval: number; periodicScanInterval: number
} }
export interface scanIntervalQuery { export interface scanIntervalQuery {
siteInfo: scanIntervalQuery_siteInfo; siteInfo: scanIntervalQuery_siteInfo
} }

View File

@ -11,9 +11,9 @@ export interface setConcurrentWorkers {
/** /**
* Set max number of concurrent scanner jobs running at once * Set max number of concurrent scanner jobs running at once
*/ */
setScannerConcurrentWorkers: number; setScannerConcurrentWorkers: number
} }
export interface setConcurrentWorkersVariables { export interface setConcurrentWorkersVariables {
workers: number; workers: number
} }

View File

@ -25,7 +25,7 @@ const PasswordProtectedShare = ({
const [invalidPassword, setInvalidPassword] = useState(false) const [invalidPassword, setInvalidPassword] = useState(false)
const onSubmit = () => { const onSubmit = () => {
refetchWithPassword(watch('password')) refetchWithPassword(watch('password') as string)
setInvalidPassword(true) setInvalidPassword(true)
} }

View File

@ -11,6 +11,14 @@ import MediaSharePage from './MediaSharePage'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import PasswordProtectedShare from './PasswordProtectedShare' import PasswordProtectedShare from './PasswordProtectedShare'
import { isNil } from '../../helpers/utils' import { isNil } from '../../helpers/utils'
import {
SharePageToken,
SharePageTokenVariables,
} from './__generated__/SharePageToken'
import {
ShareTokenValidatePassword,
ShareTokenValidatePasswordVariables,
} from './__generated__/ShareTokenValidatePassword'
export const SHARE_TOKEN_QUERY = gql` export const SHARE_TOKEN_QUERY = gql`
query SharePageToken($token: String!, $password: String) { query SharePageToken($token: String!, $password: String) {
@ -90,17 +98,20 @@ const AuthorizedTokenRoute = () => {
const token = tokenFromParams() const token = tokenFromParams()
const password = getSharePassword(token) const password = getSharePassword(token)
const { loading, error, data } = useQuery(SHARE_TOKEN_QUERY, { const { loading, error, data } = useQuery<
SharePageToken,
SharePageTokenVariables
>(SHARE_TOKEN_QUERY, {
variables: { variables: {
token, token,
password, password,
}, },
}) })
if (error) return <div>{error.message}</div> if (!isNil(error)) return <div>{error.message}</div>
if (loading) return <div>{t('general.loading.default', 'Loading...')}</div> if (loading) return <div>{t('general.loading.default', 'Loading...')}</div>
if (data.shareToken.album) { if (data?.shareToken.album) {
const SharedSubAlbumPage = () => { const SharedSubAlbumPage = () => {
const { subAlbum } = useParams() const { subAlbum } = useParams()
if (isNil(subAlbum)) if (isNil(subAlbum))
@ -128,7 +139,7 @@ const AuthorizedTokenRoute = () => {
) )
} }
if (data.shareToken.media) { if (data?.shareToken.media) {
return <MediaSharePage media={data.shareToken.media} /> return <MediaSharePage media={data.shareToken.media} />
} }
@ -144,16 +155,16 @@ export const TokenRoute = () => {
const { t } = useTranslation() const { t } = useTranslation()
const token = tokenFromParams() const token = tokenFromParams()
const { loading, error, data, refetch } = useQuery( const { loading, error, data, refetch } = useQuery<
VALIDATE_TOKEN_PASSWORD_QUERY, ShareTokenValidatePassword,
{ ShareTokenValidatePasswordVariables
notifyOnNetworkStatusChange: true, >(VALIDATE_TOKEN_PASSWORD_QUERY, {
variables: { notifyOnNetworkStatusChange: true,
token: token, variables: {
password: getSharePassword(token), token: token,
}, password: getSharePassword(token),
} },
) })
if (error) { if (error) {
if (error.message == 'GraphQL error: share not found') { if (error.message == 'GraphQL error: share not found') {

View File

@ -122,7 +122,7 @@ export interface SharePageToken_shareToken_media_exif {
* The name of the lens * The name of the lens
*/ */
lens: string | null lens: string | null
dateShot: any | null dateShot: Time | null
/** /**
* The exposure time of the image * The exposure time of the image
*/ */

View File

@ -136,7 +136,7 @@ export interface shareAlbumQuery_album_media_exif {
* The name of the lens * The name of the lens
*/ */
lens: string | null lens: string | null
dateShot: any | null dateShot: Time | null
/** /**
* The exposure time of the image * The exposure time of the image
*/ */

View File

@ -56,19 +56,24 @@ const link = split(
const linkError = onError(({ graphQLErrors, networkError }) => { const linkError = onError(({ graphQLErrors, networkError }) => {
const errorMessages = [] const errorMessages = []
const formatPath = (path: readonly (string | number)[] | undefined) =>
path?.join('::') ?? 'undefined'
if (graphQLErrors) { if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) => graphQLErrors.map(({ message, locations, path }) =>
console.log( console.log(
`[GraphQL error]: Message: ${message}, Location: ${JSON.stringify( `[GraphQL error]: Message: ${message}, Location: ${JSON.stringify(
locations locations
)} Path: ${path}` )} Path: ${formatPath(path)}`
) )
) )
if (graphQLErrors.length == 1) { if (graphQLErrors.length == 1) {
errorMessages.push({ errorMessages.push({
header: 'Something went wrong', header: 'Something went wrong',
content: `Server error: ${graphQLErrors[0].message} at (${graphQLErrors[0].path})`, content: `Server error: ${graphQLErrors[0].message} at (${formatPath(
graphQLErrors[0].path
)})`,
}) })
} else if (graphQLErrors.length > 1) { } else if (graphQLErrors.length > 1) {
errorMessages.push({ errorMessages.push({
@ -88,7 +93,8 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
console.log(`[Network error]: ${JSON.stringify(networkError)}`) console.log(`[Network error]: ${JSON.stringify(networkError)}`)
clearTokenCookie() clearTokenCookie()
const errors = (networkError as ServerError)?.result.errors || [] const errors =
((networkError as ServerError)?.result.errors as Error[]) || []
if (errors.length == 1) { if (errors.length == 1) {
errorMessages.push({ errorMessages.push({
@ -120,7 +126,7 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
type PaginateCacheType = { type PaginateCacheType = {
keyArgs: string[] keyArgs: string[]
merge: FieldMergeFunction merge: FieldMergeFunction<unknown[], unknown[]>
} }
// Modified version of Apollo's offsetLimitPagination() // Modified version of Apollo's offsetLimitPagination()
@ -130,7 +136,7 @@ const paginateCache = (keyArgs: string[]) =>
merge(existing, incoming, { args, fieldName }) { merge(existing, incoming, { args, fieldName }) {
const merged = existing ? existing.slice(0) : [] const merged = existing ? existing.slice(0) : []
if (args?.paginate) { if (args?.paginate) {
const { offset = 0 } = args.paginate const { offset = 0 } = args.paginate as { offset: number }
for (let i = 0; i < incoming.length; ++i) { for (let i = 0; i < incoming.length; ++i) {
merged[offset + i] = incoming[i] merged[offset + i] = incoming[i]
} }

View File

@ -1,49 +1,46 @@
import React, { useEffect, useReducer } from 'react' import React, { useEffect, useReducer } from 'react'
import AlbumTitle from '../album/AlbumTitle' import AlbumTitle from '../album/AlbumTitle'
import PhotoGallery from '../photoGallery/PhotoGallery' import MediaGallery, {
MEDIA_GALLERY_FRAGMENT,
} from '../photoGallery/MediaGallery'
import AlbumBoxes from './AlbumBoxes' import AlbumBoxes from './AlbumBoxes'
import AlbumFilter from '../album/AlbumFilter' import AlbumFilter from '../album/AlbumFilter'
import { import {
albumQuery_album_media_highRes, mediaGalleryReducer,
albumQuery_album_media_thumbnail,
albumQuery_album_media_videoWeb,
albumQuery_album_subAlbums,
} from '../../Pages/AlbumPage/__generated__/albumQuery'
import {
photoGalleryReducer,
urlPresentModeSetupHook, urlPresentModeSetupHook,
} from '../photoGallery/photoGalleryReducer' } from '../photoGallery/mediaGalleryReducer'
import { MediaOrdering, SetOrderingFn } from '../../hooks/useOrderingParams' import { MediaOrdering, SetOrderingFn } from '../../hooks/useOrderingParams'
import { MediaType } from '../../__generated__/globalTypes' import { gql } from '@apollo/client'
import { AlbumGalleryFields } from './__generated__/AlbumGalleryFields'
type AlbumGalleryAlbum = { export const ALBUM_GALLERY_FRAGMENT = gql`
__typename: 'Album' ${MEDIA_GALLERY_FRAGMENT}
id: string
title: string fragment AlbumGalleryFields on Album {
subAlbums: albumQuery_album_subAlbums[] id
media: { title
__typename: 'Media' subAlbums(order: { order_by: "title", order_direction: $orderDirection }) {
id: string id
type: MediaType title
/** thumbnail {
* URL to display the media in a smaller resolution id
*/ thumbnail {
thumbnail: albumQuery_album_media_thumbnail | null url
/** }
* URL to display the photo in full resolution, will be null for videos }
*/ }
highRes: albumQuery_album_media_highRes | null media(
/** paginate: { limit: $limit, offset: $offset }
* URL to get the video in a web format that can be played in the browser, will be null for photos order: { order_by: $mediaOrderBy, order_direction: $orderDirection }
*/ onlyFavorites: $onlyFavorites
videoWeb: albumQuery_album_media_videoWeb | null ) {
favorite?: boolean ...MediaGalleryFields
blurhash: string | null }
}[] }
} `
type AlbumGalleryProps = { type AlbumGalleryProps = {
album?: AlbumGalleryAlbum album?: AlbumGalleryFields
loading?: boolean loading?: boolean
customAlbumLink?(albumID: string): string customAlbumLink?(albumID: string): string
showFilter?: boolean showFilter?: boolean
@ -68,7 +65,7 @@ const AlbumGallery = React.forwardRef(
}: AlbumGalleryProps, }: AlbumGalleryProps,
ref: React.ForwardedRef<HTMLDivElement> ref: React.ForwardedRef<HTMLDivElement>
) => { ) => {
const [mediaState, dispatchMedia] = useReducer(photoGalleryReducer, { const [mediaState, dispatchMedia] = useReducer(mediaGalleryReducer, {
presenting: false, presenting: false,
activeIndex: -1, activeIndex: -1,
media: album?.media || [], media: album?.media || [],
@ -114,7 +111,7 @@ const AlbumGallery = React.forwardRef(
)} )}
<AlbumTitle album={album} disableLink /> <AlbumTitle album={album} disableLink />
{subAlbumElement} {subAlbumElement}
<PhotoGallery <MediaGallery
loading={loading} loading={loading}
mediaState={mediaState} mediaState={mediaState}
dispatchMedia={dispatchMedia} dispatchMedia={dispatchMedia}

View File

@ -0,0 +1,106 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { MediaType } from './../../../__generated__/globalTypes'
// ====================================================
// GraphQL fragment: AlbumGalleryFields
// ====================================================
export interface AlbumGalleryFields_subAlbums_thumbnail_thumbnail {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
}
export interface AlbumGalleryFields_subAlbums_thumbnail {
__typename: 'Media'
id: string
/**
* URL to display the media in a smaller resolution
*/
thumbnail: AlbumGalleryFields_subAlbums_thumbnail_thumbnail | null
}
export interface AlbumGalleryFields_subAlbums {
__typename: 'Album'
id: string
title: string
/**
* An image in this album used for previewing this album
*/
thumbnail: AlbumGalleryFields_subAlbums_thumbnail | null
}
export interface AlbumGalleryFields_media_thumbnail {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
/**
* Width of the image in pixels
*/
width: number
/**
* Height of the image in pixels
*/
height: number
}
export interface AlbumGalleryFields_media_highRes {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
}
export interface AlbumGalleryFields_media_videoWeb {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
}
export interface AlbumGalleryFields_media {
__typename: 'Media'
id: string
type: MediaType
/**
* A short string that can be used to generate a blured version of the media, to show while the original is loading
*/
blurhash: string | null
/**
* URL to display the media in a smaller resolution
*/
thumbnail: AlbumGalleryFields_media_thumbnail | null
/**
* URL to display the photo in full resolution, will be null for videos
*/
highRes: AlbumGalleryFields_media_highRes | null
/**
* URL to get the video in a web format that can be played in the browser, will be null for photos
*/
videoWeb: AlbumGalleryFields_media_videoWeb | null
favorite: boolean
}
export interface AlbumGalleryFields {
__typename: 'Album'
id: string
title: string
/**
* The albums contained in this album
*/
subAlbums: AlbumGalleryFields_subAlbums[]
/**
* The media inside this album
*/
media: AlbumGalleryFields_media[]
}

View File

@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'
import React from 'react' import React from 'react'
import Layout from './Layout' import Layout from './Layout'
test('Layout component', async () => { test('Layout component', () => {
render( render(
<Layout title="Test title"> <Layout title="Test title">
<p>layout_content</p> <p>layout_content</p>

View File

@ -9,7 +9,7 @@ import MainMenu, { MAPBOX_QUERY } from './MainMenu'
vi.mock('../../helpers/authentication.ts') vi.mock('../../helpers/authentication.ts')
const authTokenMock = authentication.authToken // as vi.MockedFunction<typeof authentication.authToken> const authTokenMock = vi.mocked(authentication.authToken)
afterEach(() => { afterEach(() => {
authTokenMock.mockClear() authTokenMock.mockClear()

View File

@ -5,6 +5,7 @@ import { authToken } from '../../helpers/authentication'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { mapboxEnabledQuery } from '../../__generated__/mapboxEnabledQuery' import { mapboxEnabledQuery } from '../../__generated__/mapboxEnabledQuery'
import { tailwindClassNames } from '../../helpers/utils' import { tailwindClassNames } from '../../helpers/utils'
import { faceDetectionEnabled } from './__generated__/faceDetectionEnabled'
export const MAPBOX_QUERY = gql` export const MAPBOX_QUERY = gql`
query mapboxEnabledQuery { query mapboxEnabledQuery {
@ -27,7 +28,7 @@ type MenuButtonProps = {
background: string background: string
activeClasses?: string activeClasses?: string
className?: string className?: string
icon?: React.ReactChild icon?: React.ReactNode
} }
const MenuButton = ({ const MenuButton = ({
@ -48,7 +49,7 @@ const MenuButton = ({
`rounded-lg my-2 outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-dark-bg`, `rounded-lg my-2 outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-dark-bg`,
className, className,
{ {
[`ring-4 lg:ring-4 ${activeClasses}`]: isActive, [`ring-4 lg:ring-4 ${activeClasses ?? ''}`]: isActive,
} }
) )
} }
@ -77,7 +78,7 @@ export const MainMenu = () => {
? useQuery<mapboxEnabledQuery>(MAPBOX_QUERY) ? useQuery<mapboxEnabledQuery>(MAPBOX_QUERY)
: null : null
const faceDetectionEnabledQuery = authToken() const faceDetectionEnabledQuery = authToken()
? useQuery(FACE_DETECTION_ENABLED_QUERY) ? useQuery<faceDetectionEnabled>(FACE_DETECTION_ENABLED_QUERY)
: null : null
const mapboxEnabled = !!mapboxQuery?.data?.mapboxToken const mapboxEnabled = !!mapboxQuery?.data?.mapboxToken

View File

@ -8,13 +8,13 @@
// ==================================================== // ====================================================
export interface faceDetectionEnabled_siteInfo { export interface faceDetectionEnabled_siteInfo {
__typename: "SiteInfo"; __typename: 'SiteInfo'
/** /**
* Whether or not face detection is enabled and working * Whether or not face detection is enabled and working
*/ */
faceDetectionEnabled: boolean; faceDetectionEnabled: boolean
} }
export interface faceDetectionEnabled { export interface faceDetectionEnabled {
siteInfo: faceDetectionEnabled_siteInfo; siteInfo: faceDetectionEnabled_siteInfo
} }

View File

@ -11,5 +11,5 @@ export interface mapboxEnabledQuery {
/** /**
* Get the mapbox api token, returns null if mapbox is not enabled * Get the mapbox api token, returns null if mapbox is not enabled
*/ */
mapboxToken: string | null; mapboxToken: string | null
} }

View File

@ -15,5 +15,5 @@ export interface mapboxToken {
/** /**
* Get media owned by the logged in user, returned in GeoJson format * Get media owned by the logged in user, returned in GeoJson format
*/ */
myMediaGeoJson: any myMediaGeoJson: Any
} }

View File

@ -19,7 +19,7 @@ const NOTIFICATION_SUBSCRIPTION = gql`
} }
` `
const messageTimeoutHandles = new Map() const messageTimeoutHandles = new Map<string, number>()
export interface Message { export interface Message {
key: string key: string
@ -101,7 +101,7 @@ const SubscriptionsHook = ({
const timeoutHandle = setTimeout(() => { const timeoutHandle = setTimeout(() => {
setMessages(messages => messages.filter(m => m.key != msg.key)) setMessages(messages => messages.filter(m => m.key != msg.key))
}, msg.timeout) }, msg.timeout) as unknown as number
messageTimeoutHandles.set(msg.key, timeoutHandle) messageTimeoutHandles.set(msg.key, timeoutHandle)
} }

View File

@ -2,8 +2,8 @@ import { render, screen } from '@testing-library/react'
import React from 'react' import React from 'react'
import { MediaType } from '../../__generated__/globalTypes' import { MediaType } from '../../__generated__/globalTypes'
import PhotoGallery from './PhotoGallery' import MediaGallery from './MediaGallery'
import { PhotoGalleryState } from './photoGalleryReducer' import { MediaGalleryState } from './mediaGalleryReducer'
vi.mock('./photoGalleryMutations', () => ({ vi.mock('./photoGalleryMutations', () => ({
useMarkFavoriteMutation: () => [vi.fn()], useMarkFavoriteMutation: () => [vi.fn()],
@ -12,7 +12,7 @@ vi.mock('./photoGalleryMutations', () => ({
test('photo gallery with media', () => { test('photo gallery with media', () => {
const dispatchMedia = vi.fn() const dispatchMedia = vi.fn()
const mediaState: PhotoGalleryState = { const mediaState: MediaGalleryState = {
activeIndex: 0, activeIndex: 0,
media: [ media: [
{ {
@ -55,7 +55,7 @@ test('photo gallery with media', () => {
} }
render( render(
<PhotoGallery <MediaGallery
dispatchMedia={dispatchMedia} dispatchMedia={dispatchMedia}
mediaState={mediaState} mediaState={mediaState}
loading={false} loading={false}
@ -71,14 +71,14 @@ describe('photo gallery presenting', () => {
const dispatchMedia = vi.fn() const dispatchMedia = vi.fn()
test('not presenting', () => { test('not presenting', () => {
const mediaStateNoPresent: PhotoGalleryState = { const mediaStateNoPresent: MediaGalleryState = {
activeIndex: -1, activeIndex: -1,
media: [], media: [],
presenting: false, presenting: false,
} }
render( render(
<PhotoGallery <MediaGallery
dispatchMedia={dispatchMedia} dispatchMedia={dispatchMedia}
loading={false} loading={false}
mediaState={mediaStateNoPresent} mediaState={mediaStateNoPresent}
@ -89,7 +89,7 @@ describe('photo gallery presenting', () => {
}) })
test('presenting', () => { test('presenting', () => {
const mediaStatePresent: PhotoGalleryState = { const mediaStatePresent: MediaGalleryState = {
activeIndex: 0, activeIndex: 0,
media: [ media: [
{ {
@ -112,7 +112,7 @@ describe('photo gallery presenting', () => {
} }
render( render(
<PhotoGallery <MediaGallery
dispatchMedia={dispatchMedia} dispatchMedia={dispatchMedia}
loading={false} loading={false}
mediaState={mediaStatePresent} mediaState={mediaStatePresent}

View File

@ -2,19 +2,18 @@ import React, { useContext } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { MediaThumbnail, MediaPlaceholder } from './MediaThumbnail' import { MediaThumbnail, MediaPlaceholder } from './MediaThumbnail'
import PresentView from './presentView/PresentView' import PresentView from './presentView/PresentView'
import { PresentMediaProps_Media } from './presentView/PresentMedia'
import { import {
openPresentModeAction, openPresentModeAction,
PhotoGalleryAction, PhotoGalleryAction,
PhotoGalleryState, MediaGalleryState,
} from './photoGalleryReducer' } from './mediaGalleryReducer'
import { import {
toggleFavoriteAction, toggleFavoriteAction,
useMarkFavoriteMutation, useMarkFavoriteMutation,
} from './photoGalleryMutations' } from './photoGalleryMutations'
import MediaSidebar from '../sidebar/MediaSidebar/MediaSidebar' import MediaSidebar from '../sidebar/MediaSidebar/MediaSidebar'
import { SidebarContext } from '../sidebar/Sidebar' import { SidebarContext } from '../sidebar/Sidebar'
import { sidebarMediaQuery_media_thumbnail } from '../sidebar/MediaSidebar/__generated__/sidebarMediaQuery' import { gql } from '@apollo/client'
const Gallery = styled.div` const Gallery = styled.div`
display: flex; display: flex;
@ -35,28 +34,42 @@ export const PhotoFiller = styled.div`
flex-grow: 999999; flex-grow: 999999;
` `
export interface PhotoGalleryProps_Media extends PresentMediaProps_Media { export const MEDIA_GALLERY_FRAGMENT = gql`
thumbnail: sidebarMediaQuery_media_thumbnail | null fragment MediaGalleryFields on Media {
blurhash: string | null id
favorite?: boolean type
} blurhash
thumbnail {
url
width
height
}
highRes {
url
}
videoWeb {
url
}
favorite
}
`
type PhotoGalleryProps = { type MediaGalleryProps = {
loading: boolean loading: boolean
mediaState: PhotoGalleryState mediaState: MediaGalleryState
dispatchMedia: React.Dispatch<PhotoGalleryAction> dispatchMedia: React.Dispatch<PhotoGalleryAction>
} }
const PhotoGallery = ({ mediaState, dispatchMedia }: PhotoGalleryProps) => { const MediaGallery = ({ mediaState, dispatchMedia }: MediaGalleryProps) => {
const [markFavorite] = useMarkFavoriteMutation() const [markFavorite] = useMarkFavoriteMutation()
const { media, activeIndex, presenting } = mediaState const { media, activeIndex, presenting } = mediaState
const { updateSidebar } = useContext(SidebarContext) const { updateSidebar } = useContext(SidebarContext)
let photoElements = [] let mediaElements = []
if (media) { if (media) {
photoElements = media.map((media, index) => { mediaElements = media.map((media, index) => {
const active = activeIndex == index const active = activeIndex == index
return ( return (
@ -85,14 +98,14 @@ const PhotoGallery = ({ mediaState, dispatchMedia }: PhotoGalleryProps) => {
}) })
} else { } else {
for (let i = 0; i < 6; i++) { for (let i = 0; i < 6; i++) {
photoElements.push(<MediaPlaceholder key={i} />) mediaElements.push(<MediaPlaceholder key={i} />)
} }
} }
return ( return (
<> <>
<Gallery data-testid="photo-gallery-wrapper"> <Gallery data-testid="photo-gallery-wrapper">
{photoElements} {mediaElements}
<PhotoFiller /> <PhotoFiller />
</Gallery> </Gallery>
{presenting && ( {presenting && (
@ -105,4 +118,4 @@ const PhotoGallery = ({ mediaState, dispatchMedia }: PhotoGalleryProps) => {
) )
} }
export default PhotoGallery export default MediaGallery

View File

@ -3,6 +3,7 @@ import styled from 'styled-components'
import { ProtectedImage } from './ProtectedMedia' import { ProtectedImage } from './ProtectedMedia'
import { MediaType } from '../../__generated__/globalTypes' import { MediaType } from '../../__generated__/globalTypes'
import { ReactComponent as VideoThumbnailIconSVG } from './icons/videoThumbnailIcon.svg' import { ReactComponent as VideoThumbnailIconSVG } from './icons/videoThumbnailIcon.svg'
import { MediaGalleryFields } from './__generated__/MediaGalleryFields'
const MediaContainer = styled.div` const MediaContainer = styled.div`
flex-grow: 1; flex-grow: 1;
@ -136,17 +137,7 @@ const VideoThumbnailIcon = styled(VideoThumbnailIconSVG)`
` `
type MediaThumbnailProps = { type MediaThumbnailProps = {
media: { media: MediaGalleryFields
id: string
type: MediaType
blurhash: string | null
favorite?: boolean
thumbnail: null | {
url: string
width: number
height: number
}
}
active: boolean active: boolean
selectImage(): void selectImage(): void
clickPresent(): void clickPresent(): void

View File

@ -0,0 +1,65 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { MediaType } from './../../../__generated__/globalTypes'
// ====================================================
// GraphQL fragment: MediaGalleryFields
// ====================================================
export interface MediaGalleryFields_thumbnail {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
/**
* Width of the image in pixels
*/
width: number
/**
* Height of the image in pixels
*/
height: number
}
export interface MediaGalleryFields_highRes {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
}
export interface MediaGalleryFields_videoWeb {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
}
export interface MediaGalleryFields {
__typename: 'Media'
id: string
type: MediaType
/**
* A short string that can be used to generate a blured version of the media, to show while the original is loading
*/
blurhash: string | null
/**
* URL to display the media in a smaller resolution
*/
thumbnail: MediaGalleryFields_thumbnail | null
/**
* URL to display the photo in full resolution, will be null for videos
*/
highRes: MediaGalleryFields_highRes | null
/**
* URL to get the video in a web format that can be played in the browser, will be null for photos
*/
videoWeb: MediaGalleryFields_videoWeb | null
favorite: boolean
}

View File

@ -8,19 +8,19 @@
// ==================================================== // ====================================================
export interface markMediaFavorite_favoriteMedia { export interface markMediaFavorite_favoriteMedia {
__typename: "Media"; __typename: 'Media'
id: string; id: string
favorite: boolean; favorite: boolean
} }
export interface markMediaFavorite { export interface markMediaFavorite {
/** /**
* Mark or unmark a media as being a favorite * Mark or unmark a media as being a favorite
*/ */
favoriteMedia: markMediaFavorite_favoriteMedia; favoriteMedia: markMediaFavorite_favoriteMedia
} }
export interface markMediaFavoriteVariables { export interface markMediaFavoriteVariables {
mediaId: string; mediaId: string
favorite: boolean; favorite: boolean
} }

View File

@ -1,10 +1,10 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { PhotoGalleryProps_Media } from './PhotoGallery' import { MediaGalleryFields } from './__generated__/MediaGalleryFields'
export interface PhotoGalleryState { export interface MediaGalleryState {
presenting: boolean presenting: boolean
activeIndex: number activeIndex: number
media: PhotoGalleryProps_Media[] media: MediaGalleryFields[]
} }
export type GalleryAction = export type GalleryAction =
@ -16,12 +16,12 @@ export type PhotoGalleryAction =
| GalleryAction | GalleryAction
| { type: 'openPresentMode'; activeIndex: number } | { type: 'openPresentMode'; activeIndex: number }
| { type: 'selectImage'; index: number } | { type: 'selectImage'; index: number }
| { type: 'replaceMedia'; media: PhotoGalleryProps_Media[] } | { type: 'replaceMedia'; media: MediaGalleryFields[] }
export function photoGalleryReducer( export function mediaGalleryReducer(
state: PhotoGalleryState, state: MediaGalleryState,
action: PhotoGalleryAction action: PhotoGalleryAction
): PhotoGalleryState { ): MediaGalleryState {
switch (action.type) { switch (action.type) {
case 'nextImage': case 'nextImage':
return { return {
@ -69,15 +69,19 @@ export function photoGalleryReducer(
} }
} }
export interface MediaGalleryPopStateEvent extends PopStateEvent {
state: MediaGalleryState
}
export const urlPresentModeSetupHook = ({ export const urlPresentModeSetupHook = ({
dispatchMedia, dispatchMedia,
openPresentMode, openPresentMode,
}: { }: {
dispatchMedia: React.Dispatch<GalleryAction> dispatchMedia: React.Dispatch<GalleryAction>
openPresentMode: (event: PopStateEvent) => void openPresentMode: (event: MediaGalleryPopStateEvent) => void
}) => { }) => {
useEffect(() => { useEffect(() => {
const urlChangeListener = (event: PopStateEvent) => { const urlChangeListener = (event: MediaGalleryPopStateEvent) => {
if (event.state.presenting === true) { if (event.state.presenting === true) {
openPresentMode(event) openPresentMode(event)
} else { } else {

View File

@ -1,5 +1,5 @@
import { MediaGalleryFields } from './__generated__/MediaGalleryFields'
import { gql, MutationFunction, useMutation } from '@apollo/client' import { gql, MutationFunction, useMutation } from '@apollo/client'
import { PhotoGalleryProps_Media } from './PhotoGallery'
import { import {
markMediaFavorite, markMediaFavorite,
markMediaFavoriteVariables, markMediaFavoriteVariables,
@ -24,7 +24,7 @@ export const toggleFavoriteAction = ({
media, media,
markFavorite, markFavorite,
}: { }: {
media: PhotoGalleryProps_Media media: MediaGalleryFields
markFavorite: MutationFunction<markMediaFavorite, markMediaFavoriteVariables> markFavorite: MutationFunction<markMediaFavorite, markMediaFavoriteVariables>
}) => { }) => {
return markFavorite({ return markFavorite({

View File

@ -1,8 +1,8 @@
import { photoGalleryReducer, PhotoGalleryState } from './photoGalleryReducer' import { mediaGalleryReducer, MediaGalleryState } from './mediaGalleryReducer'
import { MediaType } from '../../__generated__/globalTypes' import { MediaType } from '../../__generated__/globalTypes'
describe('photo gallery reducer', () => { describe('photo gallery reducer', () => {
const defaultState: PhotoGalleryState = { const defaultState: MediaGalleryState = {
presenting: false, presenting: false,
activeIndex: 0, activeIndex: 0,
media: [ media: [
@ -34,13 +34,13 @@ describe('photo gallery reducer', () => {
} }
test('next image', () => { test('next image', () => {
expect(photoGalleryReducer(defaultState, { type: 'nextImage' })).toEqual({ expect(mediaGalleryReducer(defaultState, { type: 'nextImage' })).toEqual({
...defaultState, ...defaultState,
activeIndex: 1, activeIndex: 1,
}) })
expect( expect(
photoGalleryReducer( mediaGalleryReducer(
{ ...defaultState, activeIndex: 2 }, { ...defaultState, activeIndex: 2 },
{ type: 'nextImage' } { type: 'nextImage' }
) )
@ -52,14 +52,14 @@ describe('photo gallery reducer', () => {
test('previous image', () => { test('previous image', () => {
expect( expect(
photoGalleryReducer(defaultState, { type: 'previousImage' }) mediaGalleryReducer(defaultState, { type: 'previousImage' })
).toEqual({ ).toEqual({
...defaultState, ...defaultState,
activeIndex: 2, activeIndex: 2,
}) })
expect( expect(
photoGalleryReducer( mediaGalleryReducer(
{ ...defaultState, activeIndex: 2 }, { ...defaultState, activeIndex: 2 },
{ type: 'previousImage' } { type: 'previousImage' }
) )
@ -71,21 +71,21 @@ describe('photo gallery reducer', () => {
test('select image', () => { test('select image', () => {
expect( expect(
photoGalleryReducer(defaultState, { type: 'selectImage', index: 1 }) mediaGalleryReducer(defaultState, { type: 'selectImage', index: 1 })
).toEqual({ ).toEqual({
...defaultState, ...defaultState,
activeIndex: 1, activeIndex: 1,
}) })
expect( expect(
photoGalleryReducer(defaultState, { type: 'selectImage', index: 100 }) mediaGalleryReducer(defaultState, { type: 'selectImage', index: 100 })
).toEqual({ ).toEqual({
...defaultState, ...defaultState,
activeIndex: 2, activeIndex: 2,
}) })
expect( expect(
photoGalleryReducer(defaultState, { type: 'selectImage', index: -5 }) mediaGalleryReducer(defaultState, { type: 'selectImage', index: -5 })
).toEqual({ ).toEqual({
...defaultState, ...defaultState,
activeIndex: 0, activeIndex: 0,
@ -93,7 +93,7 @@ describe('photo gallery reducer', () => {
}) })
test('present mode', () => { test('present mode', () => {
const openState = photoGalleryReducer(defaultState, { const openState = mediaGalleryReducer(defaultState, {
type: 'openPresentMode', type: 'openPresentMode',
activeIndex: 10, activeIndex: 10,
}) })
@ -104,7 +104,7 @@ describe('photo gallery reducer', () => {
}) })
expect( expect(
photoGalleryReducer(openState, { type: 'closePresentMode' }) mediaGalleryReducer(openState, { type: 'closePresentMode' })
).toEqual({ ).toEqual({
...openState, ...openState,
presenting: false, presenting: false,

View File

@ -2,17 +2,23 @@ import { render, screen } from '@testing-library/react'
import React from 'react' import React from 'react'
import { MediaType } from '../../../__generated__/globalTypes' import { MediaType } from '../../../__generated__/globalTypes'
import PresentMedia, { PresentMediaProps_Media } from './PresentMedia' import { MediaGalleryFields } from '../__generated__/MediaGalleryFields'
import PresentMedia from './PresentMedia'
test('render present image', () => { test('render present image', () => {
const media: PresentMediaProps_Media = { const media: MediaGalleryFields = {
__typename: 'Media', __typename: 'Media',
id: '123', id: '123',
type: MediaType.Photo, type: MediaType.Photo,
highRes: null, highRes: null,
blurhash: null,
videoWeb: null,
favorite: false,
thumbnail: { thumbnail: {
__typename: 'MediaURL', __typename: 'MediaURL',
url: '/sample_image.jpg', url: '/sample_image.jpg',
width: 300,
height: 200,
}, },
} }
@ -28,11 +34,13 @@ test('render present image', () => {
}) })
test('render present video', () => { test('render present video', () => {
const media: PresentMediaProps_Media = { const media: MediaGalleryFields = {
__typename: 'Media', __typename: 'Media',
id: '123', id: '123',
type: MediaType.Video, type: MediaType.Video,
highRes: null, highRes: null,
blurhash: null,
favorite: false,
videoWeb: { videoWeb: {
__typename: 'MediaURL', __typename: 'MediaURL',
url: '/sample_video.mp4', url: '/sample_video.mp4',
@ -40,6 +48,8 @@ test('render present video', () => {
thumbnail: { thumbnail: {
__typename: 'MediaURL', __typename: 'MediaURL',
url: '/sample_video_thumb.jpg', url: '/sample_video_thumb.jpg',
width: 300,
height: 200,
}, },
} }

View File

@ -2,11 +2,8 @@ import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { MediaType } from '../../../__generated__/globalTypes' import { MediaType } from '../../../__generated__/globalTypes'
import { exhaustiveCheck } from '../../../helpers/utils' import { exhaustiveCheck } from '../../../helpers/utils'
import { import { ProtectedImage, ProtectedVideo } from '../ProtectedMedia'
ProtectedImage, import { MediaGalleryFields } from '../__generated__/MediaGalleryFields'
ProtectedVideo,
ProtectedVideoProps_Media,
} from '../ProtectedMedia'
const StyledPhoto = styled(ProtectedImage)` const StyledPhoto = styled(ProtectedImage)`
position: absolute; position: absolute;
@ -26,16 +23,8 @@ const StyledVideo = styled(ProtectedVideo)`
height: 100vh; height: 100vh;
` `
export interface PresentMediaProps_Media extends ProtectedVideoProps_Media {
type: MediaType
highRes: null | {
__typename: 'MediaURL'
url: string
}
}
type PresentMediaProps = { type PresentMediaProps = {
media: PresentMediaProps_Media media: MediaGalleryFields
imageLoaded?(): void imageLoaded?(): void
} }

View File

@ -1,7 +1,7 @@
import React, { useState, useRef, useEffect } from 'react' import React, { useState, useRef, useEffect } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { debounce, DebouncedFn } from '../../../helpers/utils' import { debounce, DebouncedFn } from '../../../helpers/utils'
import { closePresentModeAction, GalleryAction } from '../photoGalleryReducer' import { closePresentModeAction, GalleryAction } from '../mediaGalleryReducer'
import ExitIcon from './icons/Exit' import ExitIcon from './icons/Exit'
import NextIcon from './icons/Next' import NextIcon from './icons/Next'

View File

@ -1,8 +1,9 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import styled, { createGlobalStyle } from 'styled-components' import styled, { createGlobalStyle } from 'styled-components'
import PresentNavigationOverlay from './PresentNavigationOverlay' import PresentNavigationOverlay from './PresentNavigationOverlay'
import PresentMedia, { PresentMediaProps_Media } from './PresentMedia' import PresentMedia from './PresentMedia'
import { closePresentModeAction, GalleryAction } from '../photoGalleryReducer' import { closePresentModeAction, GalleryAction } from '../mediaGalleryReducer'
import { MediaGalleryFields } from '../__generated__/MediaGalleryFields'
const StyledContainer = styled.div` const StyledContainer = styled.div`
position: fixed; position: fixed;
@ -24,7 +25,7 @@ const PreventScroll = createGlobalStyle`
type PresentViewProps = { type PresentViewProps = {
className?: string className?: string
imageLoaded?(): void imageLoaded?(): void
activeMedia: PresentMediaProps_Media activeMedia: MediaGalleryFields
dispatchMedia: React.Dispatch<GalleryAction> dispatchMedia: React.Dispatch<GalleryAction>
disableSaveCloseInHistory?: boolean disableSaveCloseInHistory?: boolean
} }

View File

@ -14,7 +14,7 @@ const authToken = vi.mocked(authentication.authToken)
describe('AuthorizedRoute component', () => { describe('AuthorizedRoute component', () => {
const AuthorizedComponent = () => <div>authorized content</div> const AuthorizedComponent = () => <div>authorized content</div>
test('not logged in', async () => { test('not logged in', () => {
authToken.mockImplementation(() => null) authToken.mockImplementation(() => null)
render( render(
@ -37,7 +37,7 @@ describe('AuthorizedRoute component', () => {
expect(screen.queryByText('authorized content')).toBeNull() expect(screen.queryByText('authorized content')).toBeNull()
}) })
test('logged in', async () => { test('logged in', () => {
authToken.mockImplementation(() => 'token-here') authToken.mockImplementation(() => 'token-here')
render( render(

View File

@ -2,10 +2,12 @@ import { useLazyQuery } from '@apollo/client'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { Navigate } from 'react-router-dom' import { Navigate } from 'react-router-dom'
import { authToken } from '../../helpers/authentication' import { authToken } from '../../helpers/authentication'
import { adminQuery } from '../../__generated__/adminQuery'
import { ADMIN_QUERY } from '../layout/Layout' import { ADMIN_QUERY } from '../layout/Layout'
export const useIsAdmin = () => { export const useIsAdmin = () => {
const [fetchAdminQuery, { data, called }] = useLazyQuery(ADMIN_QUERY) const [fetchAdminQuery, { data, called }] =
useLazyQuery<adminQuery>(ADMIN_QUERY)
useEffect(() => { useEffect(() => {
if (authToken() && !called) { if (authToken() && !called) {

View File

@ -29,7 +29,7 @@ describe('routes', () => {
expect(screen.getByText('mocked login page')).toBeInTheDocument() expect(screen.getByText('mocked login page')).toBeInTheDocument()
}) })
test('invalid page should print a "not found" message', async () => { test('invalid page should print a "not found" message', () => {
render( render(
<MemoryRouter initialEntries={['/random_non_existent_page']}> <MemoryRouter initialEntries={['/random_non_existent_page']}>
<Routes /> <Routes />

View File

@ -5,7 +5,7 @@ import { MediaSidebarMedia } from './MediaSidebar'
import { MediaType } from '../../../__generated__/globalTypes' import { MediaType } from '../../../__generated__/globalTypes'
describe('ExifDetails', () => { describe('ExifDetails', () => {
test('without EXIF information', async () => { test('without EXIF information', () => {
const media: MediaSidebarMedia = { const media: MediaSidebarMedia = {
id: '1730', id: '1730',
title: 'media_name.jpg', title: 'media_name.jpg',
@ -45,14 +45,14 @@ describe('ExifDetails', () => {
expect(screen.queryByText('Coordinates')).not.toBeInTheDocument() expect(screen.queryByText('Coordinates')).not.toBeInTheDocument()
}) })
test('with EXIF information', async () => { test('with EXIF information', () => {
const media: MediaSidebarMedia = { const media: MediaSidebarMedia = {
id: '1730', id: '1730',
title: 'media_name.jpg', title: 'media_name.jpg',
type: MediaType.Photo, type: MediaType.Photo,
exif: { exif: {
id: '1666', id: '1666',
description: "Media description", description: 'Media description',
camera: 'Canon EOS R', camera: 'Canon EOS R',
maker: 'Canon', maker: 'Canon',
lens: 'TAMRON SP 24-70mm F/2.8', lens: 'TAMRON SP 24-70mm F/2.8',

View File

@ -92,7 +92,7 @@ const ExifDetails = ({ media }: ExifDetailsProps) => {
let metadata = Object.keys(videoMetadata) let metadata = Object.keys(videoMetadata)
.filter(x => !['id', '__typename', 'width', 'height'].includes(x)) .filter(x => !['id', '__typename', 'width', 'height'].includes(x))
.reduce((prev, curr) => { .reduce((prev, curr) => {
const value = videoMetadata[curr as string] const value = videoMetadata[curr]
if (isNil(value)) return prev if (isNil(value)) return prev
return { return {

View File

@ -101,7 +101,7 @@ export interface sidebarMediaQuery_media_exif {
* The name of the lens * The name of the lens
*/ */
lens: string | null lens: string | null
dateShot: any | null dateShot: Time | null
/** /**
* The exposure time of the image * The exposure time of the image
*/ */

View File

@ -379,7 +379,7 @@ type SidebarShareProps = {
isPhoto: boolean isPhoto: boolean
loading: boolean loading: boolean
shares?: sidebarGetPhotoShares_media_shares[] shares?: sidebarGetPhotoShares_media_shares[]
shareItem(item: { variables: { id: string } }): void shareItem: (item: { variables: { id: string } }) => Promise<unknown>
} }
const SidebarShare = ({ const SidebarShare = ({

View File

@ -51,7 +51,7 @@ const formatBytes = (t: TranslationFn) => (bytes: number) => {
case 4: case 4:
return t('sidebar.download.filesize.tera_byte', '{{count}} TB', { count }) return t('sidebar.download.filesize.tera_byte', '{{count}} TB', { count })
default: default:
return new Error(`invalid byte value: ${bytes}`) throw new Error(`invalid byte value: ${bytes}`)
} }
} }
@ -172,7 +172,7 @@ const downloadMediaShowProgress =
return content return content
} }
const downloadBlob = async (blob: Blob, filename: string) => { const downloadBlob = (blob: Blob, filename: string) => {
const objectUrl = window.URL.createObjectURL(blob) const objectUrl = window.URL.createObjectURL(blob)
const anchor = document.createElement('a') const anchor = document.createElement('a')

View File

@ -8,9 +8,9 @@
// ==================================================== // ====================================================
export interface getAlbumSidebar_album { export interface getAlbumSidebar_album {
__typename: "Album"; __typename: 'Album'
id: string; id: string
title: string; title: string
} }
export interface getAlbumSidebar { export interface getAlbumSidebar {
@ -18,9 +18,9 @@ export interface getAlbumSidebar {
* Get album by id, user must own the album or be admin * Get album by id, user must own the album or be admin
* If valid tokenCredentials are provided, the album may be retrived without further authentication * If valid tokenCredentials are provided, the album may be retrived without further authentication
*/ */
album: getAlbumSidebar_album; album: getAlbumSidebar_album
} }
export interface getAlbumSidebarVariables { export interface getAlbumSidebarVariables {
id: string; id: string
} }

View File

@ -8,38 +8,38 @@
// ==================================================== // ====================================================
export interface resetAlbumCover_resetAlbumCover_thumbnail_thumbnail { export interface resetAlbumCover_resetAlbumCover_thumbnail_thumbnail {
__typename: "MediaURL"; __typename: 'MediaURL'
/** /**
* URL for previewing the image * URL for previewing the image
*/ */
url: string; url: string
} }
export interface resetAlbumCover_resetAlbumCover_thumbnail { export interface resetAlbumCover_resetAlbumCover_thumbnail {
__typename: "Media"; __typename: 'Media'
id: string; id: string
/** /**
* URL to display the media in a smaller resolution * URL to display the media in a smaller resolution
*/ */
thumbnail: resetAlbumCover_resetAlbumCover_thumbnail_thumbnail | null; thumbnail: resetAlbumCover_resetAlbumCover_thumbnail_thumbnail | null
} }
export interface resetAlbumCover_resetAlbumCover { export interface resetAlbumCover_resetAlbumCover {
__typename: "Album"; __typename: 'Album'
id: string; id: string
/** /**
* An image in this album used for previewing this album * An image in this album used for previewing this album
*/ */
thumbnail: resetAlbumCover_resetAlbumCover_thumbnail | null; thumbnail: resetAlbumCover_resetAlbumCover_thumbnail | null
} }
export interface resetAlbumCover { export interface resetAlbumCover {
/** /**
* Reset the assigned cover photo for an album * Reset the assigned cover photo for an album
*/ */
resetAlbumCover: resetAlbumCover_resetAlbumCover; resetAlbumCover: resetAlbumCover_resetAlbumCover
} }
export interface resetAlbumCoverVariables { export interface resetAlbumCoverVariables {
albumID: string; albumID: string
} }

View File

@ -8,38 +8,38 @@
// ==================================================== // ====================================================
export interface setAlbumCover_setAlbumCover_thumbnail_thumbnail { export interface setAlbumCover_setAlbumCover_thumbnail_thumbnail {
__typename: "MediaURL"; __typename: 'MediaURL'
/** /**
* URL for previewing the image * URL for previewing the image
*/ */
url: string; url: string
} }
export interface setAlbumCover_setAlbumCover_thumbnail { export interface setAlbumCover_setAlbumCover_thumbnail {
__typename: "Media"; __typename: 'Media'
id: string; id: string
/** /**
* URL to display the media in a smaller resolution * URL to display the media in a smaller resolution
*/ */
thumbnail: setAlbumCover_setAlbumCover_thumbnail_thumbnail | null; thumbnail: setAlbumCover_setAlbumCover_thumbnail_thumbnail | null
} }
export interface setAlbumCover_setAlbumCover { export interface setAlbumCover_setAlbumCover {
__typename: "Album"; __typename: 'Album'
id: string; id: string
/** /**
* An image in this album used for previewing this album * An image in this album used for previewing this album
*/ */
thumbnail: setAlbumCover_setAlbumCover_thumbnail | null; thumbnail: setAlbumCover_setAlbumCover_thumbnail | null
} }
export interface setAlbumCover { export interface setAlbumCover {
/** /**
* Assign a cover photo to an album * Assign a cover photo to an album
*/ */
setAlbumCover: setAlbumCover_setAlbumCover; setAlbumCover: setAlbumCover_setAlbumCover
} }
export interface setAlbumCoverVariables { export interface setAlbumCoverVariables {
coverID: string; coverID: string
} }

View File

@ -8,19 +8,19 @@
// ==================================================== // ====================================================
export interface sidebarAlbumAddShare_shareAlbum { export interface sidebarAlbumAddShare_shareAlbum {
__typename: "ShareToken"; __typename: 'ShareToken'
token: string; token: string
} }
export interface sidebarAlbumAddShare { export interface sidebarAlbumAddShare {
/** /**
* Generate share token for album * Generate share token for album
*/ */
shareAlbum: sidebarAlbumAddShare_shareAlbum; shareAlbum: sidebarAlbumAddShare_shareAlbum
} }
export interface sidebarAlbumAddShareVariables { export interface sidebarAlbumAddShareVariables {
id: string; id: string
password?: string | null; password?: string | null
expire?: any | null; expire?: Time | null
} }

View File

@ -8,19 +8,19 @@
// ==================================================== // ====================================================
export interface sidebarPhotoAddShare_shareMedia { export interface sidebarPhotoAddShare_shareMedia {
__typename: "ShareToken"; __typename: 'ShareToken'
token: string; token: string
} }
export interface sidebarPhotoAddShare { export interface sidebarPhotoAddShare {
/** /**
* Generate share token for media * Generate share token for media
*/ */
shareMedia: sidebarPhotoAddShare_shareMedia; shareMedia: sidebarPhotoAddShare_shareMedia
} }
export interface sidebarPhotoAddShareVariables { export interface sidebarPhotoAddShareVariables {
id: string; id: string
password?: string | null; password?: string | null
expire?: any | null; expire?: Time | null
} }

View File

@ -8,22 +8,22 @@
// ==================================================== // ====================================================
export interface sidebarProtectShare_protectShareToken { export interface sidebarProtectShare_protectShareToken {
__typename: "ShareToken"; __typename: 'ShareToken'
token: string; token: string
/** /**
* Whether or not a password is needed to access the share * Whether or not a password is needed to access the share
*/ */
hasPassword: boolean; hasPassword: boolean
} }
export interface sidebarProtectShare { export interface sidebarProtectShare {
/** /**
* Set a password for a token, if null is passed for the password argument, the password will be cleared * Set a password for a token, if null is passed for the password argument, the password will be cleared
*/ */
protectShareToken: sidebarProtectShare_protectShareToken; protectShareToken: sidebarProtectShare_protectShareToken
} }
export interface sidebarProtectShareVariables { export interface sidebarProtectShareVariables {
token: string; token: string
password?: string | null; password?: string | null
} }

View File

@ -8,17 +8,17 @@
// ==================================================== // ====================================================
export interface sidebareDeleteShare_deleteShareToken { export interface sidebareDeleteShare_deleteShareToken {
__typename: "ShareToken"; __typename: 'ShareToken'
token: string; token: string
} }
export interface sidebareDeleteShare { export interface sidebareDeleteShare {
/** /**
* Delete a share token by it's token value * Delete a share token by it's token value
*/ */
deleteShareToken: sidebareDeleteShare_deleteShareToken; deleteShareToken: sidebareDeleteShare_deleteShareToken
} }
export interface sidebareDeleteShareVariables { export interface sidebareDeleteShareVariables {
token: string; token: string
} }

View File

@ -15,7 +15,7 @@ import {
getActiveTimelineImage as getActiveTimelineMedia, getActiveTimelineImage as getActiveTimelineMedia,
timelineGalleryReducer, timelineGalleryReducer,
} from './timelineGalleryReducer' } from './timelineGalleryReducer'
import { urlPresentModeSetupHook } from '../photoGallery/photoGalleryReducer' import { urlPresentModeSetupHook } from '../photoGallery/mediaGalleryReducer'
import TimelineFilters from './TimelineFilters' import TimelineFilters from './TimelineFilters'
import client from '../../apolloClient' import client from '../../apolloClient'

View File

@ -1,7 +1,7 @@
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { MediaThumbnail } from '../photoGallery/MediaThumbnail' import { MediaThumbnail } from '../photoGallery/MediaThumbnail'
import { PhotoFiller } from '../photoGallery/PhotoGallery' import { PhotoFiller } from '../photoGallery/MediaGallery'
import { import {
toggleFavoriteAction, toggleFavoriteAction,
useMarkFavoriteMutation, useMarkFavoriteMutation,

View File

@ -13,7 +13,7 @@ export interface earliestMedia_myMedia {
/** /**
* The date the image was shot or the date it was imported as a fallback * The date the image was shot or the date it was imported as a fallback
*/ */
date: any date: Time
} }
export interface earliestMedia { export interface earliestMedia {

View File

@ -84,7 +84,7 @@ export interface myTimeline_myTimeline {
/** /**
* The date the image was shot or the date it was imported as a fallback * The date the image was shot or the date it was imported as a fallback
*/ */
date: any date: Time
} }
export interface myTimeline { export interface myTimeline {
@ -98,5 +98,5 @@ export interface myTimelineVariables {
onlyFavorites?: boolean | null onlyFavorites?: boolean | null
limit?: number | null limit?: number | null
offset?: number | null offset?: number | null
fromDate?: any | null fromDate?: Time | null
} }

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { myTimeline_myTimeline } from './__generated__/myTimeline' import { myTimeline_myTimeline } from './__generated__/myTimeline'
import { TimelineGroup, TimelineGroupAlbum } from './TimelineGallery' import { TimelineGroup, TimelineGroupAlbum } from './TimelineGallery'
import { GalleryAction } from '../photoGallery/photoGalleryReducer' import { GalleryAction } from '../photoGallery/mediaGalleryReducer'
import { isNil } from '../../helpers/utils' import { isNil } from '../../helpers/utils'
export interface TimelineMediaIndex { export interface TimelineMediaIndex {
@ -179,8 +179,8 @@ export const getActiveTimelineImage = ({
mediaState: TimelineGalleryState mediaState: TimelineGalleryState
}) => { }) => {
if ( if (
Object.values(mediaState.activeIndex).reduce( Object.values(mediaState.activeIndex).reduce<boolean>(
(acc, next) => next == -1 || acc, (acc, next) => next === -1 || acc,
false false
) )
) { ) {

View File

@ -1,21 +1,19 @@
import classNames, { Argument as ClassNamesArg } from 'classnames' import classNames, { Argument as ClassNamesArg } from 'classnames'
import { overrideTailwindClasses } from 'tailwind-override' import { overrideTailwindClasses } from 'tailwind-override'
// import { overrideTailwindClasses } from 'tailwind-override'
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface DebouncedFn<F extends (...args: any[]) => any> { export interface DebouncedFn<F extends (...args: unknown[]) => unknown> {
(...args: Parameters<F>): void (...args: Parameters<F>): void
cancel(): void cancel(): void
} }
export function debounce<T extends (...args: any[]) => any>( export function debounce<F extends (...args: unknown[]) => unknown>(
func: T, func: F,
wait: number, wait: number,
triggerRising?: boolean triggerRising?: boolean
): DebouncedFn<T> { ): DebouncedFn<F> {
let timeout: number | undefined = undefined let timeout: number | undefined = undefined
const debounced = (...args: Parameters<T>) => { const debounced = (...args: Parameters<F>) => {
if (timeout) { if (timeout) {
clearTimeout(timeout) clearTimeout(timeout)
timeout = undefined timeout = undefined
@ -37,11 +35,12 @@ export function debounce<T extends (...args: any[]) => any>(
return debounced return debounced
} }
export function isNil(value: any): value is undefined | null { export function isNil(value: unknown): value is undefined | null {
return value === undefined || value === null return value === undefined || value === null
} }
export function exhaustiveCheck(value: never) { export function exhaustiveCheck(value: never) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Exhaustive check failed with value: ${value}`) throw new Error(`Exhaustive check failed with value: ${value}`)
} }

View File

@ -4,24 +4,20 @@ import { useCallback, useEffect, useRef, useState } from 'react'
interface ScrollPaginationArgs<D> { interface ScrollPaginationArgs<D> {
loading: boolean loading: boolean
data: D | undefined data: D | undefined
fetchMore(args: { fetchMore: (args: {
variables: { offset: number } variables: { offset: number }
}): Promise<ApolloQueryResult<D>> }) => Promise<ApolloQueryResult<D>>
// eslint-disable-next-line @typescript-eslint/no-explicit-any getItems: (data: D) => unknown[]
getItems(data: D): any[]
} }
type ScrollPaginationResult = { type ScrollPaginationResult = {
finished: boolean finished: boolean
containerElem(node: null | Element): void containerElem: (node: null | Element) => void
} }
function useScrollPagination<D>({ const useScrollPagination: <D>(
loading, args: ScrollPaginationArgs<D>
fetchMore, ) => ScrollPaginationResult = ({ loading, fetchMore, data, getItems }) => {
data,
getItems,
}: ScrollPaginationArgs<D>): ScrollPaginationResult {
const observer = useRef<IntersectionObserver | null>(null) const observer = useRef<IntersectionObserver | null>(null)
const observerElem = useRef<Element | null>(null) const observerElem = useRef<Element | null>(null)
const [finished, setFinished] = useState(false) const [finished, setFinished] = useState(false)

View File

@ -3,12 +3,12 @@ import { useState } from 'react'
export type UrlKeyValuePair = { key: string; value: string | null } export type UrlKeyValuePair = { key: string; value: string | null }
export type UrlParams = { export type UrlParams = {
getParam(key: string, defaultValue?: string | null): string | null getParam: (key: string, defaultValue?: string | null) => string | null
setParam(key: string, value: string | null): void setParam: (key: string, value: string | null) => void
setParams(pairs: UrlKeyValuePair[]): void setParams: (pairs: UrlKeyValuePair[]) => void
} }
function useURLParameters(): UrlParams { const useURLParameters: () => UrlParams = () => {
const [urlString, setUrlString] = useState(document.location.href) const [urlString, setUrlString] = useState(document.location.href)
const url = new URL(urlString) const url = new URL(urlString)

View File

@ -10,20 +10,23 @@ import { exhaustiveCheck, isNil } from './helpers/utils'
export type TranslationFn = TFunction<'translation'> export type TranslationFn = TFunction<'translation'>
export function setupLocalization(): void { export function setupLocalization(): void {
i18n.use(initReactI18next).init({ i18n
lng: 'en', .use(initReactI18next)
fallbackLng: 'en', .init({
returnNull: false, lng: 'en',
returnEmptyString: false, fallbackLng: 'en',
returnNull: false,
returnEmptyString: false,
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
}, },
react: { react: {
useSuspense: import.meta.env.PROD, useSuspense: import.meta.env.PROD,
}, },
}) })
.catch(err => console.error('Failed to setup localization', err))
} }
const SITE_TRANSLATION = gql` const SITE_TRANSLATION = gql`

View File

@ -78,6 +78,7 @@ registerRoute(
// This allows the web app to trigger skipWaiting via // This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'}) // registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener('message', event => { self.addEventListener('message', event => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (event.data && event.data.type === 'SKIP_WAITING') { if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting() self.skipWaiting()
} }

View File

@ -45,12 +45,16 @@ export function register(config?: Config) {
// Add some additional logging to localhost, pointing developers to the // Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation. // service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => { navigator.serviceWorker.ready
console.log( .then(() => {
'This web app is being served cache-first by a service worker.' + console.log(
'worker. To learn more, visit https://cra.link/PWA' 'This web app is being served cache-first by a service worker.' +
) 'worker. To learn more, visit https://cra.link/PWA'
}) )
})
.catch(() => {
console.log('Failed to load service worker')
})
} else { } else {
// Is not localhost. Just register service worker // Is not localhost. Just register service worker
registerValidSW(swUrl, config) registerValidSW(swUrl, config)
@ -139,7 +143,7 @@ export function unregister() {
.then(registration => { .then(registration => {
registration.unregister() registration.unregister()
}) })
.catch(error => { .catch((error: Error) => {
console.error(error.message) console.error(error.message)
}) })
} }

View File

@ -17,5 +17,5 @@
"jsx": "react-jsx", "jsx": "react-jsx",
"types": ["vitest/globals", "@testing-library/jest-dom"] "types": ["vitest/globals", "@testing-library/jest-dom"]
}, },
"include": ["src", "vite.config.ts"] "include": ["src", "vite.config.ts", ".eslintrc.js", "apollo.config.js"]
} }